Use Cases (Diperbarui: 2/6/2026)

Pengembangan Storybook dengan Claude Code: UI, test, dan CI

Bangun Storybook dengan Claude Code: setup, TypeScript stories, interaction test, CI, dan review Chromatic.

Pengembangan Storybook dengan Claude Code: UI, test, dan CI

Storybook bukan galeri, melainkan kontrak UI

Storybook membantu mengembangkan komponen UI di luar aplikasi utama. Untuk pemula, ia bisa dianggap sebagai katalog tempat membuka button, form, atau card secara terpisah. Di proyek nyata, nilainya lebih besar: props, state, interaksi, aksesibilitas, review desain, dan pemeriksaan CI bisa dicatat di satu tempat.

Kalau Anda hanya meminta Claude Code “tambahkan Storybook”, hasilnya bisa berhenti di beberapa file Button.stories.tsx. Agar berguna, tentukan dulu komponen mana yang masuk, state apa yang wajib ada, apa yang diverifikasi oleh fungsi play, dan bagaimana PR menjalankan Chromatic atau pemeriksaan visual yang setara.

Panduan ini memakai React + TypeScript + Vite dengan pola Storybook 8/9+: .storybook/main.ts, CSF dengan Meta/StoryObj, satisfies, interaction test memakai @storybook/test, @storybook/addon-vitest, dan CI yang siap dipakai bersama Chromatic. Referensi resmi yang dicek: main configuration, TypeScript stories, interaction testing, Vitest addon, dan Chromatic for Storybook.

Jika Anda juga membangun design system, baca Claude Code design system dan Claude Code accessibility. Storybook paling berguna saat menjaga keputusan desain dan alur pengguna, bukan hanya menampilkan screenshot.

Mulai dari 3 kasus praktis

Tidak perlu memasukkan semua UI sekaligus. Mulailah dari komponen yang sering berubah dan dekat dengan signup, pembelian, konsultasi, atau konversi konten.

KasusState di StorybookTugas untuk Claude Code
Button design systemprimary, secondary, loading, disabled, semua ukuranTipe props, stories, controls, autodocs
Form signup gratiskosong, email invalid, proses submit, suksesplay, mock, validasi
CTA artikel atau produkdefault, highlight, mobile, teks panjangResponsif, visual diff, cek internal link

Tiga contoh ini sudah cukup untuk membantu designer meninjau state, developer memahami kontrak props, dan pemilik situs memastikan CTA yang dekat dengan revenue tetap bekerja.

Gunakan setup berbasis Vite

Untuk Vitest addon, framework Storybook berbasis Vite adalah jalur paling rapi. Di React + Vite gunakan @storybook/react-vite; di Next.js pertimbangkan @storybook/nextjs-vite.

npm create vite@latest storybook-claude-demo -- --template react-ts
cd storybook-claude-demo
npm install
npm create storybook@latest
npx storybook add @storybook/addon-a11y
npx storybook add @storybook/addon-vitest
npm install -D chromatic @vitest/browser-playwright playwright
npx playwright install chromium
{
  "scripts": {
    "dev": "vite",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "test-storybook": "vitest --project=storybook --run",
    "chromatic": "chromatic"
  }
}
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  framework: '@storybook/react-vite',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: ['@storybook/addon-docs', '@storybook/addon-a11y', '@storybook/addon-vitest'],
  docs: {
    defaultName: 'Docs',
  },
  staticDirs: ['../public'],
};

export default config;
// .storybook/preview.ts
import type { Preview } from '@storybook/react-vite';

const preview: Preview = {
  tags: ['autodocs'],
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
    a11y: {
      test: 'todo',
    },
  },
};

export default preview;

Contoh 1: Button dengan stories bertipe

Jangan membuat props khusus Storybook. Story harus menunjukkan kontrak komponen yang benar-benar dipakai.

// src/components/Button.tsx
import type { ButtonHTMLAttributes, CSSProperties, ReactNode } from 'react';

type ButtonVariant = 'primary' | 'secondary' | 'outline';
type ButtonSize = 'sm' | 'md' | 'lg';

export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
  children: ReactNode;
  variant?: ButtonVariant;
  size?: ButtonSize;
  loading?: boolean;
};

const variantStyle: Record<ButtonVariant, CSSProperties> = {
  primary: { background: '#2563eb', color: '#ffffff', borderColor: '#2563eb' },
  secondary: { background: '#0f172a', color: '#ffffff', borderColor: '#0f172a' },
  outline: { background: '#ffffff', color: '#0f172a', borderColor: '#94a3b8' },
};

