Chapter 1
The architecture before the code
This chapter covers the decisions you need to make before you write a single line of code.
Don’t open your terminal yet. The most expensive mistakes in a component library are architectural, and they compound. A bad token structure means renaming hundreds of references later. A wrong CSS strategy means rewriting every component’s styles. A missing accessibility layer means adding it retroactively, which always costs more than building it in.
Choosing your primitives layer
The accessibility primitives layer handles keyboard navigation, focus management, ARIA attributes, and screen reader behavior for interactive components like buttons, modals, selects, and tabs.
You have three options:
Build it yourself. This means implementing WAI-ARIA patterns from scratch for every interactive component. A correct modal implementation alone requires focus trapping, scroll locking, Escape key handling, focus restoration on close, and aria-modal management. Multiply that by every interactive component in your library. Unless accessibility engineering is your core skill, this path produces components that work for mouse users and break for everyone else.
Use Radix primitives. Radix provides unstyled, accessible components that you style yourself. It’s the layer underneath shadcn/ui, and it has the largest mindshare in the React ecosystem. Radix was originally built by Modulz and is now maintained by WorkOS. It’s a mature, production-proven library. This guide uses React Aria instead of Radix, not because Radix is a bad choice, but because React Aria offers deeper accessibility coverage out of the box, particularly for mobile screen readers and internationalization. That translates directly to less custom work per component.
Use React Aria. React Aria is maintained by Adobe’s design system team, the same team behind the Spectrum design system. HeroUI (formerly NextUI) has built on React Aria since v2. Untitled UI React, launched in 2025, also chose React Aria. React Aria handles mobile screen reader interactions, press event normalization across pointer types, and internationalization of ARIA labels.
This guide uses React Aria because it gives you the most accessibility coverage with the least custom work, backed by a maintenance commitment tied to Adobe’s own production needs.
Choosing your styling approach
The styling layer determines how your token values reach the rendered components and how adopters customize the visual output.
This guide styles components with plain CSS class names reading CSS custom properties. Each component owns a stylesheet next to it, and every value in that stylesheet is a var() pointing at a token. There is no CSS-in-JS runtime, no utility class generation, and no build step between the token and the rule.
The decision that matters here is a negative one: the published package does not ship Tailwind. That deserves an explanation, because Tailwind is the obvious choice and this guide still installs it.
Tailwind CSS 4’s @theme directive is good. It maps custom properties to utility classes with no JavaScript config, and for an application it is hard to beat. A component library is not an application. Importing Tailwind ships its preflight, a global reset that restyles the consumer’s headings, lists, tables, and form controls the moment they import your CSS. Consumers already using Tailwind have their own preflight and now have two. Consumers who are not never asked for one. Either way the library has reached outside its own components and changed the page.
So Tailwind stays, scoped to development. Storybook loads it, the documentation pages and example compositions use it, and none of its CSS reaches the package. (The cn() helper from chapter 2 does ship tailwind-merge, a small runtime utility with no styles of its own.) Chapter 3 sets up the two CSS entry points that enforce the split.
The cost is real: you write stylesheets instead of utility strings, and that is slower. What you get is a package that cannot restyle the application that installs it.
CSS Modules, Sass, and styled-components all work too. Sass and CSS Modules add a compile step between the token and the rule for little benefit once tokens are custom properties and class names carry a rudi- prefix. styled-components adds a runtime and a peer dependency that consumers inherit.
The three-tier token architecture
If you’ve worked with design tokens before, you’ve probably seen systems that define a color palette and reference those colors directly in components. That works until you need a dark theme, a rebrand, or a second product that shares components but looks different.
A three-tier architecture avoids this by separating what values exist from what values mean from what values a specific component uses.
Tier 1 (global tokens) defines the raw palette. color.blue.500 is #3B82F6. It has no opinion about what that color is for. It just exists as a named value in the system.
Tier 2 (semantic tokens) assigns meaning. color.brand.primary references color.blue.500. Now color.blue.500 is aliased as the primary brand color. When the brand changes to teal, you update one alias. Every component that uses color.brand.primary changes with it.
Tier 3 (component tokens) scopes values to individual components. component.button.primary-bg references color.brand.primary. A button needs padding at three sizes, a font size at three sizes, a radius, an icon gap, a focus outline color, and background, text, hover, and active colors for each of four variants. None of those are statements about the system; they are statements about a button. Left in tier 2 they crowd the semantic namespace and make every component’s values reachable by every other component.
Plenty of guides call this tier optional. It is optional right up until you have more than a handful of components, at which point retrofitting it means touching every stylesheet you have written. This guide builds it from the start.
The token schema uses the Design Tokens Community Group (DTCG) format, the leading interchange format for design tokens. The DTCG specification reached its first stable release (2025.10) in October 2025, with support from more than 10 tools including Figma, Style Dictionary, Tokens Studio, and Penpot. It is a W3C Community Group specification, not a W3C Recommendation, but it is production-ready and widely adopted. Style Dictionary v4 ships with first-class DTCG support.
Layout as a first-class concern
Most component libraries stop at UI components: buttons, inputs, modals, cards. They leave layout to the consumer, who either writes custom CSS for every page or copies page templates and strips out what they don’t need.
This guide treats layout as part of the component library. You’ll build eight layout primitives (Stack, Box, Center, Cluster, Sidebar, Switcher, Grid, Cover) that follow the intrinsic design principles described by Heydon Pickering and Andy Bell in Every Layout. These primitives respond to their container’s available space rather than the viewport width, which means they work in any context without media query breakpoints.
The layout primitives are driven by the same token system as the UI components. Changing layout.sidebar.width from 20rem to 24rem propagates to every Sidebar instance in the application. Changing layout.stack.space.default from 1.5rem to 2rem adjusts vertical rhythm globally.
Decisions summary
Before you proceed, here’s what you’re committing to:
| Decision | Choice | Why |
|---|---|---|
| Framework | React 19 + TypeScript | Largest ecosystem; TypeScript makes component APIs self-documenting |
| Accessibility primitives | React Aria | Adobe-maintained; deepest a11y coverage; used by HeroUI and Untitled UI React |
| Component styling | Plain CSS classes + custom properties | Colocated per-component stylesheets reading var(--rudi-*); no runtime, no preflight shipped to consumers |
| Docs styling | Tailwind CSS 4 | Storybook and example pages only; never imported by the published package |
| Token format | DTCG JSON + Style Dictionary v4 | Leading interchange format; platform-agnostic source; automated CSS custom property output. Style Dictionary v5 exists with a compatible config format; this guide uses v4 |
| Documentation | Storybook 10 (installed via @latest — pin the version if reproducibility matters) | Industry standard; built-in a11y addon; ESM-only maintenance release that reduced install size by 29% |
| Testing | Vitest + Testing Library | Fast; behavior-focused; accessible query selectors |
| Build tool | Vite | Fast dev server; clean library build output |
| Layout approach | Intrinsic primitives (Every Layout-inspired) | Context-independent; no media queries; token-driven |
If any of these choices don’t fit your situation, substitute freely. The architecture still holds. The specific tools are less important than the separation of concerns: tokens define values, components consume tokens, layout primitives handle spatial arrangement, and tests verify behavior.
This chapter has been corrected since it was first published: see what changed. Found an error? Report it.