Components are contracts
@da_poling| (2y ago)
Most component libraries are not systems. They are collections.
The distinction matters. A collection gives you a button, an input, and a card. Each one is designed in isolation, documented in isolation, and tested in isolation. They look coherent in a gallery. In a real application, where components compose across layers, share state, and respond to layout constraints they were not designed for, the seams show quickly.
The deeper problem is semantic. A component library typically operates at two levels, visual and structural, and conflates them. A Button is simultaneously a styling decision and a semantic role. When you reach for it, you make both choices at once, usually without realizing it. The result is that the highest layer of your application, where product decisions live, is entangled with the lowest layer, where CSS lives. Changing one risks the other. Understanding one requires knowing the other.
This is why frontend systems are hard to reason about at scale. The semantic layer, the layer that says "this is a primary action" or "this is a destructive operation" or "this is a navigation affordance," is almost never modeled explicitly. It lives in the heads of the people who built the system, expressed indirectly through prop names, variant strings, and documentation that describes appearance rather than intent. A new engineer joins the codebase and has to reconstruct that model from the bottom up, reading CSS to understand product logic.
Layering holds as a concept and is expensive in practice. The boundary between atoms, base components, composites, and application patterns is intellectually clean until you have to enforce it on real components under time pressure. Bad calls at the primitive level propagate further than expected. The style system pays for itself in consistency until the first escape hatch, and then it becomes a test of organizational discipline. The workbench, a tool for mounting components, flipping variants, and catching edge cases before they ship, is the highest-leverage investment in a component system and the first thing that gets deprioritized when props drift and nobody updates the docs.
The real finding was about the gap between intent and implementation.
The experiment
To test whether the argument held in practice, I ran a small experiment. I took a standard Radix UI setup with Tailwind and built the same dashboard feature twice: once against Radix primitives directly, and once against a minimal semantic component library I built on top of them. The semantic library had no exposed styling props. Every component expressed role, not appearance.
The whole thing was run in LLM-assisted environments (the same kind of workflow you would use to ship UI with a model in the loop). I repeated the same prompt and setup multiple times so I could compare outputs run to run, not treat a single generation as the whole story.
The results were not subtle.
What the LLM did with Radix
Given a prompt to build a dashboard action panel, the LLM produced valid, error-free JSX immediately. It also made a dozen silent design decisions, reaching for whatever variant pattern appeared most in its training data, not whatever was right for the system.
// LLM output against Radix — technically correct, semantically adrift
<Button variant="outline" size="sm" className="text-red-600 border-red-300">
Delete record
</Button>
<Button variant="default" className="bg-blue-600 text-white">
Save changes
</Button>
<Button variant="ghost" size="sm">
Cancel
</Button>There is no design system here. There is just the LLM's best guess at what these things probably look like, based on what it has seen before. The output works. It is also completely disconnected from intent.
What the LLM did with the semantic library
Given the same prompt against the semantic library, the output was different in a specific way: it stopped making decisions it had no basis for making.
// LLM output against semantic library — role expressed, appearance downstream
<Action.Destructive>Delete record</Action.Destructive>
<Action.Primary>Save changes</Action.Primary>
<Action.Cancel>Cancel</Action.Cancel>The LLM did not need to know what destructive looks like. It only needed to know that deleting a record is a destructive action. That knowledge was already in the component contract. The styling is downstream, defined once, and not the LLM's problem.
The token cost of flexibility
The difference in prompt overhead between the two approaches compounds quickly. With Radix, expressing design intent requires spelling it out in context every time: which variant maps to which role, what the color conventions are, when to use outline versus ghost versus default. With a semantic layer, that contract is already encoded. The LLM spends tokens on product logic, not on reconstructing a design system it was never trained on.
Radix vs. Semantic Library
Design system accuracy
Output consistency across generations
Tokens spent on design intent per component
Prompt corrections needed per feature
Based on a experiment building equivalent dashboard features against both libraries using Claude with identical prompts. Numbers are directionally representative, not a controlled study.
Expressing implementation in props
A component that expresses "primary action" through a prop called variant="primary" is not modeling semantics. It is modeling appearance with a semantic-sounding name. The consuming layer still has to know what primary means, what it implies visually, when it is appropriate, and when it is not. That knowledge lives nowhere in the system. It lives in convention, which is just undocumented architecture.
The more honest model separates these concerns entirely. A component should express semantic role independent of how that role is currently styled. Styling is downstream. Role is stable. When you change the visual language of the system, the semantic layer should not have to change with it. When a new engineer builds a feature, they should be able to work at the semantic layer without understanding every implementation detail below it.
That separation is not common. Most systems never get there because the path of least resistance is to keep adding variants until the props list becomes its own undocumented language.
The question of where the semantic layer actually lives in a frontend system, and who is responsible for defining and maintaining it, is one of the more underexplored problems in UI engineering. Tooling does not model it. Frameworks do not enforce it. And the gap between what a designer means when they say "primary action" and what an engineer ships when they add variant="primary" stays invisible until the system is large enough that changing either one becomes dangerous.