EmpireUI
Get Pro
← Blog8 min read#storybook#component library#typescript

Building a Component Library with Storybook 8 + TypeScript

Learn how to build a production-ready component library with Storybook 8 and TypeScript — from project setup to autodocs, testing, and publishing to npm.

Developer working on a React component library with TypeScript in VS Code

Why Storybook 8 Is Actually Worth It This Time

Storybook has had a complicated reputation. Early versions were slow, the config was a maze, and you'd spend more time fighting webpack than writing components. Storybook 8, released in early 2024, is a different beast — the team rewrote the build pipeline around Vite by default, and the difference in cold-start time is night and day.

Honestly, if your team has been avoiding Storybook because of past pain, it's time to take another look. The dev server now spins up in under 3 seconds on a mid-range laptop, and the new Component Story Format (CSF 3) cuts the boilerplate that used to make every story feel like homework.

That said, Storybook 8 still requires some intentional setup when you're pairing it with TypeScript and a monorepo. Getting the path aliases right, configuring autodocs, and wiring up your design tokens takes real effort. This guide walks through the whole thing — no hand-waving.

If you're pulling in third-party components or building something like Empire UI that covers a wide range of UI patterns, Storybook becomes your single source of truth. Every variant, every state, every edge case — documented and interactive.

Project Setup: Vite, TypeScript, and Storybook in One Shot

Start with a fresh Vite + React + TypeScript scaffold. Don't bolt Storybook onto an existing Create React App project if you can avoid it — the migration headaches aren't worth it.

npm create vite@latest my-ui-lib -- --template react-ts
cd my-ui-lib
npm install
npx storybook@latest init

The storybook init command in version 8 detects your framework and builder automatically. It'll scaffold a .storybook/main.ts and .storybook/preview.ts for you. Worth noting: it now generates config in TypeScript by default, which is exactly what you want.

One more thing — if you're in a monorepo (Turborepo, Nx, whatever), run storybook init from the package directory, not the root. Point stories in main.ts to ../src/**/*.stories.@(js|jsx|ts|tsx) and you're good.

Your tsconfig.json should have strict: true and moduleResolution: bundler for the cleanest TypeScript experience with Vite. Anything less and you'll hit subtle inference bugs that are annoying to track down.

Writing Stories That Actually Teach Something

CSF 3 stories are just objects. The mental model is simple: a meta export describes the component, named exports are individual stories. TypeScript inference does the heavy lifting on args.

import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'ghost', 'danger'],
    },
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: {
    children: 'Click me',
    variant: 'primary',
  },
};

export const Ghost: Story = {
  args: {
    children: 'Click me',
    variant: 'ghost',
  },
};

The tags: ['autodocs'] line is doing a lot of work there. It tells Storybook 8 to auto-generate a full docs page for this component, pulling prop tables directly from your TypeScript types. No JSDoc required — though adding it makes the output better.

In practice, the best stories are the ones that show failure states, not just the happy path. Write a Disabled story. Write a LongLabel story that stress-tests your 200px button constraint. Write a Loading story. These are the things your teammates will search for at 3pm on a Friday.

TypeScript Prop Tables and Autodocs: Getting It Right

The autodocs feature in Storybook 8 generates documentation from your TypeScript interfaces — but only if your types are explicit. Intersection types, generic components, and React.ComponentPropsWithoutRef spread all confuse the docgen parser.

Define a dedicated ButtonProps interface rather than relying on type inference from the component signature. This gives autodocs clean, flat prop tables with accurate descriptions.

export interface ButtonProps {
  /** The visual style of the button */
  variant?: 'primary' | 'ghost' | 'danger';
  /** Whether the button is in a loading state */
  loading?: boolean;
  /** Minimum width in px — defaults to 120 */
  minWidth?: number;
  children: React.ReactNode;
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
}

Quick aside: avoid exporting your props interface from a barrel index.ts if that barrel re-exports a dozen other things. The docgen plugin resolves types at import time and can get confused by deep re-export chains. Export directly from the component file.

