Back to Articles
June 5, 202611 min read

Designing a Component Library in React Native + TypeScript

Design tokens, theming with context, a variants API, and Storybook. How we built a 40-component library that 5 apps share, and the patterns that made it work.

React NativeTypeScriptDesign SystemStorybook

When the second app in your company needs the same Button as the first, you copy-paste. When the third app needs it, you extract a component library. When the fifth app needs it, you wish you'd thought about the API earlier.

Here's how we built a 40-component library that 5 apps share today, and the patterns that made it work — including the mistakes we'd avoid next time.

Start with design tokens

The foundation of any component library is the design tokens: colors, spacing, typography, radii, shadows. These are the values that your components reference, so changing one token propagates through the entire library.

In React Native, tokens live in a single TypeScript file:

typescript
export const tokens = { color: { primary: '#A8BFA2', background: '#F7F6F0', ink: '#1A1A1A', inkMuted: '#666666', surface: '#FFFFFF', border: '#E5E5E5', }, space: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, }, radius: { sm: 4, md: 8, lg: 16, pill: 999, }, font: { body: 'System', mono: 'Menlo', }, };

Every component imports from this file. No hardcoded colors, no magic numbers. The tokens are the single source of truth.

Theming with context

For dark mode and brand variants, we wrap the tokens in a theme context. The ThemeProvider reads the user's preference and provides the right token set. Components consume tokens via a useTheme() hook, not direct imports.

This means the same Button component renders correctly in light mode, dark mode, and any brand theme — without changing a line of component code. Adding a new theme is just adding a new token set.

The variants API

The biggest API decision we made was the variants pattern. Each component accepts a variant prop that bundles together the size, intent, and visual treatment:

typescript
<Button variant="primary" size="md" onPress={...}>Save</Button> <Button variant="ghost" size="sm" onPress={...}>Cancel</Button> <Button variant="danger" size="md" loading>Delete</Button>

Internally, the component maps the variant to a set of style overrides. This is inspired by libraries like Stitches and Vanilla Extract, and it works well in React Native.

The key insight is that variants should be opinionated. Don't expose every style override as a prop — that defeats the purpose. If a component needs too many variants, it might be two components.

Storybook for documentation

We use Storybook to document every component. Each story shows the component in its main states, with controls to tweak the props. Designers can review components in isolation, and developers can see how the API behaves without writing test code.

The React Native Storybook setup is a bit heavier than web Storybook — you need a separate app shell to run it. But the investment pays off in faster design reviews and fewer "what does this component look like" questions.

Versioning and publishing

The library is a separate package in our monorepo. Apps import it as @company/ui, not as a relative path. This forces a clean API boundary and makes versioning explicit.

We use changesets to track changes. When a developer modifies the library, they add a changeset describing the change. The CI bot opens a PR that bumps the version and updates the changelog. Apps pull the new version when they're ready.

This is more setup than "just import from a shared folder," but it pays off the moment two apps need different versions of the same component.

What we'd do differently

If we were starting over, we'd invest more in the variants API upfront. The first version of Button had too many boolean props (primary, outline, ghost, danger, disabled). Refactoring to the variant prop was a breaking change that rippled through every app.

We'd also invest more in visual regression testing. Tools like Loki and Chromatic can catch unintended style changes before they ship. The first time a button color changed in one app and not the others, we learned our lesson.