🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Exemples Storybook
Exemples complets de Storybook pour le développement de composants, le testing, la documentation et la gestion des systèmes de design
💻 Basics de Storybook javascript
🟢 simple
⭐⭐
Configuration de base de Storybook, écriture d'histoires et patterns de documentation de composants
⏱️ 30 min
🏷️ storybook, components, documentation
Prerequisites:
React, JavaScript/TypeScript, CSS, Component concepts
// Storybook Basics
// File: .storybook/main.js - Storybook Configuration
module.exports = {
stories: [
'../stories/**/*.stories.mdx',
'../stories/**/*.stories.@(js|jsx|ts|tsx|mdx)',
'../src/**/*.stories.@(js|jsx|ts|tsx|mdx)',
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-docs',
'@storybook/addon-controls',
'@storybook/addon-backgrounds',
'@storybook/addon-viewport',
'@storybook/addon-toolbars',
'@storybook/addon-design-tokens',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
features: {
buildStoriesJson: true,
storyStoreV7: true,
},
typescript: {
check: false,
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
}
// File: .storybook/preview.js - Global Configuration
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
docs: {
toc: true,
},
backgrounds: {
default: 'light',
values: [
{
name: 'light',
value: '#ffffff',
},
{
name: 'dark',
value: '#333333',
},
{
name: 'gray',
value: '#f5f5f5',
},
],
},
viewport: {
viewports: {
mobile: {
name: 'Mobile',
styles: {
width: '375px',
height: '667px',
},
},
tablet: {
name: 'Tablet',
styles: {
width: '768px',
height: '1024px',
},
},
desktop: {
name: 'Desktop',
styles: {
width: '1024px',
height: '768px',
},
},
wide: {
name: 'Wide',
styles: {
width: '1440px',
height: '900px',
},
},
},
defaultViewport: 'desktop',
},
}
// File: .storybook/manager.js - Manager Configuration
import { addons } from '@storybook/addons'
addons.setConfig({
theme: {
brandTitle: 'My Component Library',
brandUrl: 'https://example.com',
brandImage: 'https://example.com/logo.png',
},
panelPosition: 'right',
sidebar: {
showRoots: true,
collapsedRoots: ['examples'],
},
})
// File: src/components/Button/Button.stories.js - Basic Component Stories
import React from 'react'
import { Button } from './Button'
// Default metadata
export default {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'A versatile button component with multiple variants and sizes.',
},
},
},
argTypes: {
variant: {
control: {
type: 'select',
},
options: ['primary', 'secondary', 'danger', 'ghost'],
description: 'Button style variant',
},
size: {
control: {
type: 'radio',
},
options: ['small', 'medium', 'large'],
description: 'Button size',
},
disabled: {
control: 'boolean',
description: 'Whether the button is disabled',
},
loading: {
control: 'boolean',
description: 'Show loading state',
},
onClick: {
action: 'clicked',
description: 'Click event handler',
},
children: {
control: {
type: 'text',
},
description: 'Button content',
},
},
}
// Template for creating stories
const Template = (args) => <Button {...args} />
// Default story
export const Default = Template.bind({})
Default.args = {
children: 'Click me',
}
// Different variants
export const Primary = Template.bind({})
Primary.args = {
children: 'Primary Button',
variant: 'primary',
}
export const Secondary = Template.bind({})
Secondary.args = {
children: 'Secondary Button',
variant: 'secondary',
}
export const Danger = Template.bind({})
Danger.args = {
children: 'Danger Button',
variant: 'danger',
}
export const Ghost = Template.bind({})
Ghost.args = {
children: 'Ghost Button',
variant: 'ghost',
}
// Different sizes
export const Small = Template.bind({})
Small.args = {
children: 'Small Button',
size: 'small',
variant: 'primary',
}
export const Medium = Template.bind({})
Medium.args = {
children: 'Medium Button',
size: 'medium',
variant: 'primary',
}
export const Large = Template.bind({})
Large.args = {
children: 'Large Button',
size: 'large',
variant: 'primary',
}
// States
export const Disabled = Template.bind({})
Disabled.args = {
children: 'Disabled Button',
disabled: true,
}
export const Loading = Template.bind({})
Loading.args = {
children: 'Loading...',
loading: true,
}
// Complex examples
export const WithIcon = Template.bind({})
WithIcon.args = {
children: (
<>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7V12C2 16.55 4.84 20.74 9 22C13.16 20.74 16 16.55 16 12V7L12 2Z" fill="currentColor"/>
</svg>
<span style={{ marginLeft: '8px' }}>Secure Login</span>
</>
),
variant: 'primary',
}
// Interactive story
export const Interactive = () => {
const [count, setCount] = React.useState(0)
return (
<div>
<Button onClick={() => setCount(count + 1)}>
Clicked {count} times
</Button>
</div>
)
}
// File: src/components/Card/Card.stories.js - Complex Component Stories
import React from 'react'
import { Card } from './Card'
export default {
title: 'Components/Card',
component: Card,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'A flexible card component for displaying content in a contained format.',
},
},
},
argTypes: {
title: {
control: 'text',
description: 'Card title',
},
subtitle: {
control: 'text',
description: 'Card subtitle',
},
image: {
control: 'text',
description: 'Card image URL',
},
footer: {
control: 'text',
description: 'Card footer content',
},
interactive: {
control: 'boolean',
description: 'Whether the card is interactive (hoverable)',
},
},
}
const Template = (args) => <Card {...args} />
export const Default = Template.bind({})
Default.args = {
title: 'Card Title',
children: 'This is the card content. You can put any content here.',
}
export const WithSubtitle = Template.bind({})
WithSubtitle.args = {
title: 'Card with Subtitle',
subtitle: 'This is a subtitle',
children: 'Card content with subtitle above.',
}
export const WithImage = Template.bind({})
WithImage.args = {
title: 'Card with Image',
image: 'https://picsum.photos/400/200',
children: 'This card has an image at the top.',
}
export const WithFooter = Template.bind({})
WithFooter.args = {
title: 'Card with Footer',
footer: (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>Footer content</span>
<Button>Action</Button>
</div>
),
children: 'This card has a footer section.',
}
export const Interactive = Template.bind({})
Interactive.args = {
title: 'Interactive Card',
interactive: true,
children: 'This card is interactive and responds to hover.',
}
// File: src/stories/Introduction.stories.mdx - Documentation Stories
import { Meta, Story, Canvas, ArgsTable } from '@storybook/blocks'
import { Button, Card, Input } from '../src/components'
<Meta title="Documentation/Introduction" />
# Welcome to Our Component Library
This is the documentation for our React component library built with Storybook.
## Getting Started
Our components are designed to be:
- **Accessible**: Following WCAG guidelines
- **Responsive**: Working on all screen sizes
- **Customizable**: With consistent design tokens
- **Well-tested**: With comprehensive test coverage
## Basic Usage
<Canvas>
<Story name="ButtonExample">
<Button variant="primary">Hello World</Button>
</Story>
</Canvas>
## Component Examples
### Button Component
The Button component is versatile and supports multiple variants:
<Canvas>
<Story name="ButtonVariants">
<div style={{ display: 'flex', gap: '1rem' }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
<Button variant="ghost">Ghost</Button>
</div>
</Story>
</Canvas>
<ArgsTable of={Button} />
### Card Component
Cards are perfect for displaying grouped information:
<Canvas>
<Story name="CardExample">
<Card
title="Sample Card"
image="https://picsum.photos/400/200"
footer={<Button variant="primary">Learn More</Button>}
>
This is an example card with an image and footer action.
</Card>
</Story>
</Canvas>
## Design Tokens
Our components use a consistent design token system:
```javascript
// Primary colors
colors: {
primary: '#0070f3',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
warning: '#ffc107',
}
// Spacing
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
}
```
// File: src/components/ThemeProvider/ThemeProvider.stories.js - Theme Provider
import React from 'react'
import { ThemeProvider } from './ThemeProvider'
import { Button, Card, Input } from '../index'
export default {
title: 'Design System/ThemeProvider',
component: ThemeProvider,
parameters: {
docs: {
description: {
component: 'Theme provider for managing global styles and design tokens.',
},
},
},
}
const theme = {
colors: {
primary: '#0070f3',
secondary: '#6c757d',
background: '#ffffff',
text: '#333333',
},
spacing: {
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
},
}
export const Default = () => (
<ThemeProvider theme={theme}>
<div style={{ padding: '2rem' }}>
<h1>Themed Components</h1>
<div style={{ marginBottom: '1rem' }}>
<Button variant="primary">Themed Button</Button>
</div>
<Card title="Themed Card">
This card uses the theme provider for consistent styling.
</Card>
</div>
</ThemeProvider>
)
export const DarkTheme = () => {
const darkTheme = {
...theme,
colors: {
...theme.colors,
background: '#1a1a1a',
text: '#ffffff',
},
}
return (
<ThemeProvider theme={darkTheme}>
<div style={{ padding: '2rem', backgroundColor: '#1a1a1a', minHeight: '100vh' }}>
<h1 style={{ color: '#ffffff' }}>Dark Theme</h1>
<div style={{ marginBottom: '1rem' }}>
<Button variant="primary">Dark Button</Button>
</div>
<Card title="Dark Card">
This card uses the dark theme.
</Card>
</div>
</ThemeProvider>
)
}
💻 Patterns Avancés de Storybook javascript
🔴 complex
⭐⭐⭐⭐
Techniques avancées de Storybook incluant les interactions, le testing, l'automatisation et l'intégration avec les outils de design
⏱️ 45 min
🏷️ storybook, advanced, automation, testing
Prerequisites:
Storybook basics, React hooks, Testing frameworks, CSS-in-JS
// Advanced Storybook Patterns
// File: .storybook/main.js - Advanced Configuration
module.exports = {
stories: [
'../stories/**/*.stories.@(js|jsx|ts|tsx|mdx)',
'../src/**/*.stories.@(js|jsx|ts|tsx|mdx)',
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-docs',
'@storybook/addon-controls',
'@storybook/addon-backgrounds',
'@storybook/addon-viewport',
'@storybook/addon-toolbars',
'@storybook/addon-design-tokens',
'@storybook/addon-storysource',
'@storybook/preset-create-react-app',
'storybook-addon-designs',
'storybook-addon-measure',
'storybook-addon-performance',
'@storybook/addon-jest',
'@storybook/testing-react',
],
framework: '@storybook/react-vite',
core: {
disableTelemetry: true,
enableCrashReports: false,
},
env: (config) => ({
...config,
STORYBOOK_ENVIRONMENT: process.env.NODE_ENV || 'development',
}),
webpackFinal: async (config) => {
// Custom webpack configuration
config.resolve.alias = {
...config.resolve.alias,
'@': require('path').resolve(__dirname, '../src'),
}
return config
},
viteFinal: async (config) => {
// Custom Vite configuration
config.optimizeDeps = {
...config.optimizeDeps,
include: ['react', 'react-dom'],
}
return config
},
}
// File: .storybook/preview-head.html - Custom Head HTML
<style>
/* Global styles for Storybook */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 0;
}
/* Custom CSS variables for theming */
:root {
--color-primary: #0070f3;
--color-secondary: #6c757d;
--color-success: #28a745;
--color-danger: #dc3545;
--color-warning: #ffc107;
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
}
/* Loading animation */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading {
animation: spin 1s linear infinite;
}
</style>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
// File: src/components/Dropdown/Dropdown.stories.js - Interactive Components
import React, { useState } from 'react'
import { Dropdown, DropdownItem } from './Dropdown'
import { userEvent, within, waitFor } from '@storybook/testing-library'
export default {
title: 'Components/Dropdown',
component: Dropdown,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Interactive dropdown component with keyboard navigation and accessibility features.',
},
},
},
argTypes: {
trigger: {
control: 'text',
description: 'Trigger button text',
},
placement: {
control: {
type: 'select',
},
options: ['bottom-start', 'bottom-end', 'top-start', 'top-end'],
description: 'Dropdown placement',
},
disabled: {
control: 'boolean',
description: 'Disable the dropdown',
},
},
}
const Template = (args) => {
const [isOpen, setIsOpen] = useState(false)
return (
<Dropdown
{...args}
isOpen={isOpen}
onToggle={setIsOpen}
>
<DropdownItem onClick={() => console.log('Action 1')}>
Action 1
</DropdownItem>
<DropdownItem onClick={() => console.log('Action 2')}>
Action 2
</DropdownItem>
<DropdownItem disabled>
Disabled Item
</DropdownItem>
<DropdownItem danger onClick={() => console.log('Delete')}>
Delete
</DropdownItem>
</Dropdown>
)
}
export const Default = Template.bind({})
Default.args = {
trigger: 'Click me',
}
export const Disabled = Template.bind({})
Disabled.args = {
trigger: 'Disabled Dropdown',
disabled: true,
}
// Interactive play function for testing
Default.play = async ({ canvasElement }) => {
const canvas = within(canvasElement)
// Find and click the trigger
const trigger = canvas.getByRole('button', { name: /click me/i })
await userEvent.click(trigger)
// Wait for dropdown to appear
await waitFor(() => {
expect(canvas.getByRole('menu')).toBeInTheDocument()
})
// Verify menu items
expect(canvas.getByRole('menuitem', { name: /action 1/i })).toBeInTheDocument()
expect(canvas.getByRole('menuitem', { name: /action 2/i })).toBeInTheDocument()
expect(canvas.getByRole('menuitem', { name: /delete/i })).toBeInTheDocument()
}
// File: src/components/Form/Form.stories.js - Complex Form Stories
import React, { useState } from 'react'
import { Form, FormField, FormSubmit } from './Form'
export default {
title: 'Components/Form',
component: Form,
parameters: {
docs: {
description: {
component: 'Advanced form component with validation, field management, and submission handling.',
},
},
},
}
const FormTemplate = (args) => {
const [formData, setFormData] = useState({
email: '',
password: '',
remember: false,
})
const handleSubmit = (data) => {
console.log('Form submitted:', data)
alert('Form submitted successfully!')
}
return (
<Form {...args} onSubmit={handleSubmit} initialData={formData}>
<FormField
name="email"
label="Email Address"
type="email"
required
placeholder="Enter your email"
/>
<FormField
name="password"
label="Password"
type="password"
required
placeholder="Enter your password"
/>
<FormField
name="remember"
type="checkbox"
label="Remember me"
/>
<FormSubmit>Sign In</FormSubmit>
</Form>
)
}
export const Default = FormTemplate.bind({})
Default.args = {
title: 'Sign In',
}
export const WithValidation = FormTemplate.bind({})
WithValidation.args = {
title: 'Sign In',
validation: {
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
},
password: {
required: true,
minLength: 8,
},
},
}
export const MultiStep = () => {
const [currentStep, setCurrentStep] = useState(0)
const [formData, setFormData] = useState({})
const steps = [
{
title: 'Personal Information',
fields: [
{ name: 'firstName', label: 'First Name', required: true },
{ name: 'lastName', label: 'Last Name', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true },
],
},
{
title: 'Address',
fields: [
{ name: 'street', label: 'Street Address', required: true },
{ name: 'city', label: 'City', required: true },
{ name: 'zipCode', label: 'ZIP Code', required: true },
],
},
{
title: 'Preferences',
fields: [
{ name: 'newsletter', type: 'checkbox', label: 'Subscribe to newsletter' },
{ name: 'notifications', type: 'checkbox', label: 'Enable notifications' },
],
},
]
const currentStepData = steps[currentStep]
return (
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>{currentStepData.title}</h2>
<Form
onSubmit={(data) => {
const updatedData = { ...formData, ...data }
if (currentStep < steps.length - 1) {
setFormData(updatedData)
setCurrentStep(currentStep + 1)
} else {
console.log('Form completed:', updatedData)
alert('Form completed successfully!')
}
}}
>
{currentStepData.fields.map((field) => (
<FormField
key={field.name}
name={field.name}
label={field.label}
type={field.type || 'text'}
required={field.required}
/>
))}
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
<button
type="button"
onClick={() => setCurrentStep(Math.max(0, currentStep - 1))}
disabled={currentStep === 0}
>
Previous
</button>
<button type="submit">
{currentStep === steps.length - 1 ? 'Submit' : 'Next'}
</button>
</div>
</Form>
</div>
)
}
// File: .storybook/test-runner.js - Automated Testing
import { test, expect } from '@storybook/test-runner'
import { waitForElementToBeRemoved } from '@testing-library/dom'
test('renders button in default state', async ({ page }) => {
await page.goto('/iframe.html?id=components-button--default')
await expect(page.locator('button')).toContainText('Click me')
})
test('button click event works', async ({ page }) => {
await page.goto('/iframe.html?id=components-button--default&args=onClick:onClick')
await page.click('button')
await expect(page.locator('.action-log')).toContainText('clicked')
})
test('form validation works', async ({ page }) => {
await page.goto('/iframe.html?id=components-form--with-validation')
// Try to submit empty form
await page.click('button[type="submit"]')
// Check for error messages
await expect(page.locator('text=Email is required')).toBeVisible()
await expect(page.locator('text=Password is required')).toBeVisible()
// Fill in form correctly
await page.fill('input[name="email"]', '[email protected]')
await page.fill('input[name="password"]', 'password123')
// Submit should succeed
await page.click('button[type="submit"]')
await waitForElementToBeRemoved(() => page.locator('text=Email is required'))
})
// File: src/stories/playground.playground.tsx - Interactive Playground
import React, { useState } from 'react'
import { Playground } from '@storybook/addon-docs'
import { Button, Input, Card } from '../src/components'
<Playground
kind="Components/Interactive"
story="playground"
>
{{
component: () => {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
return (
<div style={{ padding: '2rem', maxWidth: '400px' }}>
<Card title="Interactive Playground">
<div style={{ marginBottom: '1rem' }}>
<Input
placeholder="Type something..."
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
<div style={{ marginBottom: '1rem' }}>
<p>You typed: {text}</p>
</div>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<Button onClick={() => setCount(count + 1)}>
Count: {count}
</Button>
<Button variant="secondary" onClick={() => setCount(0)}>
Reset
</Button>
</div>
</Card>
</div>
)
}
}}
</Playground>
// File: src/hooks/useLocalStorage/useLocalStorage.stories.js - Hook Stories
import React from 'react'
import { useLocalStorage } from './useLocalStorage'
import { Meta, Story, Canvas, ArgsTable } from '@storybook/blocks'
export default {
title: 'Hooks/useLocalStorage',
component: useLocalStorage,
parameters: {
docs: {
description: {
component: 'Custom hook for persisting state in localStorage with SSR support.',
},
},
},
}
export const Default = () => {
const [value, setValue] = useLocalStorage('my-key', 'default-value')
return (
<div>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
style={{ padding: '0.5rem', marginRight: '0.5rem' }}
/>
<button onClick={() => setValue('')}>
Clear
</button>
<p>Current value: {value}</p>
</div>
)
}
export const WithObject = () => {
const [user, setUser] = useLocalStorage('user', {
name: '',
email: '',
preferences: { theme: 'light' },
})
return (
<div>
<div style={{ marginBottom: '1rem' }}>
<input
type="text"
placeholder="Name"
value={user.name}
onChange={(e) => setUser({ ...user, name: e.target.value })}
style={{ display: 'block', marginBottom: '0.5rem', padding: '0.25rem' }}
/>
<input
type="email"
placeholder="Email"
value={user.email}
onChange={(e) => setUser({ ...user, email: e.target.value })}
style={{ display: 'block', marginBottom: '0.5rem', padding: '0.25rem' }}
/>
<select
value={user.preferences.theme}
onChange={(e) => setUser({
...user,
preferences: { ...user.preferences, theme: e.target.value }
})}
style={{ display: 'block', padding: '0.25rem' }}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<pre style={{ background: '#f5f5f5', padding: '1rem', borderRadius: '4px' }}>
{JSON.stringify(user, null, 2)}
</pre>
</div>
)
}
// File: .storybook/design-tokens.js - Design Token Integration
import { addDecorator } from '@storybook/preview'
// Design tokens for consistent styling
const designTokens = {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
},
},
spacing: {
1: '0.25rem',
2: '0.5rem',
3: '0.75rem',
4: '1rem',
5: '1.25rem',
6: '1.5rem',
8: '2rem',
12: '3rem',
16: '4rem',
},
typography: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['Fira Code', 'Monaco', 'monospace'],
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
},
},
}
// Apply design tokens globally
addDecorator((Story, context) => {
return (
<div
style={{
'--color-primary-50': designTokens.colors.primary[50],
'--color-primary-500': designTokens.colors.primary[500],
'--color-primary-600': designTokens.colors.primary[600],
'--color-gray-100': designTokens.colors.gray[100],
'--color-gray-600': designTokens.colors.gray[600],
'--spacing-4': designTokens.spacing[4],
'--spacing-6': designTokens.spacing[6],
'--font-sans': designTokens.typography.fontFamily.sans.join(', '),
'--font-base': designTokens.typography.fontSize.base,
}}
>
<Story {...context} />
</div>
)
})
export default designTokens