OneApp Docs
Platform Apps

storybook

Interactive component showcase and design system documentation built with Storybook 8. 5-theme system, density control, accessibility testing. Browse, test, and document all UI components in isolated environments.

Quick Start

Launch Storybook in 30 seconds:

pnpm --filter storybook dev

Opens at localhost:3700. Browse components, switch themes, test interactions. Skip to Quick Start →

Why storybook?

Components developed in application context. No isolated testing environment. Design inconsistencies across apps. Manual visual testing for regressions. Accessibility issues caught late. No shared component documentation. Theme variations tested manually.

storybook solves this by providing an isolated environment where components are developed, tested, and documented independently from applications.

Production-ready with Storybook 8, 5-theme system, density control, accessibility testing (a11y addon), visual regression (Chromatic), component testing (Vitest + Playwright), and auto-generated documentation.

Use cases

  • Component Development — Build and test components in isolation
  • Design System Docs — Interactive documentation for all UI components
  • Visual Regression — Automated screenshot comparison with Chromatic
  • Accessibility Testing — Automated a11y checks with axe-core
  • Theme Validation — Test components across 5 different themes

How it works

storybook provides an interactive component browser with live examples:

// stories/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "@repo/uni-ui";

const meta: Meta<typeof Button> = {
  title: "Components/Button",
  component: Button,
  tags: ["autodocs"], // Auto-generate docs
  argTypes: {
    variant: {
      control: "select",
      options: ["primary", "secondary", "outline", "ghost"]
    }
  }
};

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

// Each export = one story (one component state)
export const Primary: Story = {
  args: {
    variant: "primary",
    children: "Click me"
  }
};

export const WithInteraction: Story = {
  args: { children: "Test me" },
  play: async ({ canvasElement }) => {
    // Automated interaction testing
    const canvas = within(canvasElement);
    const button = canvas.getByRole("button");
    await userEvent.click(button);
    await expect(button).toHaveFocus();
  }
};

Uses Storybook 8 for component isolation, Vite for fast builds, and Chromatic for visual regression testing.

Key features

5-Theme System — Light, Dark, Corporate, Marketing, Admin

Density Control — Comfortable and Compact spacing options

Accessibility Testing — Built-in a11y addon with axe-core

Visual Regression — Chromatic integration for screenshot comparison

Component Testing — Vitest + Playwright for interaction tests

Auto Documentation — Generated from TypeScript types

Quick Start

1. Start the Storybook dev server

pnpm --filter storybook dev

2. Browse components

Open http://localhost:3700 and explore the component library.

3. Create a new story

platform/apps/storybook/stories/MyComponent.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { MyComponent } from "@repo/uni-ui";

const meta: Meta<typeof MyComponent> = {
  title: "Components/MyComponent",
  component: MyComponent,
  tags: ["autodocs"]
};

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

export const Default: Story = {
  args: {
    label: "Hello World"
  }
};

4. Test interactions

Add interaction testing
import { within, userEvent, expect } from "@storybook/test";

export const WithTest: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.click(canvas.getByRole("button"));
  }
};

That's it! Your component now appears in Storybook with auto-generated documentation, interactive controls, and automated accessibility testing.

Theme Switching

Use the theme toolbar at the top of Storybook to preview your component across all 5 themes instantly.


Technical Details

For Developers: Technical implementation details

Overview

PropertyValue
Locationplatform/apps/storybook
Port3700 (development)
FrameworkStorybook 8.5.0 + Vite 6
Visual TestingChromatic
Unit TestingVitest 4.0 + @vitest/browser-playwright
Accessibility@storybook/addon-a11y (axe-core)
Tech StackReact 19, TypeScript 5, Tailwind CSS 4

Project Structure

