How to structure a UI library that scales — folders, exports, and naming conventions

The folder structure you pick on day one is the one you'll live with across 40 components. Get it wrong and you'll be fighting your own codebase by article 10. Get it right and adding a new component takes five minutes and follows a pattern every contributor can learn in one sitting.
Before we write a single component, we need to talk about where it lives, what it exports, and what it's called. These decisions feel small early on. By the time you have 20 components, a bad structure is actively slowing you down — inconsistent imports, broken tree-shaking, and a root index.ts that nobody wants to touch.
This article locks in the pattern we'll use for every component in this series.
The naive structure and why it breaks
Most developers start here:
It works for three components. At ten it's already a mess — no separation between the component logic, its types, and its tests. At twenty, index.ts is a wall of exports and every file is a scroll to find. And if you ever want to add a Storybook story or a test file alongside the component, you have nowhere clean to put it.
The flat structure has no room to grow.
The component folder pattern
Every component in this library gets its own folder:
Each folder contains exactly three files to start:
Button.tsx — the component implementation. Nothing else lives here. No inline type declarations that belong in the types file, no utility functions that belong in a shared module.
Button.types.ts — all TypeScript types and interfaces for this component. Props, variant types, ref types. Keeping these separate means consumers can import just the types without importing the component — useful for testing utilities and documentation tooling.
index.ts — the component's public API. It re-exports everything from the other two files. This is what makes import { Button } from '@your-lib/ui/Button' work cleanly.
As the series progresses, each folder may also contain Button.test.tsx and Button.stories.tsx. The folder structure has room for them without any re-organisation.
The component index file
Each component's index.ts looks like this:
Two rules here:
Named exports only — never default exports. Default exports break tree-shaking in some bundler configurations and make refactoring harder. A named export tells the bundler exactly what's being used. A default export requires the bundler to guess.
Export types explicitly with export type. This keeps type-only exports out of the JavaScript output. It's cleaner and it avoids issues with isolatedModules in TypeScript strict configs.
The root entry point
The root src/index.ts re-exports from every component folder:
This is the file consumers import from when they do:
A few things to get right here:
Keep it flat. The root index.ts only re-exports from component folders — it never contains implementation. If you find yourself writing logic in the root entry point, it belongs in a component or a shared utility.
Add components here as you build them. Don't batch-add entries at the end of the series. Add the export the moment the component is ready. It keeps the entry point in sync with the actual state of the library.
Don't re-export everything with export * from. Barrel exports using * disable tree-shaking. Consumers end up bundling components they never imported. Always be explicit.
Naming conventions
Consistency matters more than the specific convention you pick. Here's what we'll use throughout this series:
Components — PascalCase:
Files — PascalCase matching the component name:
Props interfaces — ComponentNameProps:
Variant types — ComponentNameVariants:
CSS classes — Tailwind utility classes only. No custom class names, no BEM, no CSS modules. Everything is a Tailwind utility or a CVA variant. This keeps the styling layer predictable and eliminates an entire category of naming decisions.
Where shared utilities live
Some things don't belong to a single component. A cn utility for merging Tailwind classes. A useControllable hook for managing controlled and uncontrolled state. A set of shared ARIA helpers.
These live in:
The cn utility in particular is used by every component in the library. Set it up now:
Install the dependencies:
clsx handles conditional class logic cleanly. twMerge resolves Tailwind conflicts — so cn('px-4', 'px-6') correctly produces px-6 instead of both classes fighting each other. Every component will use this.
The full structure at the end of this series
Here's what the src/ folder will look like when all 40 components are built:
Every folder follows the same pattern. Every export follows the same rules. When you come back to this codebase six months from now — or when someone else opens it for the first time — the structure tells them exactly where to look.
Up next: Publishing your first React component to npm — the complete setup — package.json fields that matter, the .npmignore file, and how to run a test publish before anything goes live.
Related: Setting up a React component library with Vite, TypeScript, and Tailwind v4 — if you haven't completed the project setup yet, start there before locking in this folder structure.
Muhammad Athar is the founder of DesignDev.io and the engineer behind everything you read here. He writes about the decisions behind building real products — the components, the architecture, and the tradeoffs that don't make it into the tutorial.
Photo by Maksym Kaharlytskyi on Unsplash

