Library

Storybook

Interactive component workshop for Sitka. Browse every component in isolation, switch themes, test viewports, and check accessibility — all without running the full app.

Overview

Sitka ships with Storybook 8 pre-configured. All 31 components have stories in src/stories/. The setup uses @storybook/react-vite so Storybook builds independently of Next.js — no version coupling, instant HMR.

Storybook

8.6

Framework

react-vite

Tailwind

v4 via Vite

Stories

31 components

Run locally

Start Storybook on localhost:6006:

bash
npm run storybook

To build a static version for deployment:

bash
npm run build-storybook
# output → storybook-static/

Built-in features

Theme switcher

Toggle between dark and light via the toolbar. Uses data-theme attribute — identical to the production implementation.

Viewport presets

Mobile (390 × 844), Tablet (768 × 1024), Desktop (1440 × 900) — match real device breakpoints.

Accessibility audit

The a11y addon runs axe-core on every story automatically. Violations surface in the Accessibility panel.

Controls & autodocs

All props are wired to interactive controls. Component pages are auto-generated from TypeScript types and JSDoc.

Writing a story

Stories live in src/stories/ and follow the Component Story Format (CSF 3). Here is the Button story as a reference:

src/stories/Button.stories.tsx
tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "@/components/ui/Button";

const meta: Meta<typeof Button> = {
  title: "Actions/Button",
  component: Button,
  tags: ["autodocs"],
  argTypes: {
    variant: { control: "select", options: ["primary", "secondary", "ghost", "danger"] },
    size:    { control: "select", options: ["sm", "md", "lg"] },
    disabled: { control: "boolean" },
    loading:  { control: "boolean" },
  },
};
export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: { variant: "primary", size: "md", children: "Button" },
};

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

Configuration

The config lives in .storybook/.

.storybook/main.ts

.storybook/main.ts
ts
import type { StorybookConfig } from "@storybook/react-vite";
import path from "path";
import tailwindVite from "@tailwindcss/vite";

const config: StorybookConfig = {
  stories: ["../src/**/*.stories.@(ts|tsx)"],
  addons: ["@storybook/addon-essentials", "@storybook/addon-a11y"],
  framework: { name: "@storybook/react-vite", options: {} },
  staticDirs: ["../public"],
  async viteFinal(cfg) {
    const { mergeConfig } = await import("vite");
    return mergeConfig(cfg, {
      plugins: [tailwindVite()],
      resolve: { alias: { "@": path.resolve(__dirname, "../src") } },
    });
  },
};
export default config;

.storybook/preview.tsx

.storybook/preview.tsx
tsx
import type { Preview, Decorator } from "@storybook/react";
import React from "react";
import "../src/app/globals.css";

const withTheme: Decorator = (Story, context) => {
  const theme = context.globals.theme ?? "dark";
  return (
    <div
      data-theme={theme}
      style={{
        background: theme === "dark" ? "rgb(9 9 12)" : "rgb(252 252 253)",
        minHeight: "100vh",
        padding: "2rem",
        colorScheme: theme,
      }}
    >
      <Story />
    </div>
  );
};

export default {
  decorators: [withTheme],
  globalTypes: {
    theme: {
      name: "Theme",
      defaultValue: "dark",
      toolbar: {
        icon: "circlehollow",
        items: [
          { value: "dark",  title: "Dark",  icon: "moon" },
          { value: "light", title: "Light", icon: "sun" },
        ],
        showName: true,
      },
    },
  },
} satisfies Preview;

Related