Skip to main content

data · aria

Attribute variants for headless UI libraries like Radix, Ark and React Aria — data-[state=open]: and aria-expanded: prefixes, built safely.

What they build

Both helpers put an attribute variant in front of a set of classes, so a component can style itself from the state its headless library — Radix, Ark, React Aria — already writes to the DOM.

Attribute variants
data("state", "open", "opacity-100");           // → "data-[state=open]:opacity-100"data("disabled", null, "pointer-events-none");  // → "data-[disabled]:pointer-events-none"aria("expanded", "rotate-180");                 // → "aria-expanded:rotate-180"

data takes the attribute, a value and the classes — pass nullas the value to match the attribute's presence rather than a particular value. aria takes the attribute and the classes.

Why these are not keys

ss accepts base plus Tailwind's own keys — 149 in total, and every one of them is a fixed name. That closed set is what makes autocomplete exhaustive and a typo a compile error.

Anything carrying a value of its own — data-*, aria-*, supports-*, has-*, not-*, an arbitrary min-[…] — is deliberately absent from that list, because the value cannot be enumerated ahead of time. These two helpers cover the first two; everything else goes through withPrefix.

Values with spaces

A value containing a space cannot appear in a class name, so write it Tailwind's way — with _, which Tailwind reads back as a space.

Underscore for a space
data("state", "half_open", "opacity-50");  // matches data-state="half open"

A literal space is caught, not ignored

Passing a literal space warns in development rather than silently producing a class that matches nothing.

The arguments must be literals

Both helpers assemble the variant prefix at runtime, so Tailwind never sees the finished class in your source — they need the build plugin, exactly like ss. One line of setup covers all of them.

The scanner reads the literal strings at the call site and over-approximates on purpose, so a value that flips between two states costs you nothing: both branches are enumerated.

Seen by the scanner
data("state", open ? "open" : "closed", "p-2")   // both values are read

A value held in a variable is not in the source to read, and neither is an interpolated template. Keep the attribute, the value and the classes literal at the call site — a ternary is fine, a variable is not.