modular-css v25.3.0

# modular-css Guide

# Introduction

# Overview

modular-css implements the best features of the CSS Modules spec and then adds on several extra features to make for a smoother developer experience. It has support for almost all the different ways you might want to work, easy extensibility, and allows for using the entire ecosystem of postcss plugins to customize your CSS however you see fit.

# Why

CSS Modules has been abandoned for anyone who doesn’t use webpack, and the webpack version doesn’t support the features we need. Attempts to improve that situation have been unsuccessful for a variety of reasons. Thus, a perfect storm of compelling reasons to learn PostCSS was found.

Also because this:

Green pills look gross

# How

There are a lot of different ways to use modular-css, pick your favorite!

# Features

modular-css implements the best features of the CSS Modules spec and then adds on several extra features to make for a smoother developer experience.

# Selector Scoping

By default all CSS selectors live in the global scope of the page and are chosen based on specificity rules. This has proven to be a model that makes it difficult to succeed and incredibly easy to dig yourself into a hole you can’t climb out of. modular-css scopes all selectors to the local file by default, ensuring that your CSS is always exactly as specific as it should be.

Edit in REPL
.wooga {
    color: red;
}

will be output as

.mcf250d69f_wooga {
    color: red;
}

By default the selector scoping is based off hashing the contents of the file but you can also provide your own custom function.

Using these now-mangled selectors would be problematic, if modular-css didn’t give you the tools required to use them easily. When using the browserify transform any require() calls for CSS files will instead return an object where the keys match the classes/ids defined in the requested CSS file.

var css = require("./styles.css");

// css is:
/*
{
    wooga : "mcf250d69f_wooga",
    booga : "mcf250d69f_wooga mcf250d69f_booga",
    ...
}
*/

// So then you can render that class trivially
const html = `<div class="${css.wooga}">Wooga</div>`;

// which then has the properly scoped selector
// <div class="mcf250d69f_wooga">Wooga</div>

// Also easy-to-use with JSX!
const jsx = <div class={css.wooga}>Wooga</div>;

These arrays of selectors can then be applied to elements using the much more nicely-named object keys and you’re off to the races.

You can opt out of selector scoping by wrapping your classes/ids in the :global() pseudo-class, this will prevent them from being renamed but they will still be available in the module’s exported object.

Edit in REPL
:global(.global) {
    color: red;
}

when transformed to JS looks like this

var css = require("./styles.css");

// css is:
/*
{
    global : "global"
}
*/

Selector scoping is only done on simple classes/ids, any selectors containing tags or pseudo-selectors won’t be exported.

:global() is treated the same as a CSS pseudo-class and therefore cannot wrap multiple comma seperated rules. For example if you’re using a CSS reset the following is required:

Edit in REPL
/* Local Scoped */
ol, ul {
    list-style: none;
}

/* Global Scoped (Wrong!) */
:global(ol, ul) {
    list-style: none;
}

/* Global Scoped (Correct!) */
:global(ol), :global(ul) {
    list-style: none;
}

Adding :global() to every comma seperated rule would be tedious when using something like Eric Meyer’s CSS Reset. Therefore it is recommended that you seperate the reset in to its own file, and make use of the postcss-import module with the after or done hooks to include the file when modular-css has finished processing. You would then need to include @import "reset.css"; somewhere in one of your CSS files.

# Style Composition

Selector limitations mean that it’s difficult to use complicated selectors, so to enable building anything of complexity you can compose selectors. These compositions can be within a file, reference global CSS class, or even pull in classes defined in other files.

Edit in REPL
.composable {
    background: black;
}

.local {
    composes: composable;

    color: red;
}

/* Will be stripped from the CSS output because it doesn't */
/* contain any actual rules */
.removed {
    composes: local;
}

When this file is required the JS object will contain the expected keys, but the arrays will now contain more values.

var css = require("./style.css");

// css is:
/*
{
    composable : "dafdfcc_composable",
    local      : "dafdfcc_composable aeacf0c_local",
    removed    : "dafdfcc_composable aeacf0c_local aeacf0c_removed"
}
*/

