Designing a Reusable Component Library on Radix UI
Radix UI's pitch — unstyled, accessible component primitives — is honest about what it is and isn't. It solves the genuinely hard part (focus management, keyboard nav, ARIA wiring for a Dialog or a Combobox) and leaves the rest to you. Here's what "the rest" actually involved building a small internal component library on top of it.
What Radix earns you for free
Correct focus trapping in modals, correct aria-* attributes on menus and comboboxes, escape-key and outside-click handling that actually accounts for edge cases (nested popovers, portal rendering) most teams get subtly wrong when building these from scratch. That's not a small thing — accessibility bugs in custom-built dropdowns and dialogs are extremely common, and Radix's primitives having already solved them was the actual reason to adopt it, more than styling convenience.
What you still have to build
A styling layer. Radix ships zero CSS. Every primitive needs your design tokens applied — consistent spacing, color, radius, motion — which means every component gets a thin wrapper that owns styling while passing Radix's behavior through untouched.
Composition conventions. Radix's compound-component pattern (Dialog.Root, Dialog.Trigger, Dialog.Content) is powerful but verbose at every call site. Wrapping the common cases (a standard confirm dialog, a standard dropdown menu) in a simpler API kept most usage sites from needing to know Radix's internals at all.
Variants. Radix doesn't have an opinion on "primary vs. secondary button" or "small vs. large input." That's product design, and it's the layer that actually makes a component library feel coherent rather than like a pile of individually-styled primitives.
The decision that paid off most
Keeping the Radix primitive fully passed-through as an escape hatch on every wrapped component (accepting and forwarding arbitrary Radix props), instead of hiding it behind a narrower custom API. Every time a one-off page needed slightly different behavior than the "standard" wrapped version supported, that escape hatch meant reaching for a prop instead of forking the component.
Takeaway
Radix UI isn't a component library — it's the hardest 20% of building one, done correctly, handed to you. The remaining 80% (styling, composition ergonomics, variants) is real, ongoing design system work, and treating Radix as "done, just add CSS" undersells how much of that work is still yours.