storybook/
├── stories/                          # Component stories
│   ├── Button.stories.tsx            # Button component stories
│   ├── Input.stories.tsx             # Input component stories
│   ├── Select.stories.tsx            # Select component stories
│   ├── Dialog.stories.tsx            # Dialog component stories
│   ├── Table.stories.tsx             # Table component stories
│   └── ...                           # 50+ component stories
├── .storybook/                       # Storybook configuration
│   ├── main.ts                       # Core Storybook config
│   ├── manager.ts                    # UI/Manager customization
│   ├── preview.tsx                   # Global decorators & parameters
│   ├── test-runner.ts                # Test runner configuration
│   ├── vitest.setup.ts               # Vitest setup
│   └── utils/                        # Helper utilities
│       ├── themes.ts                 # Theme configuration
│       └── decorators.ts             # Custom decorators
├── styles/                           # Global styles
│   └── storybook.css                 # Storybook-specific styles
├── public/                           # Static assets
│   ├── images/                       # Image assets
│   └── fonts/                        # Custom fonts
├── tailwind.config.ts                # Tailwind configuration
├── vitest.config.ts                  # Vitest configuration
├── chromatic.config.json             # Chromatic configuration
└── package.json                      # Dependencies and scripts

Theme System

5 Built-In Themes

ThemePrimary ColorDescriptionUse Case
LightBlueDefault light themeGeneral purpose, default UI
DarkBlueDark mode variantDark mode support
CorporateDeep BlueProfessional themeBusiness applications
MarketingPink/MagentaVibrant, energetic themeMarketing pages
AdminRedFunctional, alert themeAdmin interfaces

Theme Configuration

// .storybook/utils/themes.ts
export const themes = {
  light: {
    className: "theme-light",
    name: "Light",
    colors: {
      primary: "hsl(221 83% 53%)", // Blue
      background: "hsl(0 0% 100%)",
      foreground: "hsl(222 47% 11%)"
    }
  },
  dark: {
    className: "theme-dark",
    name: "Dark",
    colors: {
      primary: "hsl(221 83% 53%)",
      background: "hsl(222 47% 11%)",
      foreground: "hsl(210 40% 98%)"
    }
  },
  corporate: {
    className: "theme-corporate",
    name: "Corporate",
    colors: {
      primary: "hsl(214 100% 25%)", // Deep Blue
      background: "hsl(0 0% 100%)",
      foreground: "hsl(222 47% 11%)"
    }
  },
  marketing: {
    className: "theme-marketing",
    name: "Marketing",
    colors: {
      primary: "hsl(336 84% 50%)", // Pink/Magenta
      background: "hsl(0 0% 100%)",
      foreground: "hsl(222 47% 11%)"
    }
  },
  admin: {
    className: "theme-admin",
    name: "Admin",
    colors: {
      primary: "hsl(0 84% 60%)", // Red
      background: "hsl(0 0% 100%)",
      foreground: "hsl(222 47% 11%)"
    }
  }
};

Theme Decorator

// .storybook/preview.tsx
import { withThemeByClassName } from "@storybook/addon-themes";
import { themes } from "./utils/themes";

export const decorators = [
  withThemeByClassName({
    themes: {
      light: themes.light.className,
      dark: themes.dark.className,
      corporate: themes.corporate.className,
      marketing: themes.marketing.className,
      admin: themes.admin.className
    },
    defaultTheme: "light"
  })
];

Switching Themes in Stories

// stories/Button.stories.tsx
export const AllThemes: Story = {
  parameters: {
    // Test across all themes
    chromatic: {
      modes: {
        light: { theme: "light" },
        dark: { theme: "dark" },
        corporate: { theme: "corporate" },
        marketing: { theme: "marketing" },
        admin: { theme: "admin" }
      }
    }
  }
};

Density System

Comfortable vs Compact

// .storybook/preview.tsx
export const globalTypes = {
  density: {
    description: "Spacing density",
    defaultValue: "comfortable",
    toolbar: {
      title: "Density",
      icon: "component",
      items: [
        { value: "comfortable", title: "Comfortable" },
        { value: "compact", title: "Compact" },
      ],
      dynamicTitle: true,
    },
  },
};

