CSS
    selectorsfrontend

    How to use the advanced CSS attr() function for types and units

    attr(data-color color, #333) feeding straight into background-color sounds like it already ships, but as of early 2026 it's spec-only. Here's what attr() actually does today, and what's coming.

    Editor: Paul RadfordJul 26, 20267 min read

    Paul RadfordFull-Stack Developer & Editor. Paul is a full-stack developer and editor of template.tips. Articles here are AI-drafted and reviewed by Paul, who knows the code well enough to catch what's wrong and cut the hype.

    How to use the advanced CSS attr() function for types and units

    How to use the advanced CSS attr() function for types and units

    The CSS attr() function has always been able to read an HTML attribute and drop it into the content property as a string. The CSS Values and Units Module Level 4 spec extends that with a type-or-unit parameter, so attr(data-width px, 100px) can cast an attribute directly into a length, and attr(data-color color, #333) can cast one into a color, usable on properties like width or background-color. That said, as of early 2026 no shipping browser engine implements the extended form outside of properties like content. This is a spec you should design for and test against, not one you can ship on today.

    What attr() does today versus what Level 4 adds

    The version of attr() that actually works everywhere returns a raw string and is only reliably usable on the content property, most often in a pseudo-element:

    css
    1/* Works in every current browser: string type, content property only */
    2a[href]: :after {
    3 content: "; (" attr(href) ")";
    4}

    That's genuinely useful for tooltips, print stylesheets, and debugging overlays, but it's limited. You can't take attr(href) and feed it into background-image: url(...), and you can't take a numeric attribute and use it as a width. The Values and Units Level 4 draft fixes exactly that gap by adding a second, optional parameter that tells the parser what type to expect and what to fall back to if parsing fails:

    text
    1attr(attribute-name type-or-unit, fallback-value)

    The type-or-unit token can be a raw category like number, integer, color, or url, or a concrete unit like px, deg, %, s, or Hz. That distinction matters: attr(data-width px, 100px) doesn't just say "treat this as a length," it says "parse this as a bare number and append px," which is different from attr(data-width length, 100px), where the attribute value would need to already contain a unit.

    Setting layout dimensions from data attributes

    Here's the pattern the spec is aiming for, written the way you'd actually use it once support lands:

    html
    1<!-- Each card carries its own width as plain markup data -->
    2<div class="card" data-width="320">Card A</div>
    3<div class="card" data-width="not-a-number">Card B</div>
    4<div class="card">Card C (no attribute at all)</div>
    css
    1.card {
    2 /* Parses data-width as a bare number and appends "px".
    3 Falls back to 240px if the attribute is missing or unparsable. */
    4 width: attr(data-width px, 240px);
    5 display: inline-block;
    6 padding: 1rem;
    7 border: 1px solid #ccc;
    8}

    Under the spec's rules, Card A renders at 320px, and both Card B and Card C fall back to 240px, silently, with no console warning. That silence is worth remembering: it means a typo in your data attribute doesn't break the page, but it also doesn't tell you it happened.

    Pulling colors straight out of markup

    The color type is arguably the more compelling use case, because it lets a CMS, a design system, or server-rendered markup drive theming without inline style attributes or a build step:

    html
    1<button class="tag" data-accent="#2563eb">Design</button>
    2<button class="tag" data-accent="rebeccapurple">Engineering</button>
    3<button class="tag" data-accent="lol">Marketing</button>
    css
    1.tag {
    2 /* "color" here is the type-or-unit keyword, not a CSS color value */
    3 background-color: attr(data-accent color, #6b7280);
    4 color: white;
    5 padding: 0.35rem 0.75rem;
    6 border: none;
    7 border-radius: 999px;
    8}

    The first two buttons get their exact accent color. The third, with data-accent="lol", isn't a valid color token, so it falls back to the neutral gray. This is the whole appeal of typed attr(): no JavaScript, no inline styles, no CSS custom property wiring, just markup that carries its own presentation data and CSS that knows how to type-check it.

    How does the browser handle an invalid attribute value?

    This is the part worth understanding at a spec level, because it explains both the safety and the debugging cost of the feature. When a browser evaluates a typed attr(), it tries to parse the attribute string as the declared type. If that parse fails, the whole declaration becomes what the spec calls invalid at computed-value time. If a fallback was supplied, the fallback wins. If no fallback was supplied, the property behaves as though the declaration were never written at all: it falls through to the next matching rule, the inherited value, or the property's initial value.

    That's a meaningfully different failure mode from a plain syntax error in your CSS, which the browser catches at parse time and simply ignores. Invalid-at-computed-value-time failures happen per element, at render time, based on data the browser doesn't control. A shared stylesheet can look completely correct and still produce inconsistent results across a page, depending entirely on what content authors, CMS fields, or user input put into each attribute. Always supply a fallback for anything layout-critical. An unset width collapsing to auto is a very different bug than an unset width staying at a sane default.

    Where does this actually work right now?

    Nowhere in production, and that's not a knock on any one vendor. The extended, typed form of attr() was proposed in early drafts of the Values and Units spec more than ten years ago and has stalled repeatedly over implementation complexity and disagreement about how aggressively browsers should validate arbitrary attribute strings as CSS values. The MDN reference page for attr() documents the extended syntax and its compatibility table shows it unimplemented in Chrome, Firefox, and Safari as of this writing. Don't trust a blog post (including this one) over that table; check it directly before you plan a feature around this.

    You can feature-detect it cleanly with CSS.supports(), which is the right guard to put in front of any progressive enhancement:

    js
    1// Detect support for typed attr() before relying on it in a stylesheet
    2const supportsTypedAttr = CSS.supports('width', 'attr(data-width px, 100px)');
    3
    4if (!supportsTypedAttr) {
    5 document.documentElement.classList.add('no-typed-attr');
    6}

    Pair that with a CSS fallback path (custom properties, described below) gated behind the .no-typed-attr class, and you get a codebase that will pick up native support automatically the day a browser ships it, without a rewrite. The MDN documentation for CSS.supports() covers the exact syntax-checking semantics if you want to confirm how it evaluates unsupported functions.

    Security and validation trade-offs

    Typed attr() reads a raw attribute string and hands it to the CSS parser, not to a script engine, so there's no code-execution vector comparable to unsanitized innerHTML. A malicious data-width value can't run JavaScript through this mechanism. But two real risks are worth planning around before you lean on it:

    • Layout abuse from untrusted markup. If data-width is populated from user-generated content (a comment field, a third-party widget, a CMS block someone doesn't fully control), a value like data-width="999999" can blow out a layout even though it's technically valid. Typed attr() on its own doesn't clamp anything; you still want width: min(attr(data-width px, 240px), 100%) or a similar guard once min()/max() composition with attr() is supported.
    • Silent fallback hides bugs. Because an invalid attribute value fails quietly and falls back with no console error, a trailing semicolon in data-color="#2563eb;" or a stray space will make an element render with the default color and give you nothing to grep for in devtools. Treat typed attr() values the same way you'd treat any external input: validate them server-side or at the point of template rendering, and don't rely on the browser's silent fallback as your only safety net.

    What to use instead until browsers catch up

    The practical, production-ready equivalent today is CSS custom properties, either inline or set via JavaScript, combined with var()'s own fallback argument:

    html
    1<div class="card" style="--card-width: 320px;">Card A</div>
    2<div class="card">Card B (no inline property)</div>
    css
    1.card {
    2 width: var(--card-width, 240px);
    3}

    This works in every browser that supports custom properties, which is effectively all of them since 2017, and if you want real type enforcement rather than just a string substitution, register the property with @property and a syntax descriptor of <length>. That gives you the same invalid-at-computed-value-time fallback behavior the Level 4 spec promises for attr(), available now, with full browser support.

    Structure your markup with that migration in mind: keep the semantic data in data-* attributes for accessibility and tooling, mirror the values into inline custom properties for styling, and swap the custom-property layer out for native typed attr() calls once CSS.supports() tells you it's safe. That's a rewrite of a few selectors, not an architecture change, and it means you get the ergonomic win the spec is chasing without betting a shipping feature on a browser capability that, more than a decade after it was first proposed, still hasn't landed anywhere.

    Related Articles