Referensi cepat Storybook untuk component-driven development. Stories format, args, decorators, addons, testing, dan composition. Perfect buat developer yang bangun design system.
# Init di project yang udah ada (React, Vue, Svelte, Angular, dll)
npx storybook@latest init
# Install manual
npm install storybook @storybook/react @storybook/react-vite --dev
npx storybook initSetelah init, Storybook bakal bikin:
.storybook/
main.ts # Konfigurasi Storybook
preview.ts # Global decorators dan parameters
src/stories/ # Contoh stories# Dev mode (hot reload)
npm run storybook
# Build static storybook (untuk deploy)
npm run build-storybook
# Dengan custom port
npx storybook dev -p 6007
# CI mode (tanpa auto-open browser)
npx storybook dev --ci -p 6007// src/components/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger', 'ghost'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
disabled: { control: 'boolean' },
onClick: { action: 'clicked' },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
// Default story
export const Primary: Story = {
args: {
variant: 'primary',
size: 'md',
children: 'Click Me',
},
};
// Dengan args berbeda
export const Secondary: Story = {
args: {
...Primary.args,
variant: 'secondary',
children: 'Secondary',
},
};
export const Disabled: Story = {
args: {
...Primary.args,
disabled: true,
children: 'Disabled',
},
};
// Large size
export const Large: Story = {
args: {
...Primary.args,
size: 'lg',
children: 'Large Button',
},
};export const Template: Story = {
render: (args) => ({
component: Button,
props: args,
}),
};
// Story dengan custom render
export const WithIcon: Story = {
render: () => (
<Button variant="primary">
<svg width="16" height="16" fill="currentColor">
<path d="M8 0a8 8 0 100 16A8 8 0 008 0z" />
</svg>
Save
</Button>
),
};
// Multiple stories dari satu template
const Template: StoryFn<typeof Button> = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = { variant: 'primary', children: 'Primary' };
export const Secondary = Template.bind({});
Secondary.args = { variant: 'secondary', children: 'Secondary' };export const Playground: Story = {
args: {
variant: 'primary',
size: 'md',
children: 'Playground',
disabled: false,
loading: false,
},
};
// Args composition
const baseArgs = {
children: 'Button',
size: 'md' as const,
};
export const Primary: Story = { args: { ...baseArgs, variant: 'primary' } };
export const Danger: Story = { args: { ...baseArgs, variant: 'danger' } };const meta = {
argTypes: {
// Text input
label: { control: 'text' },
// Number input
count: { control: 'number', min: 0, max: 100, step: 5 },
// Boolean
show: { control: 'boolean' },
// Select dropdown
color: {
control: 'select',
options: ['red', 'green', 'blue', 'yellow'],
},
// Radio buttons
layout: {
control: 'radio',
options: ['horizontal', 'vertical'],
},
// Inline radio
align: {
control: 'inline-radio',
options: ['left', 'center', 'right'],
},
// Range slider
opacity: {
control: 'range',
min: 0,
max: 1,
step: 0.1,
},
// Object/JSON
style: { control: 'object' },
// Date picker
date: { control: 'date' },
// Color picker
backgroundColor: { control: 'color' },
// File upload
image: { control: 'file', accept: 'image/*' },
// Custom control
customProp: {
control: {
type: 'select',
labels: { 0: 'Small', 1: 'Medium', 2: 'Large' },
},
options: [0, 1, 2],
mapping: { 0: 'sm', 1: 'md', 2: 'lg' },
},
},
};Wrap story dengan markup atau provider tambahan.
// Global decorator untuk semua stories
const meta = {
decorators: [
(Story) => (
<div style={{ padding: '20px', background: '#f0f0f0' }}>
<Story />
</div>
),
],
};
// Per-story decorator
export const DarkMode: Story = {
decorators: [
(Story) => (
<div className="dark-theme">
<Story />
</div>
),
],
};
// Provider decorator (Redux, Theme, Router, dll)
const meta = {
decorators: [
(Story) => (
<ThemeProvider theme={darkTheme}>
<BrowserRouter>
<Story />
</BrowserRouter>
</ThemeProvider>
),
],
};// .storybook/preview.ts
import type { Preview } from '@storybook/react';
const preview: Preview = {
decorators: [
(Story) => (
<div style={{ fontFamily: 'Inter, sans-serif' }}>
<Story />
</div>
),
],
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#1a1a1a' },
{ name: 'brand', value: '#6366f1' },
],
},
},
};
export default preview;// Per-story parameters
export const Canvas: Story = {
parameters: {
layout: 'fullscreen',
backgrounds: { default: 'dark' },
viewport: { defaultViewport: 'mobile1' },
},
};
// Common parameters
const meta = {
parameters: {
layout: 'centered', // 'centered' | 'fullscreen' | 'padded'
docs: {
description: {
component: 'Reusable button component with variants.',
},
},
design: {
type: 'figma',
url: 'https://www.figma.com/file/abc123/Button',
},
},
};// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: [
'../src/**/*.mdx',
'../src/**/*.stories.@(ts|tsx)',
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-coverage',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag', // 'tag' | true | false
defaultName: 'Documentation',
},
staticDirs: ['../public'],
typescript: {
check: false,
reactDocgen: 'react-docgen-typescript',
},
viteFinal: async (config) => {
// Custom Vite config
config.resolve.alias = {
...config.resolve.alias,
'@': '/src',
};
return config;
},
};
export default config;// Actions (event logging)
import type { Meta } from '@storybook/react';
const meta: Meta = {
parameters: {
actions: {
argTypesRegex: '^on[A-Z].*', // auto-detect onClick, onChange, dll
},
},
};
// Manual action
import { action } from '@storybook/addon-actions';
export const Demo: Story = {
args: {
onClick: action('button-clicked'),
onChange: action('text-changed'),
},
};
// Viewport addon (responsive testing)
const meta = {
parameters: {
viewport: {
viewports: {
iPhone: { name: 'iPhone', styles: { width: '375px', height: '812px' } },
iPad: { name: 'iPad', styles: { width: '768px', height: '1024px' } },
desktop: { name: 'Desktop', styles: { width: '1920px', height: '1080px' } },
},
defaultViewport: 'iPhone',
},
},
};
// Measure & Outline (CSS debugging)
// Aktif otomatis dengan addon-essentials
// Tampil di toolbar addon
// Accessibility (a11y) testing
const meta = {
parameters: {
a11y: {
element: '#storybook-root',
config: {
rules: [
{ id: 'color-contrast', enabled: true },
],
},
options: {},
manual: false,
},
},
};import { expect } from '@storybook/test';
import { within, userEvent } from '@storybook/test';
export const LoginForm: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Interact dengan component
await userEvent.type(canvas.getByPlaceholderText('Email'), 'test@test.com');
await userEvent.type(canvas.getByPlaceholderText('Password'), 'password123');
await userEvent.click(canvas.getByRole('button', { name: 'Login' }));
// Assert
await expect(canvas.getByText('Welcome!')).toBeVisible();
},
};
// Async play dengan delay
export const WithDelay: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByText('Load'));
await new Promise(r => setTimeout(r, 500));
await expect(canvas.getByText('Loaded')).toBeInTheDocument();
},
};# Install
npm install @storybook/test-runner --dev
# Run tests (headless browser)
npm run test-storybook
# Dengan specific stories
npx test-storybook --url "http://localhost:6007" \
--storyFilter "Components/Button/*"
# Watch mode
npx test-storybook --watch
# Generate coverage
npx test-storybook --coverage// Import story sebagai test case
import { composeStories } from '@storybook/react';
import { render, screen } from '@testing-library/react';
import * as stories from './Button.stories';
const { Primary, Disabled } = composeStories(stories);
test('renders primary button', () => {
render(<Primary />);
expect(screen.getByText('Click Me')).toBeInTheDocument();
});
test('disabled button is disabled', () => {
render(<Disabled />);
expect(screen.getByRole('button')).toBeDisabled();
});
// Run play functions
test('runs play function', async () => {
const { Primary } = composeStories(stories);
const { container } = render(<Primary />);
await Primary.play?.({ canvasElement: container });
});// Title format dengan / untuk grouping
const meta = {
title: 'Design System/Forms/Input', // Group > Subgroup > Component
component: Input,
};
// Atau dot notation
const meta = {
title: 'Components.Input', // Flat hierarchy dengan dot separator
component: Input,
};// Group related components
export default {
title: 'Components/Form Controls',
} as Meta;
export const TextInput = {
render: () => <TextInput />,
};
export const Select = {
render: () => <Select />,
};
export const Checkbox = {
render: () => <Checkbox />,
};export default {
title: 'Pages/Login',
component: LoginPage,
parameters: {
layout: 'fullscreen',
viewport: { defaultViewport: 'desktop' },
},
} as Meta<typeof LoginPage>;
export const Default: Story = {};
export const WithError: Story = {
args: {
error: 'Invalid credentials',
},
};// .storybook/main.ts
// Combine multiple Storybooks dari berbagai repo
const config = {
refs: {
designSystem: {
title: 'Design System',
url: 'https://design-system.example.com/storybook',
},
adminPanel: {
title: 'Admin Panel',
url: 'https://admin.example.com/storybook',
},
},
};# Build static files
npm run build-storybook
# Output: ./storybook-static/
# Deploy ke Vercel
npx vercel ./storybook-static --prod
# Deploy ke Netlify
npx netlify deploy --dir=./storybook-static --prod
# Deploy ke Chromatic (visual regression)
npx chromatic --project-token=YOUR_TOKEN
# Deploy ke GitHub Pages
# .github/workflows/storybook.yml# Install
npm install chromatic --dev
# Publish storybook + run visual tests
npx chromatic
# CI integration
# GitHub Actions:
# - name: Publish to Chromatic
# run: npx chromatic --project-token=${{ secrets.CHROMATIC_TOKEN }}Login atau daftar akun gratis untuk membaca cheat sheet ini.