// Decorator to apply density class
export const decorators = [
  (Story, context) => {
    const density = context.globals.density || "comfortable";
    return (
      <div className={`density-${density}`}>
        <Story />

    );
  },
];

CSS Variables:

/* styles/storybook.css */
:root {
  /* Comfortable density (default) */
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
}

.density-compact {
  /* Compact density (reduced spacing) */
  --spacing-xs: 0.125rem;
  --spacing-sm: 0.25rem;
  --spacing-md: 0.5rem;
  --spacing-lg: 0.75rem;
  --spacing-xl: 1rem;
}

Writing Stories

Basic Story Structure

// stories/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "@repo/uni-ui";

// Meta defines component metadata
const meta: Meta<typeof Button> = {
  title: "Components/Button", // Sidebar location
  component: Button,
  tags: ["autodocs"], // Enable auto-generated docs
  parameters: {
    layout: "centered", // Story layout
  },
  argTypes: {
    variant: {
      control: "select",
      options: ["primary", "secondary", "outline", "ghost", "destructive"],
      description: "Visual style variant",
    },
    size: {
      control: "select",
      options: ["sm", "md", "lg"],
      description: "Button size",
    },
    disabled: {
      control: "boolean",
      description: "Disabled state",
    },
  },
};

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

// Each export = one story
export const Primary: Story = {
  args: {
    variant: "primary",
    children: "Primary Button",
  },
};

export const Secondary: Story = {
  args: {
    variant: "secondary",
    children: "Secondary Button",
  },
};

export const AllVariants: Story = {
  render: () => (
    <div className="flex gap-4">
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="destructive">Delete</Button>

  ),
};

Story with Interaction Testing

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

export const WithInteraction: Story = {
  args: {
    children: "Click me",
    onClick: fn() // Mock function from @storybook/test
  },
  play: async ({ args, canvasElement }) => {
    const canvas = within(canvasElement);

    // Find the button
    const button = canvas.getByRole("button");

    // Test interaction
    await userEvent.click(button);

    // Assertions
    await expect(args.onClick).toHaveBeenCalled();
    await expect(button).toHaveFocus();
  }
};

Story with Custom Decorator

export const WithBackground: Story = {
  args: { children: "Button on background" },
  decorators: [
    (Story) => (
      <div className="p-8 bg-gray-100 rounded">
        <Story />

    ),
  ],
};

Story with Multiple Parameters

export const Documented: Story = {
  args: {
    variant: "primary",
    children: "Documented Button"
  },
  parameters: {
    docs: {
      description: {
        story: "This button demonstrates primary variant usage."
      }
    },
    a11y: {
      config: {
        rules: [
          { id: "color-contrast", enabled: true },
          { id: "button-name", enabled: true }
        ]
      }
    },
    chromatic: {
      delay: 300, // Wait 300ms before screenshot
      diffThreshold: 0.2 // 20% difference threshold
    }
  }
};

Storybook Configuration

Main Configuration

// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-vite";
import { join, dirname } from "node:path";

function getAbsolutePath(value: string): string {
  return dirname(require.resolve(join(value, "package.json")));
}

const config: StorybookConfig = {
  // Story locations
  stories: ["../stories/**/*.stories.@(ts|tsx)"],

  // Addons
  addons: [
    getAbsolutePath("@storybook/addon-essentials"),
    getAbsolutePath("@storybook/addon-a11y"),
    getAbsolutePath("@storybook/addon-themes"),
    getAbsolutePath("@storybook/addon-vitest"),
    getAbsolutePath("@chromatic-com/storybook"),
    getAbsolutePath("@storybook/addon-interactions")
  ],

  // Framework
  framework: {
    name: getAbsolutePath("@storybook/react-vite"),
    options: {}
  },

  // TypeScript
  typescript: {
    check: true,
    reactDocgen: "react-docgen-typescript",
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true)
    }
  },

  // Vite configuration
  viteFinal: async (config) => {
    return {
      ...config,
      resolve: {
        ...config.resolve,
        alias: {
          ...config.resolve?.alias,
          "@": join(__dirname, "../")
        }
      }
    };
  },

  // Static directories
  staticDirs: ["../public"],

  // Documentation
  docs: {
    autodocs: "tag" // Auto-generate docs for stories with 'autodocs' tag
  }
};

