The CSS attr() function got nothin’ on custom properties

Avatar of Chris Coyier
Chris Coyier on (Updated on )

Normally, the connection between CSS and HTML is that CSS selectors match HTML elements, and the CSS styles them. CSS doesn’t know about the actual content in the HTML. But there is a way CSS can get its hands on data in HTML, so long as that data is within an attribute on that HTML element.

It’s like this:

div::after {
  content: attr(data-whatever);
}

That’s certainly interesting. You could use it for (rather inaccessible) tooltips, for example:

<button data-tooltip="Information only mouse-having sighted people will see.">
  Button
</button>
button:hover::after {
  content: attr(data-tooltip);
  /* positioned and styled and whatnot */
  /* ya, a :focus style would buy you a tad more a11y */
}

But you can’t put HTML in the attribute value, so those tooltips are limited to a string value, and couldn’t have a title, link, or anything like that inside them.

Here’s a better use case. There is an old print stylesheet chestnut where you use attr() to add the URL’s to links, so you can actually see what a link is linking to:

@media (print) {
  a[href]::after {
    content: " (" attr(href) " )";
  }
}

That’s clever. But what else? Could you pass a color down?

<h2 data-color="#f06d06">
  Custom Colored Header
</h2>

That’s not invalid, but it isn’t useful.

h2 {
  /* Not gonna work */
  color: attr(data-color);
}

The value from attr() is a string. Even though that string is in the same format as a hex code, it won’t be used as a hex code.

Nor can you pass a URL that can actually be used in something like background-image(). Nor you can pass a unit like 3, 20px or 4rem or 0.8vw.

CSS’s attr() function is only strings, and strings are only really useful as content, and content (being unselectable and somewhat inaccessible) isn’t particularly useful anyway. You can’t select the text of psuedo content, for example, nor search for it, making it rather inacessible.

You know what can pass any sort of value and is equally easy to implement as attributes?

CSS custom properties!

You can pop them right into the style attribute of any element. Now those values are available to that element:

<button 
  style="
    --tooltip-string: 'Ug. Tooltips.';
    --tooltip-color: #f06d06;
    --tooltip-font-size: 11px;
    --tooltip-top: -10px
  "
>
  Button
</button>

We’re passing a string to CSS above, but also a color and length values. Those values are immediately usable as-is:

button::after {
  content: var(--tooltip-string);
  color: var(--tooltip-color);
  font-size: var(--tooltip-font-size);
}

Here’s that demo with some fiddly “logic” (would need to be improved a lot to be actually useful) to allow variations:

See the Pen CSS Custom Properies Mo’ Betta’ than attr() by Chris Coyier (@chriscoyier) on CodePen.

This really isn’t any more accessible, for the record. If I were implementing tooltips for real, I’d probably read the heck out of this.

What about some other “good” use cases for attr()?

One that comes up a lot is responsive data tables. Imagine a table with headers along a top row and rows of data below:

<table>
  <thead>
  <tr>
    <th>First Name</th>
    <th>Last Name</th>
    ....
  </tr>
  </thead>
  <tbody>
  <tr>
    <td>Chris</td>
    <td>Coyier</td>
    ...
  </tr>
  ...
  </tbody>
</table>

Rows of data like that might become problematic on small screens (too wide). So in a reponsive data table, we might hide that top row, and show labels on a per-cell basis instead.

@media (max-width: 500px) {
  thead {
    display: none;
  }
  /* Need to reveal another label now that we've hidden the normal labels */
}

Where does that label come from? We could do…

 . ...
  <tr>
    <td data-label="First Name">Chris</td>
    <td data-label="Last Name">Coyier</td>
    ...
  </tr>

Then:

td::before { 
  content: attr(data-label);
  /* Also display: block things and such */ 
}

That’s a pretty good use case. If we use some kinda of accessible hiding method for that <thead>, it might even pass a11y muster.

But this same exact thing is doable with CSS custom properties…

 . ...
  <tr>
    <td style="--label: 'First Name';">Chris</td>
    <td style="--label: 'Last Name';">Chris</td>
    ...
  </tr>
td::before { 
  content: var(--label);
  ...
}

Eric Bidelman pointed me to a method of using psueudo content to show an input’s value.

<style>
  input {
    vertical-align: middle;
    margin: 2em;
    font-size: 14px;
    height: 20px;
  }
  input::after {
    content: attr(data-value) '/' attr(max);
    position: relative;
    left: 135px;
    top: -20px;
  }
</style>

<input type="range" min="0" max="100" value="25">

<script>
  var input = document.querySelector('input');

  input.dataset.value = input.value; // Set an initial value.

  input.addEventListener('change', function(e) {
    this.dataset.value = this.value;
  });
</script>

That feels a smidge dangerous to me since I didn’t think pseudo content was supposed to work on replaced elements like an <input>. It’s probably a job for output, and the JavaScript would be essentially the same. You could use pseudo content with the additional element, but there’s really no need for that.


Exploiting the fact that psuedo content can’t be copied is also clever. For example, GitHub does code block line numbering with data-line-number="" and ::before { content: attr(data-line-number); }.

Nobody likes selecting line numbers when they are trying to copy code! Good use here (probably even more flexible than CSS counters), but again, something that CSS custom properties could handle as well.

<td style="--line-num: 5"> ... </td>

You could argue this is better because if you did want to use CSS counters, you could use that first value to kick things off and not need it on every line.

See the Pen Line Numbering by Chris Coyier (@chriscoyier) on CodePen.

Same deal with typographic trickery involving duplicating text in CSS for stylistic reasons. Check out this cool demo by Mandy Michael using attr(). I’m sure you can imagine how --heading: "Fracture"; could do the trick there.

The CSS3 Values spec (in Candidate Recommendation) has a way to make attr() useful

I’m not sure it matters much, as I’d argue CSS custom properties are a near total replacement for attr(), but the spec does specifically cover this, presumably as an attempt to make it more useful.

The idea is to set the type of value as you grab it in CSS.

<div data-color="red">Some Words</div>
div {
  color: attr(data-color color);
}

Or…

<span data-size="50">span</span>
span {
  font-size: attr(data-size px);
}

But as far as I can tell, no browser supports this.