Skip to content

Latest commit

 

History

History
682 lines (588 loc) · 13.9 KB

File metadata and controls

682 lines (588 loc) · 13.9 KB

Storybook Component Development Guide

Storybook is a powerful tool for building UI components in isolation and documenting them. This guide covers setup, configuration, and best practices for using Storybook effectively.

🚀 Getting Started

Installation

# React
npx storybook@latest init

# Vue
npx storybook@latest init --builder vue3

# Angular
npx storybook@latest init --builder angular

# Manual installation
npm install --save-dev @storybook/react @storybook/addon-essentials

Basic Setup

// .storybook/main.js
module.exports = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-a11y',
    '@storybook/addon-performance',
    '@storybook/addon-viewport',
    '@storybook/addon-docs',
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
  features: {
    buildStoriesJson: true,
  },
};

⚙️ Configuration

Advanced Configuration

// .storybook/main.js
module.exports = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-a11y',
    '@storybook/addon-performance',
    '@storybook/addon-viewport',
    '@storybook/addon-docs',
    '@storybook/addon-controls',
    '@storybook/addon-actions',
    '@storybook/addon-backgrounds',
    '@storybook/addon-measure',
    '@storybook/addon-outline',
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
  webpackFinal: async (config) => {
    // Add support for CSS modules
    config.module.rules.push({
      test: /\.module\.css$/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            modules: true,
          },
        },
      ],
    });
    
    return config;
  },
  features: {
    buildStoriesJson: true,
  },
};

TypeScript Configuration

// .storybook/main.js
module.exports = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-docs',
  ],
  framework: '@storybook/react',
  typescript: {
    check: false,
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
    },
  },
};

🎯 Writing Stories

Basic Story Structure

// Button.stories.js
import { Button } from './Button';

export default {
  title: 'Components/Button',
  component: Button,
  parameters: {
    docs: {
      description: {
        component: 'A reusable button component with multiple variants and sizes.',
      },
    },
  },
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: ['primary', 'secondary', 'danger'],
      description: 'The visual style variant of the button',
    },
    size: {
      control: { type: 'select' },
      options: ['small', 'medium', 'large'],
      description: 'The size of the button',
    },
    disabled: {
      control: { type: 'boolean' },
      description: 'Whether the button is disabled',
    },
    onClick: {
      action: 'clicked',
      description: 'Function called when button is clicked',
    },
  },
};

const Template = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  variant: 'primary',
  children: 'Button',
};

export const Secondary = Template.bind({});
Secondary.args = {
  variant: 'secondary',
  children: 'Button',
};

export const Danger = Template.bind({});
Danger.args = {
  variant: 'danger',
  children: 'Button',
};

export const Large = Template.bind({});
Large.args = {
  size: 'large',
  children: 'Large Button',
};

export const Small = Template.bind({});
Small.args = {
  size: 'small',
  children: 'Small Button',
};

Advanced Story Patterns

// Card.stories.js
import { Card } from './Card';
import { Button } from '../Button/Button';

export default {
  title: 'Components/Card',
  component: Card,
  decorators: [
    (Story) => (
      <div style={{ padding: '20px', backgroundColor: '#f5f5f5' }}>
        <Story />
      </div>
    ),
  ],
  parameters: {
    layout: 'centered',
  },
};

const Template = (args) => <Card {...args} />;

export const Default = Template.bind({});
Default.args = {
  title: 'Card Title',
  children: 'This is the card content.',
};

export const WithActions = Template.bind({});
WithActions.args = {
  title: 'Card with Actions',
  children: 'This card has action buttons.',
  actions: (
    <div style={{ display: 'flex', gap: '8px' }}>
      <Button size="small">Cancel</Button>
      <Button size="small" variant="primary">Save</Button>
    </div>
  ),
};

export const Loading = Template.bind({});
Loading.args = {
  title: 'Loading Card',
  children: 'This card is in a loading state.',
  loading: true,
};

export const Error = Template.bind({});
Error.args = {
  title: 'Error Card',
  children: 'This card shows an error state.',
  error: 'Something went wrong',
};

Interactive Stories

// Counter.stories.js
import { useState } from 'react';
import { Counter } from './Counter';

export default {
  title: 'Components/Counter',
  component: Counter,
};

const Template = (args) => {
  const [count, setCount] = useState(args.initialValue || 0);
  
  return (
    <Counter
      {...args}
      value={count}
      onChange={setCount}
    />
  );
};

export const Default = Template.bind({});
Default.args = {
  initialValue: 0,
  min: 0,
  max: 100,
};

export const WithStep = Template.bind({});
WithStep.args = {
  initialValue: 10,
  step: 5,
  min: 0,
  max: 50,
};

🔧 Addons and Plugins

Essential Addons

// .storybook/main.js
module.exports = {
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-a11y',
    '@storybook/addon-performance',
    '@storybook/addon-viewport',
    '@storybook/addon-docs',
    '@storybook/addon-controls',
    '@storybook/addon-actions',
    '@storybook/addon-backgrounds',
    '@storybook/addon-measure',
    '@storybook/addon-outline',
  ],
};

Custom Addons

// .storybook/addons/theme-addon.js
import { addons } from '@storybook/addons';
import { STORY_CHANGED } from '@storybook/core-events';

const themeAddon = {
  id: 'theme-addon',
  title: 'Theme',
  type: 'tool',
  match: ({ viewMode }) => viewMode === 'story',
  render: () => {
    const channel = addons.getChannel();
    
    const handleThemeChange = (theme) => {
      channel.emit('theme-changed', theme);
    };
    
    return {
      type: 'div',
      innerHTML: `
        <div style="padding: 10px;">
          <label>Theme:</label>
          <select onchange="window.handleThemeChange(this.value)">
            <option value="light">Light</option>
            <option value="dark">Dark</option>
          </select>
        </div>
      `,
    };
  },
};