export default config;

Preview Configuration

// .storybook/preview.tsx
import type { Preview } from "@storybook/react";
import { withThemeByClassName } from "@storybook/addon-themes";
import "@repo/uni-ui/theme.css"; // Tailwind CSS 4 theme

const preview: Preview = {
  // Global parameters
  parameters: {
    // Controls
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
      expanded: true, // Expand controls panel by default
    },

    // Actions
    actions: {
      argTypesRegex: "^on[A-Z].*", // Auto-detect event handlers
    },

    // Backgrounds
    backgrounds: {
      default: "light",
      values: [
        { name: "light", value: "#ffffff" },
        { name: "dark", value: "#1a1a1a" },
        { name: "gray", value: "#f5f5f5" },
      ],
    },

    // Layout
    layout: "centered", // Default layout

    // Documentation
    docs: {
      toc: true, // Show table of contents
      source: {
        type: "dynamic", // Show source code
      },
    },

    // Viewport
    viewport: {
      viewports: {
        mobile: {
          name: "Mobile",
          styles: { width: "375px", height: "667px" },
        },
        tablet: {
          name: "Tablet",
          styles: { width: "768px", height: "1024px" },
        },
        desktop: {
          name: "Desktop",
          styles: { width: "1920px", height: "1080px" },
        },
      },
    },
  },

  // Global types (toolbar items)
  globalTypes: {
    theme: {
      description: "Global theme for components",
      defaultValue: "light",
      toolbar: {
        title: "Theme",
        icon: "paintbrush",
        items: [
          { value: "light", title: "Light", icon: "circlehollow" },
          { value: "dark", title: "Dark", icon: "circle" },
          { value: "corporate", title: "Corporate", icon: "diamond" },
          { value: "marketing", title: "Marketing", icon: "heart" },
          { value: "admin", title: "Admin", icon: "admin" },
        ],
        dynamicTitle: true,
      },
    },
    density: {
      description: "Spacing density",
      defaultValue: "comfortable",
      toolbar: {
        title: "Density",
        icon: "component",
        items: [
          { value: "comfortable", title: "Comfortable" },
          { value: "compact", title: "Compact" },
        ],
        dynamicTitle: true,
      },
    },
  },

  // Global decorators
  decorators: [
    // Theme decorator
    withThemeByClassName({
      themes: {
        light: "theme-light",
        dark: "theme-dark",
        corporate: "theme-corporate",
        marketing: "theme-marketing",
        admin: "theme-admin",
      },
      defaultTheme: "light",
    }),

    // Density decorator
    (Story, context) => {
      const density = context.globals.density || "comfortable";
      return (
        <div className={`density-${density}`}>
          <Story />

      );
    },
  ],
};

export default preview;

Manager Configuration

// .storybook/manager.ts
import { addons } from "@storybook/manager-api";
import { themes } from "@storybook/theming";

addons.setConfig({
  theme: themes.light,
  sidebar: {
    showRoots: true,
    collapsedRoots: ["other"]
  },
  toolbar: {
    title: { hidden: false },
    zoom: { hidden: false },
    eject: { hidden: false },
    copy: { hidden: false },
    fullscreen: { hidden: false }
  }
});

Accessibility Testing

A11y Addon Configuration

// .storybook/preview.tsx
parameters: {
  a11y: {
    // Default a11y configuration
    element: "#storybook-root",
    config: {
      rules: [
        {
          id: "color-contrast",
          enabled: true,
          reviewOnFail: true,
        },
        {
          id: "button-name",
          enabled: true,
        },
        {
          id: "label",
          enabled: true,
        },
        {
          id: "link-name",
          enabled: true,
        },
      ],
    },
    options: {
      runOnly: {
        type: "tag",
        values: ["wcag2a", "wcag2aa", "wcag21aa"],
      },
    },
  },
}