const sizeStyle: Record<ButtonSize, CSSProperties> = {
  sm: { minHeight: 32, padding: '0 12px', fontSize: 14 },
  md: { minHeight: 40, padding: '0 16px', fontSize: 15 },
  lg: { minHeight: 48, padding: '0 20px', fontSize: 16 },
};

export function Button({
  children,
  variant = 'primary',
  size = 'md',
  loading = false,
  disabled,
  style,
  ...props
}: ButtonProps) {
  return (
    <button
      {...props}
      disabled={disabled || loading}
      aria-busy={loading || undefined}
      style={{
        ...variantStyle[variant],
        ...sizeStyle[size],
        borderWidth: 1,
        borderStyle: 'solid',
        borderRadius: 6,
        fontWeight: 700,
        cursor: disabled || loading ? 'not-allowed' : 'pointer',
        opacity: disabled ? 0.55 : 1,
        ...style,
      }}
    >
      {loading ? 'Sending...' : children}
    </button>
  );
}
// src/components/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';

const meta = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
  args: {
    children: 'Start free trial',
    variant: 'primary',
    size: 'md',
  },
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'outline'],
    },
    size: {
      control: 'inline-radio',
      options: ['sm', 'md', 'lg'],
    },
    loading: { control: 'boolean' },
    disabled: { control: 'boolean' },
  },
} satisfies Meta<typeof Button>;

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

export const Primary = {} satisfies Story;

export const Secondary = {
  args: {
    variant: 'secondary',
    children: 'View pricing',
  },
} satisfies Story;

export const AllSizes = {
  render: () => (
    <div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
    </div>
  ),
} satisfies Story;

export const Loading = {
  args: {
    loading: true,
  },
} satisfies Story;

Contoh 2: interaction test untuk form signup

Fungsi play berjalan setelah story dirender. Dengan begitu, kita bisa menguji input, error, dan submit.

// src/components/SignupForm.tsx
import type { FormEvent } from 'react';
import { useState } from 'react';
import { Button } from './Button';

export type SignupFormData = {
  name: string;
  email: string;
  plan: 'free' | 'team';
};

type SignupFormProps = {
  plan?: SignupFormData['plan'];
  onSubmit?: (data: SignupFormData) => Promise<void> | void;
};

export function SignupForm({ plan = 'free', onSubmit }: SignupFormProps) {
  const [message, setMessage] = useState('');

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = new FormData(event.currentTarget);
    const data: SignupFormData = {
      name: String(form.get('name') ?? ''),
      email: String(form.get('email') ?? ''),
      plan,
    };

    if (!data.email.includes('@')) {
      setMessage('Please enter a valid email address.');
      return;
    }

    await onSubmit?.(data);
    setMessage('Thanks, we will send the setup guide.');
  }

  return (
    <form onSubmit={handleSubmit} style={{ display: 'grid', gap: 12, maxWidth: 360 }}>
      <label>
        Name
        <input name="name" required style={{ display: 'block', width: '100%', minHeight: 36 }} />
      </label>
      <label>
        Email
        <input name="email" type="email" required style={{ display: 'block', width: '100%', minHeight: 36 }} />
      </label>
      <Button type="submit">Get the guide</Button>
      {message ? <p role="status">{message}</p> : null}
    </form>
  );
}
// src/components/SignupForm.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, fn, userEvent, within } from '@storybook/test';
import { SignupForm } from './SignupForm';

const meta = {
  title: 'Components/SignupForm',
  component: SignupForm,
  args: {
    plan: 'team',
    onSubmit: fn(),
  },
} satisfies Meta<typeof SignupForm>;

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

export const Empty = {} satisfies Story;

export const InvalidEmail = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    await userEvent.type(canvas.getByLabelText('Name'), 'Masa');
    await userEvent.type(canvas.getByLabelText('Email'), 'masa.example.com');
    await userEvent.click(canvas.getByRole('button', { name: 'Get the guide' }));

    await expect(canvas.getByRole('status')).toHaveTextContent('valid email');
  },
} satisfies Story;

export const SuccessfulSubmit = {
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);

    await userEvent.type(canvas.getByLabelText('Name'), 'Masa');
    await userEvent.type(canvas.getByLabelText('Email'), 'masa@example.com');
    await userEvent.click(canvas.getByRole('button', { name: 'Get the guide' }));

    await expect(args.onSubmit).toHaveBeenCalledWith({
      name: 'Masa',
      email: 'masa@example.com',
      plan: 'team',
    });
    await expect(canvas.getByRole('status')).toHaveTextContent('setup guide');
  },
} satisfies Story;

