Pengembangan Storybook dengan Claude Code: UI, test, dan CI
Bangun Storybook dengan Claude Code: setup, TypeScript stories, interaction test, CI, dan review Chromatic.
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.
| Kasus | State di Storybook | Tugas untuk Claude Code |
|---|---|---|
| Button design system | primary, secondary, loading, disabled, semua ukuran | Tipe props, stories, controls, autodocs |
| Form signup gratis | kosong, email invalid, proses submit, sukses | play, mock, validasi |
| CTA artikel atau produk | default, highlight, mobile, teks panjang | Responsif, 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.
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.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.