Per-Story A11y Configuration

export const AccessibleButton: Story = {
  args: {
    children: "Accessible Button",
    "aria-label": "Submit form"
  },
  parameters: {
    a11y: {
      config: {
        rules: [
          // Highlight specific rules
          { id: "color-contrast", enabled: true }
        ]
      }
    }
  }
};

Manual A11y Testing

import { expect } from "@storybook/test";
import { axe, toHaveNoViolations } from "jest-axe";

expect.extend(toHaveNoViolations);

export const ManualA11yTest: Story = {
  play: async ({ canvasElement }) => {
    const results = await axe(canvasElement);
    expect(results).toHaveNoViolations();
  }
};

Visual Regression Testing

Chromatic Configuration

// chromatic.config.json
{
  "projectId": "your-project-id",
  "buildScriptName": "build-storybook",
  "exitZeroOnChanges": true,
  "exitOnceUploaded": true,
  "onlyChanged": true,
  "externals": ["public/**"],
  "debug": false
}

Running Chromatic

# Run Chromatic visual tests
pnpm --filter storybook chromatic

# With specific options
npx chromatic \
  --project-token=$CHROMATIC_PROJECT_TOKEN \
  --only-changed \
  --auto-accept-changes main

Chromatic-Specific Story Configuration

export const ChromaticTest: Story = {
  parameters: {
    chromatic: {
      delay: 500, // Wait 500ms before screenshot
      diffThreshold: 0.3, // 30% difference threshold
      pauseAnimationAtEnd: true, // Pause animations
      viewports: [375, 768, 1920], // Test multiple viewports
      modes: {
        light: { theme: "light" },
        dark: { theme: "dark" }
      }
    }
  }
};

Disabling Chromatic for Specific Stories

export const SkipChromatic: Story = {
  parameters: {
    chromatic: { disableSnapshot: true }
  }
};

Component Testing

Vitest Configuration

// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    setupFiles: [".storybook/vitest.setup.ts"],
    browser: {
      enabled: true,
      name: "playwright",
      provider: "playwright",
      headless: true
    },
    coverage: {
      provider: "v8",
      reporter: ["text", "json", "html"],
      exclude: ["**/*.stories.tsx", "**/node_modules/**"]
    }
  }
});

Test Runner Configuration

// .storybook/test-runner.ts
import type { TestRunnerConfig } from "@storybook/test-runner";

const config: TestRunnerConfig = {
  async postVisit(page, context) {
    // Run accessibility tests
    const elementHandler = await page.$("#storybook-root");
    const innerHTML = await elementHandler?.innerHTML();
    expect(innerHTML).toBeTruthy();
  }
};

export default config;

Running Tests

# Run interaction tests
pnpm --filter storybook test-storybook

# Run with coverage
pnpm --filter storybook coverage:collect

# Run specific story
pnpm --filter storybook test-storybook --stories-filter="Button/*"

Environment Variables

# Storybook Server
STORYBOOK_PORT=3700

# Chromatic
CHROMATIC_PROJECT_TOKEN="chpt_..."
CHROMATIC_APP_CODE="..."

# Feature Flags
NEXT_PUBLIC_ENABLE_STORYBOOK_PREVIEW="true"

Scripts Reference

{
  "scripts": {
    "dev": "storybook dev -p 3700",
    "build": "storybook build",
    "build-storybook": "storybook build",
    "preview": "http-server storybook-static -p 3700",
    "test-storybook": "test-storybook",
    "coverage:collect": "test-storybook --coverage",
    "chromatic": "chromatic --exit-zero-on-changes",
    "lint": "eslint stories/**/*.tsx",
    "typecheck": "tsc --noEmit"
  }
}

