Custom Attributes are Fast, Good, and Cheap

After years working with HTML and CSS, we have come to a surprising conclusion.

Avoid using CSS classes and className and classList when deriving styles based on some “state.” Instead, prefer HTML element attributes, and CSS attribute selectors.
— Me, this week, all the time

I’ve thought about this practice for a long time, but it took until yesterday (27 Jan 2018) to motivate me enough to finish this post.

[This post first begun August 28, 2016, was not completed until August 16, 2019.]

[This post received an update to the attribute selectors list, and the data-entity refactoring example on May 11, 2020.]

See original tweet

CSS class selectors…

Classes (and tags) are categorical selectors (the “whatness” of something). But thanks to inheritance, and the specificity of the class selector, if you stick with classes, you end up with overwhelming whatness.

Lessons of the past…

Some things we thought we learned along the way:

  • do not use tables to structure page layout
  • do not use ID selectors in CSS (too specific, not generic enough)
  • do not use tagName selectors (too general, definitions have to be overridden)
  • use class selectors and avoid the cascade
  • use selectors in combination to override the cascade
  • use descendant selectors to name-space rules
  • do not over-use descendant selectors

These lessons are meant to shield us from hard-to-maintain, hard-to-correct mistakes.

But, they reduce our thinking, along with the pre-processing tools like SASS and patterns like BEM (Block, Element, Modifier), into producing classes for everything… including states.

States are not classes.

A stop light is red, not because it is a stop light, but because when the state is stop, the view or display is red.

Stop relying on them.

Although I’m speaking strictly of CSS classes (and tagNames), classes (as a class) attempt to define some experience based on the category a thing is or belongs to – rather than what the thing does. Classes are both too general (because they are categorical) and too specific (because elements can belong to more than one category – i.e., inherit from multiple ancestors).

Communicating state.

Classes in combination can work – that is the basis for utility-first CSS frameworks – for example:

<div class="max-w-sm mx-auto flex p-6 bg-white rounded-lg shadow-xl">...</div>

But utility-first CSS has nothing to do with communicating state, and everything to do with re-usable composition of rules.

Of course, the industry habit has been to create three classes for re-use, them combine them, e.g., as a signin.main.button selector, to specify one-off styles when all three are present in a unique situation – to override inherited specificity.

Classes are the wrong “abstraction”.

You can still use a class for an element, but should avoid multi-classing anything.

Classes are not singular attributes, they represent groups of attributes. Combining more than one class into an element creates more dependencies for that element.

When you modify any of the shared classes, expected the unexpected results.

Prefer attributes in combination instead.

It makes more sense and easier reading to differentiate things based on their individual attributes, especially in combinations.

Like classes, attributes are not “inherited” from ancestor elements, so they are always specific enough.

/* defines a general/default/unqualified/no-attribute behavior */
[base] {color: orange;}

/* defines a one-off behavior */
[base][modified] {color: aqua;}

/* because attributes are in sets, they can be written in any order */
[active][stop][light] {}

/* which is the same as */
[light][stop][active] {}

Think of attributes as scopes, their values as state-based specifiers.

An attribute selector has the same specificity as a class.

/* Find any element with the href attribute. */
[href] {}

An attribute selector with a value increases the specificity.

/* Find any element with an empty href attribute. */
[href=""] {}

You can increase the specificity and/or fine-tune selection by states using any of the partial combinators, matching by:

  • presence: [attr]
  • exact value: [attr="yes"]
  • value starting with: [attr^="y"]
  • value ending with: [attr$="s"]
  • value containing: [attr*="yes"]
  • value contained in space-separated list: [attr~="yes"]
  • value contained in hyphen-separated list: [attr|="yes"]
  • case-insensitive matching: [attr="Yes" i]

See more at
[attribute] by Sara Cope (2011) and
Splicing HTML’s DNA With CSS Attribute Selectors by John Rhea (2018).

Short example

(Note, 11 May 2020: The following example really works – custom attributes in HTML are supported in all browsers, and will be located by the corresponding CSS attribute selectors. The point is to focus on the attribute as a state indicator. A “data-attribute” refactoring appears at the end of this post.)

A signal element can be styled with common base behavior:

[signal] { layout ruleset }

The same element with a state needs an attribute value, rather than a new class:

[signal=stop] ... { color: red }

So, start with a minimal HTML:

<ul>
<li></li>
<li></li>
<li></li>
</ul>

Next

We can style it with this base CSS:

[signal] {
  background: grey;
  display: inline-block;
  margin: 0;
  outline: 1px solid; 
  padding: 0;
}

