Introducing AM - Attribute Modules for CSS

Moving beyond class-based styling

A few months ago, I read an article by Harry Roberts where he introduced an interesting concept for working with related classes in CSS. In his article, he describes the use of the [] characters in class attributes to help understand their purpose at a quick glance. He presents this example, arguing that it makes the class declaration more scannable - that is, more understandable at a glance:

<div class="[ foo  foo--bar ]  [ baz  baz--foo ]">

I must admit, I was initially extremely uncomfortable with the technique. The idea of classes with names like [ and ], that match no CSS, that are repeated within a single class attribute, that are purely designed for humans rather than the browser seemed, well, wrong. I still think that, actually, but it got me thinking about markup & semantics much more deeply, so thanks Harry!

As I looked into it, several people were suggesting similar approaches, such as using /ย (Ben Everard), or |ย (Stephen Nolan), but the feeling of unnaturalness persisted. All I could think was:

How do you have so many classes that you need more classes to make things readable?

Because, letโ€™s be clear, this is madness. Readable, scannable HTML is a worthy goal, but these kinds of techniques show that thereโ€™s something fundamentally broken with class-based styling.

More vs fewer classes - a brief aside

The surprising thing was, while the presence of so many classes in the markup was unsettling to me, people like Harry were just so damn persuasive. Appealing to things like OOCSS and the Single Responsibility Principle, and from my own experience building a series of sites of increasing complexity, I could tell there was value in the decomposition of styling behaviour, but it wasnโ€™t until recently that I found a way to implement it that I was happy with.

I had previously adopted a version of BEM that emphasised isolation over reuse &dash; each new block inherits no styling by default, allowing components to be developed separately and avoids the risk of breaking something elsewhere on the site. But the tradeoff there is fragmentation &dash; when you find yourself with 10 different link styles, 12 shades of blue, 18 subtly different button styles etc. Nicole Sullivan, the creator of OOCSS, gave a fantastic presentation last year in Melbourne that spoke about how common that problem was, and how to recover from it.

For me, it felt like the accepted solution was to dive into the capabilities of CSS pre-processors in order to have the isolation of BEM but the consistency of OOCSS. For example, instead of this:

<a class='btn large rounded'>
.btn { /* button styles */ }
.large { /* global large-type modifier */ }
.rounded { /* global rounded-border modifier */ }

you would have:

<a class='btn btn--large btn--rounded'>
.btn { /* button styles */ }
.btn--large {
  @extend %large-type;
}
.btn--rounded {
  @extend %rounded-borders;
}

I ended up with files full of mixins and placeholders like _typography.scss and _brand.scss, which allowed me to keep a handle on fragmentation, but also maintain by-default style isolation for each new component. And so things were ok, for a while.

Modifiers: how the M breaks BEM

Doing any research on the topic of CSS class naming & maintainability, youโ€™re bound to come across Nicolas Gallagherโ€™s excellent article โ€œAbout HTML semantics and front-end architectureโ€. One part in particular caught my attention, which he calls โ€˜single-classโ€™ vs โ€˜multi-classโ€™ patterns for modifiers. To summarize, the two potential versions of your HTML look like this:

<a class='btn--large'> <!-- Single class -->
<a class='btn btn--large'> <!-- Multi class -->

Thatโ€™s facilitated through two alternative CSS patterns:

/* Single class */
.btn, .btn--large { /* base button styles */ }
.btn--large { /* large button styles */ }

/* Multi class */
.btn { /* base button styles */ }
.btn--large { /* large button styles */ }

The difference here is whether btn--large is sufficient on its own, or whether it depends on the class btn being present. The single-class pattern says yes, it feels simpler and avoids the case where someone forgets to include btn. Itโ€™s also less repetitious, and with SASSโ€™s @extend functionality it doesnโ€™t feel like much of a burden on the CSS side, but it has a truly fatal flaw.

Contextual overrides

Letโ€™s say all your buttons have a background colour, except for those in your top navigation bar. With the multi-class pattern, all buttons, large or small, rounded or square, etc, still include the class btn, so you can target them like so:

header > nav > .btn { background: none; }

With the single-class pattern, we donโ€™t know which variant of button we might be overriding, so weโ€™re forced to do:

header > nav {
  .btn, .btn--large, .btn--rounded { background: none; }
}

Obviously, this is not ideal - adding another button variant means checking anywhere that overrides button styles and adding another class. This is a bit of a deal breaker, so most people simply argue for a return to the multiple-class style (Nicholas Gallagher, Ben Smithett). Iโ€™ve seen some alternative proposals, such as Tommy Marshallโ€™s or Ben Frainโ€™s, that use the attribute prefix selector ^= which allows you to test whether an attribute value starts with a certain string, e.g.:

<a class='btn--large'>
[class^='btn'] { /* base button styles */ }
.btn--large { /* large button styles */ }
header > nav > [class^='btn'] { /* Overrides for all buttons */ }

This achieves easy contextual overrides for a single-class pattern, but itโ€™s too brittle to be a serious option. Most damningly, if another class appears before btn--large, the prefix selector doesnโ€™t match, and everything breaks. Also, thereโ€™s no obvious way of permitting multiple variants such as btn--large--rounded.

I appreciate the inventiveness of this approach, but itโ€™s a dead end. And itโ€™s where I got stuck, too, until something occurred to me.

Why the fuck are we using classes?

Forgive my bluntness, but can anyone give me a good reason why classes are the only place we add styling information? Hereโ€™s what the HTML living standard has to say:

3.2.5.7 The class attribute

The attribute, if specified, must have a value that is a set of space-separated tokens representing the various classes that the element belongs to.

There are no additional restrictions on the tokens authors can use in the class attribute, but authors are encouraged to use values that describe the nature of the content, rather than values that describe the desired presentation of the content.

So yes, it makes perfect sense that we use classes to describe โ€˜the nature of the contentโ€™, but it feels like weโ€™re asking more of the humble class attribute than it can give us. This one attribute holds everything, from enormous BEM-style names like primary-nav__sub-nav--current to utilities like u-textTruncate or left or clearfix, to JavaScript hooks like js-whatevs, and so we spend a lot of time coming up with names that donโ€™t conflict with any others but are still vaguely readable.

Itโ€™s manageable through convention & discipline, and even helped by techniques like Harryโ€™s at the top of this article, but the truth is weโ€™re operating in a global namespace, and no amount of conventions can change that. Which is what makes AM different.

But before we can talk about that, we need to brush up on a lesser-known feature of CSS.

Welcome ~=, the magic selector

It turns out browsers since IE7 have had a particularly powerful CSS rule called the space-separated attribute selector, described here on CSS Tricks. It matches arbitrary attribute values, separated by spaces, just like they were classes. So the following two lines of CSS are equivalent:

.dat-class { /* dem styles */ };
[class~='dat-class'] { /* dem styles */ };

In the same way that <div class='a b c'> doesnโ€™t care which order the a, b and c are in, or what else is present, neither does the ~= selector. But ~= isnโ€™t limited to the class attribute, it can work on any attribute. And itโ€™s the key to a whole new approach.

Attribute Modules

Attribute Modules, or AM, at its core is about defining namespaces for your styles to live in. Letโ€™s begin with a simple example, a grid, first as classes:

<div class="row">
    <div class="column-12">Full</div>
</div>
<div class="row">
    <div class="column-4">Thirds</div>
    <div class="column-4">Thirds</div>
    <div class="column-4">Thirds</div>
</div>
.row { /* max-width, clearfixes */ }
.column-1 { /* 1/12th width, floated */ }
.column-2 { /* 1/6th width, floated */ }
.column-3 { /* 1/4th width, floated */ }
.column-4 { /* 1/3rd width, floated */ }
.column-5 { /* 5/12th width, floated */ }
/* etc */
.column-12 { /* 100% width, floated */ }

Now letโ€™s build it with attribute modules. We have two modules, rows and columns. Rows, so far, have no variations. Columns have 12.

<div am-Row>
    <div am-Column="12">Full</div>
</div>
<div am-Row>
    <div am-Column="4">Thirds</div>
    <div am-Column="4">Thirds</div>
    <div am-Column="4">Thirds</div>