Command Descriptions:

  • dev — Start Storybook dev server on port 3700
  • build — Build static Storybook site
  • preview — Preview built Storybook locally
  • test-storybook — Run interaction tests
  • coverage:collect — Run tests with coverage report
  • chromatic — Run visual regression tests
  • lint — Lint story files
  • typecheck — Type check TypeScript

Deployment

Static Build

# Build Storybook for deployment
pnpm --filter storybook build

# Output directory: storybook-static/
# Can be deployed to any static hosting

Chromatic Hosting

Chromatic automatically hosts your Storybook and provides shareable URLs:

# Deploy to Chromatic
npx chromatic --project-token=$CHROMATIC_PROJECT_TOKEN

# Output:
# ✔ Storybook published to https://your-project.chromatic.com
# ✔ Visual tests passed: 0 changes detected

Cloudflare Pages Deployment

# Deploy to Cloudflare Pages
pnpm --filter storybook build
wrangler pages deploy storybook-static --project-name=storybook

Vercel Deployment

# Deploy to Vercel
pnpm --filter storybook build
vercel --prod storybook-static

Best Practices

Story Organization

stories/
├── Components/           # UI components
│   ├── Button.stories.tsx
│   ├── Input.stories.tsx
│   └── ...
├── Forms/                # Form components
│   ├── FormField.stories.tsx
│   ├── FormLabel.stories.tsx
│   └── ...
├── Layout/               # Layout components
│   ├── Container.stories.tsx
│   ├── Grid.stories.tsx
│   └── ...
└── Examples/             # Full page examples
    ├── LoginPage.stories.tsx
    └── DashboardPage.stories.tsx

Naming Conventions

  • Story files: ComponentName.stories.tsx
  • Story exports: PascalCase (e.g., Primary, WithIcon)
  • Story titles: Category/ComponentName (e.g., Components/Button)

Documentation

// Add JSDoc comments for auto-generated docs
export interface ButtonProps {
  /** Visual style variant */
  variant?: "primary" | "secondary" | "outline";

  /** Button size */
  size?: "sm" | "md" | "lg";

  /** Disabled state */
  disabled?: boolean;

  /** Click handler */
  onClick?: () => void;
}

Interaction Testing

// Test user interactions comprehensively
export const CompleteInteraction: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // 1. Find elements
    const input = canvas.getByRole("textbox");
    const button = canvas.getByRole("button");

    // 2. Perform actions
    await userEvent.type(input, "test@example.com");
    await userEvent.click(button);

    // 3. Assert results
    await expect(input).toHaveValue("test@example.com");
    await expect(button).toBeDisabled();
  }
};

Troubleshooting

Storybook Not Loading

Issue: Storybook fails to start with build errors.

Solution: Clear cache and rebuild:

pnpm --filter storybook clean
rm -rf node_modules/.cache
pnpm --filter storybook dev

Theme Not Switching

Issue: Theme toolbar doesn't change component appearance.

Solution: Verify theme decorator is applied:

// .storybook/preview.tsx
import { withThemeByClassName } from "@storybook/addon-themes";

export const decorators = [
  withThemeByClassName({
    themes: {
      light: "theme-light",
      dark: "theme-dark"
    },
    defaultTheme: "light"
  })
];

Chromatic Visual Diffs

Issue: Chromatic shows false positive visual differences.

Solution: Increase diff threshold or add delay:

export const Story: Story = {
  parameters: {
    chromatic: {
      delay: 1000, // Wait 1s for animations
      diffThreshold: 0.5 // Increase threshold to 50%
    }
  }
};

A11y Violations

Issue: Accessibility tests failing.

Solution: Fix ARIA labels and color contrast:

<Button
  aria-label="Submit form" // Add ARIA label
  className="bg-blue-600 text-white" // Ensure sufficient contrast
>
  Submit
</Button>
  • @repo/uni-ui — UI components showcased in Storybook
  • docs — Documentation site using some components

On this page