Composition also works between files, by providing the source file.

Edit in REPL
/* === style-guide.css === */

.body {
    margin: 10px;
    height: 100%;
}

/* === home-page.css === */

.body {
    composes: body from "/style-guide.css";

    padding: 10px;
}

Styles can also be composed directly from the global scope to help with interoperability with CSS frameworks or other non-module styles.

Edit in REPL
.box {
    composes: d-flex, px-4, py-3 from global;

    color: blue;
}

If you’re going to be doing a lot of composition with another file you can store the filename into a value for ease of referencing.

Edit in REPL
/* === style-guide.css === */
.heading {
    font-size: 140%;
}

.body {
    margin: 10px;
    height: 100%;
}

/* === home-page.css === */
@value guide: "/style-guide.css";

.head {
    composes: heading from guide;

    font-size: 120%;
}

.body {
    composes: body from guide;

    padding: 10px;
}

# Values

Values are re-usable pieces of content that can be used instead of hardcoding colors, sizes, media queries, or most other forms of CSS values. They’re automatically replaced during the build with their defined value, and can also be composed between files for further re-use or overriding. They’re effectively static versions of CSS variables, but with a few extra build-time powers we’ll get into later.

Edit in REPL
@value alert: #F00;
@value small: (max-width: 600px);

@media small {
    .alert { color: alert; }
}

will output

