Your design system has a coupling problem
Source: Dev.to
Introduction
I write bluntly, I value your time—less waffle, more value.
Pick a popular component library and find the Button component. You’ll see:
- Structure – semantic HTML with ARIA attributes (the accessibility contract).
- Interaction – JavaScript handling focus, keyboard events, disclosure state, scroll locking, etc.
- Aesthetics – color values, spacing, font size, border radius, hover transitions, and variants (xl, lg, md, primary, secondary, …).
One component is doing three jobs.
The Three Pillars
| Pillar | Concern | Example |
|---|---|---|
| Structure | Semantic HTML, ARIA roles, accessibility contract | What element is it? What ARIA role does it have? |
| Interaction | Behavioural logic, focus management, keyboard navigation, scroll locking | Keyboard shortcut, modal open/close |
| Aesthetics | Visual treatment – colors, spacing, typography, border radius, elevation | Brand palette, hover states |
These pillars constantly change in any product. Updating the color palette (aesthetics), adding new form fields (structure), or introducing a keyboard shortcut (interaction) can quickly become a breeding ground for tangled UI/UX.
Tokens
Tokens are named variables that store visual decisions. Instead of hard‑coding #2563eb in a component, you reference --color-primary. Instead of 16px, you reference --space-md. The value lives in one place; everything else points to it.
/* Example token definitions */
:root {
--color-primary: #2563eb;
--space-md: 1rem;
--radius-sm: 4px;
}My Argument
Decouple. That’s it.
Components vs. Tokens
- Components own structure and interaction.
- Tokens own aesthetics.
Concrete example
// Button component (structure & interaction only)
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}/* Token system – aesthetics */
:root {
--color-primary: #2563eb;
--button-bg: var(--color-primary);
--button-padding: var(--space-md);
--button-radius: var(--radius-sm);
}
/* Component hook */
.button {
background: var(--button-bg);
padding: var(--button-padding);
border-radius: var(--button-radius);
}Three layers (raw HTML, :root vars, CSS mapping) → zero entanglement.
What This Fixes
Theme change
Edit a single token:
/* Before */
--color-primary: #2563eb;
/* After */
--color-primary: #dc2626;All components using --color-action-primary update automatically—no component files touched, no JavaScript redeployed.
Brand refresh
Change one key/value pair in the token file, and the entire UI reflects the new brand instantly.
So, Why Not CSS‑in‑JS?
CSS‑in‑JS solved specificity wars, naming collisions, and dead code, but it introduced a new coupling: visual treatment became part of the component tree. Overriding it requires understanding the wrapper; theming requires knowing the theme shape; testing requires mocking runtime values. We traded global CSS problems for component coupling—still a mess, just in a different file.
Tailwind?
Tailwind removed JavaScript coupling, which is progress. However, utility strings like:
<button class="bg-primary text-white py-2 px-4 rounded">
Click me
</button>still entangle aesthetics with structure. Theming means find‑and‑replace across every component; dark mode requires dark: on every class; a brand refresh touches every file.
Tailwind is utility‑first. The approach described here is token‑first: visual decisions live in a separate token layer, not in markup.
The Separation
| Concern | Owns | Doesn’t touch |
|---|---|---|
| Structure | HTML elements, ARIA roles, semantic contracts | Colors, spacing, JS behaviour |
| Interaction | Focus, keyboard navigation, disclosure, scroll lock | Markup choice, visual treatment |
| Aesthetics | Tokens → CSS custom properties → component hooks | DOM structure, event handling |
No overlap—change one column, the others remain unaffected.
Conclusion
This isn’t theoretical; it’s a practical way to build UI systems. Stop tying together concerns that belong in separate layers.