Skip to content
← All posts
design-systems

Why my design system runs on shadcn/ui, React, and Zod

A design system is a pile of tool choices. Here is the reasoning behind mine, and the one problem each piece actually solves.

Design system cover
Published
Reading time
3 min read

A design system is only as good as the choices underneath it. It is easy to list a stack. It is more useful to explain why each piece is there and what it would cost to remove. Here is the reasoning behind mine.

shadcn/ui: own the components, do not import them

The biggest decision was to not adopt a component library in the traditional way. With shadcn/ui the components live in my repo as code I own, not as a versioned dependency I import and hope about. When a design changes or an edge case shows up, I edit the component instead of fighting a library’s API or waiting for an upstream release. It is the difference between renting your UI primitives and owning them, and for a system meant to last across several apps, ownership wins.

Tailwind: styling that travels with the component

Paired with shadcn, Tailwind keeps styling co-located with the markup and driven by design tokens. There is no separate stylesheet to keep in sync and no naming debates, and theming becomes a matter of swapping token values. When the whole system shares one set of tokens, consistency is the default instead of something I have to police in review.

TypeScript: the contract every component honors

Types are the contract. A design system is a public API for the rest of the codebase, and TypeScript makes that API explicit: props are documented by their types, misuse fails at compile time, and refactors are safe because the compiler finds every call site. Without it, a shared component library is a field of quiet landmines.

Zod: validation where data actually enters

TypeScript disappears at runtime, so anywhere untrusted data comes in (forms, API responses, URL params) I reach for Zod. In the form components especially, pairing Zod with React Hook Form gives me one schema that drives both the runtime validation and the inferred TypeScript type. One source of truth, checked at both runtime and compile time. That removes a whole category of “the data was not the shape I assumed” bugs.

TanStack Query and Zustand: two kinds of state, two tools

I keep server state and client state separate on purpose. TanStack Query owns anything that comes from the server: fetching, caching, and revalidation, the boring but critical parts I do not want to hand-roll. Zustand owns the small, local UI state that does not belong on the server at all. Cramming both into one global store is a classic source of pain, and splitting them keeps each simple.

Storybook: build components in isolation

Every component is developed and documented in Storybook, away from any app that consumes it. That forces the component to stand on its own, makes every state and edge case visible, and doubles as living documentation for the team. If a component is painful to render in isolation, that is usually a design smell worth fixing early.

Changesets and CI: a shared package needs real versioning

Because the system is a package other apps depend on, changes need versioning and a changelog, not vibes. Changesets handles semantic version bumps and release notes, and GitHub Actions runs the checks and publishes. Consumers upgrade deliberately, and they can see exactly what changed.

Vitest and Playwright: test the system, not just the apps

Unit tests with Vitest cover component logic, and Playwright covers the interactions that only break in a real browser. A bug caught in the design system is a bug caught once, instead of in every app that uses it. That leverage is the whole point.

The thread

None of these are trendy picks for their own sake. Each one earns its place by solving a specific, recurring problem: own your components, co-locate styling, enforce contracts, validate at the edges, separate your state, build in isolation, version deliberately, and test the shared thing once. A design system is leverage, and the tooling is what keeps that leverage from turning into a liability.