I write bluntly, I value your time, it's not to everyone's taste. Less waffle more value. Let's go.
Pick a popular component library, find the Button component. You'll find semantic HTML with ARIA attributes (structure), JavaScript handling focus and keyboard events (interaction), and then the skin — color values, spacing, font size, border radius, hover transitions, and variants for good measure (xl, lg, md, primary, secondary etc).
That's one thing doing three jobs (structure, interactivity, skin/aesthetic).
Structure — the semantic HTML and accessibility contract. What element is it? What ARIA role does it have? What does it declare to assistive technology?
Interaction — the behavioural logic. Focus management, disclosure state, keyboard navigation, scroll locking (i.e modal open).
Aesthetics — the visual treatment. Colors, spacing, typography, border radius, elevation. Everything that defines the look of a digital interface....the brand.
These "3 pillars" are subject to constant change in any product...no exclusions. We need to anticipate these changes/fluctuations. Examples: The color palette is updated (aesthetics). New form fields are required which rightly demands accessibility & inclusion (structural). Requirement for a keyboard shortcut is introduced (interaction).
Binding Structure, Interaction, Aesthetics is a breeding ground for terrible UI/UX. Think buttons...we have icons, pagination, forms, linking to 3rd party sites...all buttons, all entangled. Whats the ARIA, whats the color, what's the icon size. Entangled...And that's buttons, which are an intrinsic part of forms. So now we're dealing with inputs and checkboxes and textareas and the list goes forever on.
Let's step away from a coupled system where changing a color means touching every component that references it. It seems like an obvious solution to me. Or just please, no more theme providers that wrap your entire app in a JavaScript object of visual decisions/demands. Looking at you MUI & Chakra UI (& Flutter - somewhat excused).
Hello tokens
Tokens, named variables that store visual decisions. A color, a spacing value, a border radius. Instead of hardcoding #2563eb in a component, you reference --color-primary. Instead of 16px, you reference --space-md. The value lives in one place, the token. Everything else points to it.
My argument
Decouple. That's it.
Components own structure and interaction. Tokens own aesthetics.
Concretely:
- A
<Button>defines its<button>element,role,aria-pressed, keyboard handler. That's it. Nocolor, nopadding, noborder-radius. None. - A token system defines every visual value —
--color-primary: #2563eb,--space-md: 1rem,--radius-sm: 4px— as CSS custom properties. Primitives feed semantic names (--color-action-primary), semantic names feed component bindings (--button-bg). - One CSS file maps tokens to component hooks:
.button { background: var(--button-bg); padding: var(--button-padding); border-radius: var(--button-radius); }.
Just look. If you have buttons that's it. It's dead simple. Let's remove complications.
.button {
background: var(--button-bg);
padding: var(--button-padding);
border-radius: var(--button-radius);
}
That's the entire wiring. Three layers (raw HTML, :root vars, CSS mapping), zero entanglement.
What this fixes
Theme change? Edit one token. --color-primary: #2563eb becomes --color-primary: #dc2626. Every component using --color-action-primary updates. No component files touched. No JS redeployed. One CSS variable, done. Oh...your buttons all use the primary color...update one token, done. Oh...you have a backend system that requires a tweak of the same brand, sure, same components, same patterns, different tokens, new look, done. All by changing 1 key/value pair.
So, why not CSS-in-JS?
CSS-in-JS solved real problems — specificity wars, naming collisions, dead code. But it welded visual treatment to the component tree. styled(Button) isn't styling a button, it's creating a new component. That's coupling.
We traded global CSS problems for component coupling. Same mess, different file.
Uhm, Tailwind?
Tailwind removed JavaScript coupling. Real progress. But bg-blue-500 hover:bg-blue-600 dark:bg-blue-400 px-4 py-2 rounded-lg is still aesthetics tangled with structure. Theming means find-and-replace across every component. Dark mode means dark: on every class. Brand refresh means touching every file.
Tailwind is utility-first. What I'm describing is token-first. The visual decisions don't live in markup at all. They live in one token layer that feeds CSS that feeds components. The component never knows what color it is.
The separation
| Concern | Owns | Doesn't touch |
|---|---|---|
| Structure | HTML elements, ARIA roles, semantic contracts | Colors, spacing, JS behaviour |
| Interaction | Focus, keyboard nav, disclosure, scroll lock | Markup choice, visual treatment |
| Aesthetics | Tokens → CSS custom properties → component hooks | DOM structure, event handling |
Three columns. No overlap. Change one, the others don't know and don't care.
This isn't theoretical. This is the way we should be building - stop tying things together that share different concerns. It's a gap that needs addressing. As such - I built this. And if individual tokens are too much (I agree) here's a helper.
Top comments (0)