§Attribute-Derived Styling
Published on
In a component, state is often expressed twice: once for behavior, and again for presentation.
To reduce the chance of both falling out of sync, we can reflect state directly in the DOM using aria-* and data-* attributes, then derive styles from them.
§Making Use of Existing State
ARIA attributes expose state to assistive technologies. Common examples include aria-selected, aria-expanded, and aria-disabled.
For concepts ARIA doesn’t cover, data-* attributes can express state explicitly, like data-open, data-closed, or data-highlighted.
When styles derive from these attributes, the DOM becomes the single source of truth.
§Prop-Derived vs Attribute-Derived
Consider a component where animations change based on its position and whether it’s open or closed.
A common approach is to derive styles directly from props:
const Component = ({
open,
position,
...props
}: ComponentProps) => {
return (
<div
className={cn(
'rounded-md bg-white p-2 shadow-sm',
open ? 'animate-in fade-in-0' : 'animate-out fade-out-0',
position === 'top' && 'slide-in-from-top-2',
position === 'bottom' && 'slide-in-from-bottom-2',
position === 'left' && 'slide-in-from-left-2',
position === 'right' && 'slide-in-from-right-2'
)}
{...props}
/>
);
};Alternatively, state can live in the DOM and styles derive from it:
const Component = ({
open,
position,
...props
}: ComponentProps) => {
return (
<div
data-open={open || undefined}
data-closed={!open || undefined}
data-position={position}
className='
rounded-md bg-white p-2 shadow-sm
data-[open]:animate-in
data-[open]:fade-in-0
data-[closed]:animate-out
data-[closed]:fade-out-0
data-[position=top]:slide-in-from-top-2
data-[position=bottom]:slide-in-from-bottom-2
data-[position=left]:slide-in-from-left-2
data-[position=right]:slide-in-from-right-2
'
{...props}
/>
);
};State expressed once, styles derived from it. Nothing to keep in sync.