[lamp] {
  background: #adadad;
  border-radius: 2em 2em; /* make circular :) */
  border: 1px solid;
  height: 3em;
  list-style-type: none;
  margin: 1em;
  padding: 0;
  width: 3em;  
}

Then add state-specific styles with attribute combinations:

[signal="stop"]  [stop] { background: red; }
[signal="slow"]  [slow] { background: yellow; }
[signal="go"]    [go]   { background: green; }

In JavaScript, add/set the attribute for ‘signal’ based on current vs. next state:

!(function() {
  var signal = document.querySelector('[signal]')
  var btn = document.querySelector('[next]')
  var lamp; // store state here, no need to call signal.getAttribute

  function next() {
    lamp = (lamp == 'stop' ? 'go' : (lamp == 'go' ? 'slow' : 'stop'))
    signal.setAttribute('signal', lamp)      
  }

  next() // initialize...

  btn.addEventListener('click', next)
}());

View the source for this example at https://gist.github.com/dfkaye/612d34a66b9bb74efc29.

View the codepen demo.

Use them to replace BEM

To deal with “class-itis”, BEM (or “block, element, modifier”) architecture was developed in the early 2010’s (following SMACSS and OOCSS).

It is not a bad approach, but BEM is a symptom, not a solution.

Here’s an element from a social networking site, that specifies the class attribute using the BEM namespace strategy:

<span class="css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0">class="presence-indicator presence-entity__indicator presence-entity__indicator--size-8 presence-indicator--is-reachable presence-indicator--size-8 ember-view"</span>

I completely understand and sympathize, but at the end of the day, you’re all fired. I don’t want to maintain that kind of repetition, nor do I want to promote sending all that bloat over the wire repeatedly.

My tweet on this: https://twitter.com/dfkaye/status/957373096104665088

BEM is still trying to handle both categorical styling as well as stateful styling in a single selector, by solely relying on .className selectors.

Fixing that example a bit

Let’s break some conventions to get our heads around this, because this is fundamental. We can fix all this later with something more semantic or conventional. The thing to keep in mind: class-categorical styling and stateful styling are two different things.

First, let’s cut down the repetition between presence-indicator and presence-entity__indicator. These both seem to mean that another user is online, so let’s call it online.

Second, there are two other modifiers, size-8 and is-reachable. Let’s use reachable for the second one (its presence means true, its absence means false).

The size-8 modifier, on the other hand, hints that a utility rule for sizing is needed. We’ll defer that decision for later in this post.

Here’s the revised HTML:

<span class="css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0">class="online size-8 reachable"</span>

Add CSS for that with partial-match-attribute selectors:

[online] { outline: 2px aqua }
[size-8] { padding: 8px }
[reachable] { background: green }

Yes, this actually works in browsers later than IE6. True, the markup isn’t “valid HTML”. The point is that we can use any attribute other than class to control the style, and we use that attribute’s presence and/or content to control styling by element state.

Using only attributes for styles (a digression)

This year we created a third-party component using [company-component] attribute selectors to restrict our styles to our container and prevent them affecting the host page.

(Note: We used JavaScript to walk the elements in the component and add company-component attributes to all of them.)

An element with special meaning, like “start” or “date-picker” or “progress”, would be assigned that value to the attribute.

All other elements received their tagNames in lowercase as the attribute value. For example an h2 element would be created as


, and the CSS selector ruleset for it would be [company-component=h2] { /* h2 rules here */ }.

By using attribute selectors entirely, we controlled all styles on the container, the form elements, the summary, any lists, etc., without affecting styles in the host page.

Conclusion

Custom attributes are fast, good, and cheap.

They are supported in all browsers after IE6. They are easy to add/modify/remove.

Voilá.

Wait a minute, Wait a minute…!! Shouldn’t we use data-*attr* for custom attributes?

OK, OK, yes. For “valid” HTML, custom attributes should be prefixed with data-. When you use the data- prefix style, you automatically get the element.dataset API in JavaScript (in 2015’s modern browsers).

And back in our example refactoring, since we’re dealing with an “entity”, we can add a data-entity attribute and incorporate the previous attributes (online, size-8, reachable) as attribute values instead:

...

And the CSS for that with partial-match-attribute selectors (in this case, matching a value contained in space-separated list):

[data-entity~="online"] { outline: 2px aqua }
[data-entity~="size-8"] { padding: 8px }
[data-entity~="reachable"] { background: green }

Now we can see that the size-8 rule isn’t “state” specific, and can be pulled into the default ruleset for data-entity, leaving us with

[data-entity] { padding: 8px }
[data-entity~="online"] { outline: 2px aqua }
[data-entity~="reachable"] { background: green }

Voilá.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s