🎯 empfohlene Sammlungen
Balanced sample collections from various categories for you to explore
Storybook Beispiele
Umfassende Storybook Beispiele für Komponentenentwicklung, Testing, Dokumentation und Design-System-Management
💻 Storybook Grundlagen javascript
🟢 simple
⭐⭐
Grundlegende Storybook-Setup, Story-Schreibung und Komponenten-Dokumentations-Patterns
⏱️ 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>
)
}
💻 Fortgeschrittene Storybook Patterns javascript
🔴 complex
⭐⭐⭐⭐
Fortgeschrittene Storybook-Techniken inklusive Interaktionen, Testing, Automatisierung und Design-Tool-Integration
⏱️ 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