</div>
[am-Row] { /* max-width, clearfixes */ }
[am-Column~="1"] { /* 1/12th width, floated */ }
[am-Column~="2"] { /* 1/6th width, floated */ }
[am-Column~="3"] { /* 1/4th width, floated */ }
[am-Column~="4"] { /* 1/3rd width, floated */ }
[am-Column~="5"] { /* 5/12th width, floated */ }
/* etc */
[am-Column~="12"] { /* 100% width, floated */ }

The first thing you will notice is the am- prefix. This is a core part of AM, and ensures that attribute modules do not conflict with existing attributes. You can use any prefix you like &dash; Iโ€™ve experimented with ui-, css- and others, but settled on am- for these examples. If HTML validity is important to you or your project, simply choose a prefix that begins with data-, the idea is the same.

The second thing you might notice is that values like "1", "4" or "12" would make terrible class names &dash; theyโ€™re far too generic and the chances of collisions would be high. But because weโ€™ve defined our own namespace, in effect carving off a little place for us to work, we are free to use the most concise, meaningful tokens we choose.

Flexibility with attribute values

So far, the differences are pretty minor. But since each module defines its own namespace, letโ€™s try a slightly different scheme for our values:

<div am-Row>
    <div am-Column>Full</div>
</div>
<div am-Row>
    <div am-Column="1/3">Thirds</div>
    <div am-Column="1/3">Thirds</div>
    <div am-Column="1/3">Thirds</div>
</div>
[am-Row] { /* max-width, clearfixes */ }
[am-Column] { /* 100% width, floated */ }
[am-Column~="1/12"] { /* 1/12th width */ }
[am-Column~="1/6"] { /* 1/6th width */ }
[am-Column~="1/4"] { /* 1/4th width */ }
[am-Column~="1/3"] { /* 1/3rd width */ }
[am-Column~="5/12"] { /* 5/12ths width */ }
/* etc */

Now weโ€™re able to use naming that makes sense for our domain &dash; a width of 1/3 makes immediate sense whereas 4 needs us to remember weโ€™re using a 12-column grid. But weโ€™ve also been able to define a default style for all columns &dash; that is, the attribute column with no value is treated as a full-width column. And as a bonus, weโ€™ve also been able to move repeated logic (the fact that columns are floated left) into this attribute rule.

Styling both attributes and values

This is one of the key benefits of this approach. The presence of an attribute, e.g. am-Button, can and should be styled. The particular values of each attribute then alter and adapt these base styles.

In the grid example above, weโ€™re doing exactly that: the markup am-Column="1/3" matches both [am-Column] and [am-Column~="1/3"], so the result is the base styles + variations. It gives us a way to capture the fact that all columns are columns without needing to duplicate classes or use SASSโ€™s @extend functionality.

The zero-class approach to BEM modifiers

Back to our single-class vs multi-class patterns for BEM modifiers, AM gives us a zero-class option. For our button examples above, this is how the markup looks:

<a am-Button>Normal button</a>
<a am-Button='large'>Large button</a>
<a am-Button='rounded'>Rounded button</a>
<a am-Button='large rounded'>Large rounded button</a>
[am-Button] { /* base button styles */ }
[am-Button~="large"] { /* large button styles */ }
[am-Button~="rounded"] { /* round button styles */ }

By creating a new Attribute Module am-Button, we can separate out the styles that are common to all buttons, to those that make a button large, to those that round a buttonโ€™s corners. Not only can we then freely combine these variations (e.g. am-Button='large rounded'), we can also target the attribute itself for any contextual overrides:

header > nav > [am-Button] { background: none; }

Now it doesnโ€™t matter what variant of button we choose to use, or how many variants we choose to define, the point is that all buttons will match the selector [am-Button], so we know our override will be valid.

The AMCSS Project

Myself, Ben Schwarz and Ben Smithett have begun work on a formal specification for AM. If youโ€™d like to read more about how these techniques extend to blocks, elements, breakpoints & more, head there, and if you have questions, please raise an issue.

Weโ€™ve also started a documentation site with a wider range of AM examples at amcss.github.io. If youโ€™re interesting in contributing feedback, examples, edge-cases or would like us to point to your own AM libraries, please reach out to us on GitHub.