Attribute-Derived Styling
Published on
In a component, state is often represented twice: once as component state, and again as styling conditions. One controls behavior. The other controls presentation. Keeping those two aligned is incidental complexity.
An alternative to that is to reflect state directly in the DOM using aria-* and data-* attributes, and derive styles from those attributes. Styles respond to declared state, instead of duplicating it through parallel conditional logic.
Making Use of Existing State
ARIA attributes exist to expose interactive state and conditions to assistive technologies. Widely used attributes arearia-selected, aria-expanded, and aria-disabled.
When styles derive from those same attributes, semantics and presentation observe the same signals.
Where ARIA does not model a concept, data-* attributes can be used to express state explicitly. Common examples include open and closed state, such as data-state=open and data-state=closed, or active and inactive state, such as data-state=active and data-state=inactive.
Attribute-Derived vs Prop-Derived
Consider a popover component, where the content’s visual state depends on whether it is open or closed, and on its position relative to the trigger.
With attribute-derived styling, state is expressed exclusively through DOM attributes, and styles are derived from those attributes.
const PopoverContent = ({
open,
side,
...props
}: PopoverContentProps) => {
return (
<div
data-state={open ? 'open' : 'closed'}
data-side={side}
className='
rounded-md bg-white p-2 shadow-sm
data-[state=open]:animate-in
data-[state=closed]:animate-out
data-[state=open]:fade-in-0
data-[state=closed]:fade-out-0
data-[side=bottom]:slide-in-from-top-2
data-[side=top]:slide-in-from-bottom-2
data-[side=right]:slide-in-from-left-2
data-[side=left]:slide-in-from-right-2
'
{...props}
/>
);
};In contrast, it is common to derive styles directly from component props, while attributes are set independently.
const PopoverContent = ({
open,
side,
...props
}: PopoverContentProps) => {
return (
<div
data-state={open ? 'open' : 'closed'}
data-side={side}
className={cn(
'rounded-md bg-white p-2 shadow-sm',
open ? 'animate-in fade-in-0' : 'animate-out fade-out-0',
side === 'bottom' && 'slide-in-from-top-2',
side === 'top' && 'slide-in-from-bottom-2',
side === 'right' && 'slide-in-from-left-2',
side === 'left' && 'slide-in-from-right-2'
)}
{...props}
/>
);
};This works, but it duplicates representation. State is expressed once for semantics and again for styling. The two must be kept aligned.
One Source of Truth
Attribute-derived styling avoids restating state for presentation.
State is already exposed in the DOM to support behavior and accessibility. When styles derive from those same attributes, state is represented once and consumed consistently.
Fewer representations of the same state mean fewer opportunities for semantics and presentation to diverge.