addons.register('theme-addon', () => {
  addons.add('theme-addon', themeAddon);
});

Accessibility Testing

// .storybook/main.js
module.exports = {
  addons: [
    '@storybook/addon-a11y',
  ],
};

// Button.stories.js
export default {
  title: 'Components/Button',
  component: Button,
  parameters: {
    a11y: {
      element: '#root',
      config: {
        rules: [
          {
            id: 'color-contrast',
            enabled: true,
          },
        ],
      },
    },
  },
};

📊 Documentation

MDX Documentation

<!-- Button.mdx -->
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { Button } from './Button';

<Meta title="Components/Button" component={Button} />

# Button

A reusable button component with multiple variants and sizes.

## Usage

```jsx
import { Button } from './Button';

<Button variant="primary" size="large">
  Click me
</Button>

Props

Examples

{args => Primary Button} {args => Secondary Button}

Design Guidelines

  • Use primary buttons for the main action on a page
  • Use secondary buttons for secondary actions
  • Use danger buttons for destructive actions
  • Ensure buttons have sufficient contrast for accessibility

### Component Documentation
```javascript
// Button.stories.js
export default {
  title: 'Components/Button',
  component: Button,
  parameters: {
    docs: {
      description: {
        component: `
          A reusable button component with multiple variants and sizes.
          
          ## Usage
          
          \`\`\`jsx
          import { Button } from './Button';
          
          <Button variant="primary" size="large">
            Click me
          </Button>
          \`\`\`
          
          ## Design Guidelines
          
          - Use primary buttons for the main action on a page
          - Use secondary buttons for secondary actions
          - Use danger buttons for destructive actions
          - Ensure buttons have sufficient contrast for accessibility
        `,
      },
    },
  },
};

🎯 Testing Integration

Visual Testing

// .storybook/main.js
module.exports = {
  addons: [
    '@storybook/addon-storyshots',
    '@storybook/addon-storyshots-puppeteer',
  ],
};

// Button.stories.js
export default {
  title: 'Components/Button',
  component: Button,
  parameters: {
    storyshots: {
      disable: false,
    },
  },
};

Unit Testing

// Button.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('renders with correct text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);
    
    fireEvent.click(screen.getByText('Click me'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when disabled prop is true', () => {
    render(<Button disabled>Click me</Button>);
    expect(screen.getByText('Click me')).toBeDisabled();
  });
});

Accessibility Testing

// Button.stories.js
export default {
  title: 'Components/Button',
  component: Button,
  parameters: {
    a11y: {
      config: {
        rules: [
          {
            id: 'color-contrast',
            enabled: true,
          },
          {
            id: 'button-name',
            enabled: true,
          },
        ],
      },
    },
  },
};

🔧 Advanced Features

Design Tokens

// .storybook/design-tokens.js
export const tokens = {
  colors: {
    primary: '#007bff',
    secondary: '#6c757d',
    success: '#28a745',
    danger: '#dc3545',
  },
  spacing: {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
    xl: '32px',
  },
  typography: {
    fontFamily: 'Inter, sans-serif',
    fontSize: {
      sm: '14px',
      md: '16px',
      lg: '18px',
      xl: '24px',
    },
  },
};

// Button.stories.js
import { tokens } from '../.storybook/design-tokens';

export default {
  title: 'Components/Button',
  component: Button,
  parameters: {
    docs: {
      description: {
        component: `
          Button component using design tokens:
          - Primary color: ${tokens.colors.primary}
          - Medium spacing: ${tokens.spacing.md}
          - Medium font size: ${tokens.typography.fontSize.md}
        `,
      },
    },
  },
};

Theme Support

// .storybook/theme.js
import { createTheme } from '@mui/material/styles';

export const lightTheme = createTheme({
  palette: {
    mode: 'light',
    primary: {
      main: '#007bff',
    },
  },
});

export const darkTheme = createTheme({
  palette: {
    mode: 'dark',
    primary: {
      main: '#90caf9',
    },
  },
});

// .storybook/preview.js
import { lightTheme, darkTheme } from './theme';

export const parameters = {
  backgrounds: {
    default: 'light',
    values: [
      { name: 'light', value: lightTheme.palette.background.default },
      { name: 'dark', value: darkTheme.palette.background.default },
    ],
  },
};

🚨 Common Issues & Solutions

1. Story Not Rendering

// ❌ Problem: Story not rendering
export const MyStory = () => <Button>Click me</Button>;

// ✅ Solution: Use Template pattern
const Template = (args) => <Button {...args} />;

export const MyStory = Template.bind({});
MyStory.args = {
  children: 'Click me',
};

2. Controls Not Working

// ❌ Problem: Controls not showing
export default {
  title: 'Components/Button',
  component: Button,
  // Missing argTypes
};

// ✅ Solution: Add argTypes
export default {
  title: 'Components/Button',
  component: Button,
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: ['primary', 'secondary', 'danger'],
    },
    size: {
      control: { type: 'select' },
      options: ['small', 'medium', 'large'],
    },
  },
};

3. TypeScript Issues

// ❌ Problem: TypeScript errors in stories
export const MyStory = (args: ButtonProps) => <Button {...args} />;

// ✅ Solution: Use proper typing
import { ComponentStory, ComponentMeta } from '@storybook/react';

const meta: ComponentMeta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
};

export default meta;

const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;

export const MyStory = Template.bind({});
MyStory.args = {
  children: 'Click me',
};

📚 Additional Resources