Contoh 3: Vitest dan CI visual

Vitest addon mengubah stories menjadi component test di browser. Story yang memiliki play juga menjalankan interaksi dan assertion.

// .storybook/vitest.setup.ts
import { setProjectAnnotations } from '@storybook/react-vite';
import * as previewAnnotations from './preview';

setProjectAnnotations([previewAnnotations]);
// vitest.config.ts
import { defineConfig, defineProject, mergeConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import viteConfig from './vite.config';

const dirname = path.dirname(fileURLToPath(import.meta.url));

export default mergeConfig(
  viteConfig,
  defineConfig({
    test: {
      projects: [
        defineProject({
          extends: true,
          plugins: [
            storybookTest({
              configDir: path.join(dirname, '.storybook'),
              storybookScript: 'npm run storybook -- --ci',
              storybookUrl: 'http://localhost:6006',
              tags: {
                include: ['test'],
                exclude: ['experimental'],
              },
            }),
          ],
          test: {
            name: 'storybook',
            browser: {
              enabled: true,
              provider: playwright({}),
              headless: true,
              instances: [{ browser: 'chromium' }],
            },
            setupFiles: ['./.storybook/vitest.setup.ts'],
          },
        }),
      ],
    },
  }),
);
# .github/workflows/storybook.yml
name: storybook

on:
  pull_request:

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npm run test-storybook
      - run: npm run build-storybook
      - name: Publish to Chromatic
        if: ${{ secrets.CHROMATIC_PROJECT_TOKEN != '' }}
        run: npx chromatic --project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }}

Template permintaan untuk Claude Code

Siapkan Storybook untuk proyek React + TypeScript + Vite.

Scope:
- src/components/Button.tsx
- src/components/SignupForm.tsx

Requirement:
- Gunakan @storybook/react-vite di .storybook/main.ts
- Tulis CSF dengan Meta/StoryObj dan satisfies
- Buat stories Button untuk primary, secondary, outline, loading, disabled, dan semua size
- Buat play functions SignupForm untuk invalid email dan successful submit
- Gunakan within, userEvent, expect, dan fn dari @storybook/test
- Konfigurasi Vitest addon agar test-storybook berjalan di CI
- Tambahkan build-storybook dan visual check gaya Chromatic di GitHub Actions

Jangan:
- Jangan memakai pseudocode
- Jangan menambah props yang tidak ada di komponen asli
- Jangan menulis secrets di kode
- Jangan memformat file di luar scope

Laporan akhir:
- File yang berubah
- Command yang dijalankan
- Risiko tersisa

Kesalahan yang sering terjadi

Pertama, membuat props khusus Storybook. Kedua, hanya menguji happy path dan lupa invalid input, loading, disabled, atau double submit. Ketiga, membiarkan tanggal, random value, animasi, dan gambar remote membuat diff visual terlalu berisik. Keempat, mencampur @storybook/react, @storybook/react-vite, dan @storybook/nextjs-vite. Kelima, lupa menginstal Chromium di CI. Keenam, menunda CTA, product card, dan inquiry form, padahal bagian itu paling dekat dengan revenue.

CTA dan hasil praktik

Untuk media teknis atau situs produk, Storybook juga bisa menjaga alur monetisasi. Form download gratis, CTA produk, form konsultasi, dan kartu internal link dapat dibuat sebagai stories, diuji lewat interaction test, dan direview secara visual di setiap PR.

Untuk mulai dengan pola prompt yang bisa dipakai ulang, gunakan cheat sheet gratis. Untuk workflow yang lebih siap pakai, lihat produk dan template. Jika ingin menggabungkan Storybook, CI, Chromatic, aksesibilitas, dan review CTA di repository nyata, gunakan training dan konsultasi Claude Code.

Saat dicoba pada proyek React kecil, penghematan terbesar bukan dari variasi Button, tetapi dari story gagal pada SignupForm. Memisahkan InvalidEmail dan SuccessfulSubmit, lalu menjalankan test-storybook dan build-storybook di CI, membuat masalah form dan CTA muncul lebih cepat sebelum perlu meminta koreksi tambahan ke Claude Code.

#Claude Code #Storybook #pengembangan UI #components #frontend
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.