If you're building components inspired by styles like those in the glassmorphism components collection — frosted glass effects, layered backgrounds — you'll likely have CSS custom property props. Document those as string with a good JSDoc description. Autodocs won't infer what --glass-blur: 12px means without your help.

Design Tokens, Themes, and the Preview File

Your .storybook/preview.ts is where you wire in global CSS, theme providers, and anything that needs to wrap every story. In 2026 most teams are using CSS custom properties for design tokens, which makes this straightforward.

// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import '../src/styles/tokens.css';
import '../src/styles/global.css';

const preview: Preview = {
  parameters: {
    backgrounds: {
      default: 'dark',
      values: [
        { name: 'dark', value: '#0f0f0f' },
        { name: 'light', value: '#ffffff' },
        { name: 'surface', value: '#1a1a2e' },
      ],
    },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;

If your library supports dark/light theming via a context provider, wrap stories using the decorators array. One decorator that injects your <ThemeProvider> and reads a Storybook toolbar global is all you need — don't overthink it.

Worth noting: if you're pulling visual inspiration from something like neumorphism or neobrutalism, your design tokens will look very different from a standard Material Design token set. Box shadow values like 8px 8px 16px #bebebe, -8px -8px 16px #ffffff deserve their own token namespace. Keep them separate from layout and typography tokens.

Testing Components Inside Storybook

Storybook 8 ships with @storybook/test — a thin wrapper around Vitest and Testing Library that lets you write interaction tests directly inside story files. It's one of the better ideas the ecosystem has had in years.

import { expect, fn, userEvent, within } from '@storybook/test';

export const ClickTest: Story = {
  args: {
    onClick: fn(),
    children: 'Submit',
  },
  play: async ({ args, canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button');
    await userEvent.click(button);
    await expect(args.onClick).toHaveBeenCalledOnce();
  },
};

These play functions run in the Storybook canvas, so you're testing against a real rendered DOM, not jsdom. You can catch visual regressions and interaction bugs in the same place you document the component. That colocation is genuinely useful.

For visual regression testing, pair Storybook with Chromatic (built by the same team) or run storybook test --coverage locally before pushing. Either way, your stories double as your test suite — you're not writing things twice.

Publishing and Consuming the Library

Once you're happy with the components, you need to build for distribution. Vite's library mode handles this cleanly. Set lib.entry in vite.config.ts to your main index.ts barrel, set formats: ['es', 'cjs'], and point package.json exports to the build output.

Don't forget to add sideEffects: false to your package.json — this enables tree-shaking in consumer apps. A component library with 40 components should not force consumers to download all 40 when they only use 3.

The tools at Empire UI follow this same principle. You can browse the components and pick what you need without pulling in the whole kitchen sink. Your library should work the same way.

After publishing to npm (or a private registry), consumers just npm install your-lib and import. Storybook's static build (storybook build) gives you a deployable docs site you can host on Vercel or Netlify in about 90 seconds. Ship the docs alongside the package — teams that skip this regret it within a month.

FAQ

Do I need Storybook 8 specifically, or will Storybook 7 work?

Storybook 7 still works, but 8 brings Vite as the default builder and a faster test runner. If you're starting fresh, there's no reason to go with 7.

How do I handle CSS Modules or Tailwind in Storybook stories?

Both work out of the box with the Vite builder. For Tailwind, just import your global CSS in .storybook/preview.ts — the PostCSS pipeline is picked up automatically from your vite.config.ts.

Can I use Storybook without publishing the library to npm?

Absolutely. Storybook is just a dev tool and docs host — you can use it for an internal design system that never touches npm. Build the static site and deploy it to your intranet.

How do I keep Storybook stories in sync as components evolve?

Colocate stories next to components (Button.stories.tsx beside Button.tsx) and review them in PRs like code. Treating stories as documentation that drifts is what causes the problem.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Building Design Systems That Scale: Engineering Guide 2026React Component API Design: Props, Variants and Compound PatternsStorybook 8 with React: Setup, Stories and Visual TestingPublishing npm Packages: From Local Component to Public Library