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:
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 ‐ 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 ‐ 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 ‐ 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 ‐ 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 ‐ 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 ‐ 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.