@media (max-width: 600px) {
    .alert { color: #F00; }
}

# Importing @values

@values can be imported from another file by using a slightly different syntax.

Edit in REPL
/* === colors.css === */
@value main: red;
@value bg: white;

/* === site.css === */
@value main from "./colors.css";

body {
    color: main;
}

It’s also possible to import multiple values at once.

Edit in REPL
/* === colors.css === */
@value main: red;
@value bg: white;

/* === site.css === */
@value main, bg from "./colors.css";

body {
    color: main;
    background: bg;
}

# Namespaced @values

@values can be imported as a namespace which provides a convenient shorthand way to access a bunch of shared values from a file.

Edit in REPL
/* === colors.css === */
@value main: red;
@value bg: white;

/* === site.css === */
@value * as colors from "./colors.css";

body {
    color: colors.main;
    background: colors.bg;
}

# Wildcard @values

It’s possible to import all the @value definitions from another file into the current one. Any local @value declarations will override the imported values.

Edit in REPL
/* === colors.css === */
@value main: red;
@value bg: white;

/* === site.css === */
@value * from "./colors.css";
@value bg: black;

body {
    /* black */
    background: bg;
    /* red */
    color: main;
}

Since all files in modular-css with @value declaration make that value available to other files it’s possible to use the wildcard imports feature to build complex theming systems. When using wildcard imports all the @values from the source file are re-exported by the file doing the importing.

Edit in REPL
/* === colors.css === */
@value main: red;
@value bg: white;

/* === mobile-colors.css === */
@value * from "./colors.css";
@value bg: gray;

/* === site.css === */
@value * as colors from "./mobile-colors.css";

body {
    /* gray */
    background: colors.bg;
    /* red */
    color: colors.main;
}

# Other Features

These features can help when you find yourself bumping up against the edges of a few specific problems in modular-css but are best used sparingly.

# Overriding Styles

Sometimes a component will need some customization for use in a specific location/design, but you don’t want to bake that customization into the component.:external(...) helps you solve that problem.

In this case we’ve got an input component that is normally 100% of the width of its container, but when it’s within the fieldset component it should only be half as wide.

Edit in REPL
/* == input.css == */
.input {
    width: 100%;
}

/* == fieldset.css == */
.fieldset :external(input from "./input.css") {
    width: 50%;
}

will create output like this

.mcd8e24dd1_input {
    width: 100%;
}

.mcf250d69f_fieldset .mcd8e24dd1_input {
    width: 50%;
}

# Composing Files

When necessary you can also use the @composes at-rule to enable composing an entire CSS file, instead of going rule-by-rule. This is mostly useful when you’ve got a base style you want to apply to a component but you need to modify just a single style from the base. Instead of manually creating shadowed versions of all the classes in the base CSS, you can use @composes to save a bunch of repetition and potential for fat-fingering.

Edit in REPL
/* == base.css == */
.header {
    color: red;
}

.body {
    color: blue;
}

/* Imagine 20-30 more styles here... */

/* == custom.css == */
@composes "./base.css";

.title {
    composes: header;

    background: red;
}

When custom.css is required the JS object will contain the selectors defined in that file as well as any selectors from base.css. It also allowed for the .title class in custom.css to use composes: header even though .header wasn’t defined in that file at all.

var css = require("./custom.css");

// css is:
/*
{
    // from custom.css
    title   : "dafdfcc_header aeacf0c_title",

    // plus everything from base.css
    header : "dafdfcc_header",
    body   : "dafdfcc_body"
}
*/

There can be only one @composes declaration per file, just to keep things straightforward. Chaining of files is supported as well, the files will be processed in the correct order based on their dependencies and files at the end of the chain will include all of the rules from every other file they & their dependencies included. This feels like it could get hard to manage quickly, so it’s recommended to use @composes only when necessary and try to avoid reaching for it as the very first solution to a problem!

# vs CSS Modules

While modular-css was originallly built directly off the CSS Modules spec, during the course of development some decisions have been made that have broken 100% compatibility. In general these changes have been made in an attempt to try and add consistency or pave cowpaths. There have also been a few feature additions that enable solving new classes of problems or fix pain points in the spec.

# :global

In CSS Modules the :global pseudo-class allows usage with or without parentheses around its arguments.

/* CSS Modules */
:global .one { ... }
:global(.one) { ... }

In modular-css only the parenthesized form is allowed to reduce ambiguity around which selectors/names are being made global.

/* modular-css */
:global(.one) { ... }

# Scope

In CSS Modules it is possible to switch back and forth from local and global scope using :global and :local.

/* CSS Modules */
.localA :global .global-b .global-c :local(.localD.localE) .global-d { ... }

In modular-css all CSS is local by default, and since :global() is required to use parentheses there is no need for the :local pseudo.

/* modular-css */
.localA :global(.global-b .global-c) .localD.localE :global(.global-d) { ... }

# Style overriding

In CSS Modules there is no support for modifying styles in rules that aren’t direct ancestors via composes.

Discussion around potential solutions was happening in css-modules/css-modules#147 but eventually dried up.

modular-css has implemented the :external() proposal from that issue. More context is available in the Overriding Styles section.

Edit in REPL
/* === input.css === */
.input {
    width: 100%;
}

/* === fieldset.css === */
.fieldset :external(input from "./input.css") {
    width: 50%;
}

# Namespaces

CSS Modules allows for importing multiple values from an external file.

/* CSS Modules */
@value one, two, three, red, blue, green, small, medium, large from "./constants.css";

.a {
  color: red;
  width: small;
}

modular-css implements a suggestion made in css-modules/css-modules#186 to allow importing all of a file’s exported values and aliasing it for easy use. More documentation can be found in the Namespaced values section.

Edit in REPL
/* === constants.css === */
@value red: #F00;
@value small: 0.5rem;
/* ... */

/* === namespaced.css === */
@value * as constants from "/constants.css";

.a {
  color: constants.red;
  width: constants.small;
}

# Support

CSS Modules is really only fully-supported as part of webpack these days. The repo is effectively mothballed, and there appears to be almost no one supporting the current codebase. Since webpack is the only live version & we prefer to use rollup for it’s cleaner output and smaller bundles we needed an approach that could be flexibly be used with a variety of tools.

# composes location

After a long time with composes being required to be the first declaration in a rule the ability to use a tool like stylelint-order has reduced the need for modular-css to enforce positional requirements. After a push in #645 the requirement was removed in #646, which was published in v25. This is a change from CSS Modules, which continues to require that composes be the first declaration in a rule.