Skip to main content

Writing React Stories: Components & Controls

A Storybook story is a representation of a component in a particular state or configuration. Stories are written in TypeScript or JavaScript files alongside your components and use a standard export pattern: a Meta object that describes the component and a set of Story components that show different use cases. The Meta object is Storybook's core metadata layer—it holds JSDoc annotations, parameter overrides, and arg definitions that power the interactive controls panel. This pattern, adopted in Storybook 6.0+, makes stories self-documenting and enables automatic documentation generation and control inference.

How Do You Write a Basic Story in Storybook?

A story file uses two key exports: Meta (component metadata) and one or more Story functions. Here's a simple Button component with two stories:

import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta = {
component: Button,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {
label: 'Click me',
variant: 'primary',
},
};

export const Secondary: Story = {
args: {
label: 'Secondary button',
variant: 'secondary',
},
};

Let's break down this pattern:

  • Meta (exported as default) tells Storybook:

    • component: which React component this story file documents.
    • tags: ['autodocs']: auto-generate a docs page from JSDoc and controls.
    • parameters: global story settings like layout, viewport, or background.
  • Story (named exports) are individual use cases. Each story is an object with:

    • args: props passed to the component (controls infer inputs from arg types).
    • parameters: story-specific settings that override Meta parameters.
    • decorators: wrapper components (e.g., theme providers) scoped to this story.

This pattern is type-safe—TypeScript infers args from your component's prop types, so passing invalid args triggers a compile error.

What Are Args and How Do You Use Them?

Args are the primary way to pass props to your component in a story. Storybook's Controls add-on automatically generates interactive UI for each arg, matching the arg's type. If your component accepts a label: string, Storybook creates a text input; if it accepts size: 'small' | 'medium' | 'large', it creates a dropdown.

Here's a more complex Button example with various arg types:

import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

interface ButtonProps {
label: string;
variant: 'primary' | 'secondary' | 'danger';
size: 'small' | 'medium' | 'large';
disabled: boolean;
onClick?: () => void;
}

const meta = {
component: Button,
title: 'Components/Button',
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
description: 'Button color scheme',
},
size: {
control: 'radio',
options: ['small', 'medium', 'large'],
},
disabled: {
control: 'boolean',
},
onClick: {
action: 'clicked',
},
},
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {
label: 'Primary button',
variant: 'primary',
size: 'medium',
disabled: false,
},
};

export const Danger: Story = {
args: {
label: 'Delete',
variant: 'danger',
size: 'medium',
disabled: false,
},
};

export const Disabled: Story = {
args: {
label: 'Disabled',
variant: 'secondary',
size: 'medium',
disabled: true,
},
};

In this example:

  • argTypes explicitly defines control types and metadata for each arg (e.g., select dropdown, radio, boolean toggle).
  • action: 'clicked' tells Storybook to log onClick calls to the Actions tab, so you can verify callbacks fire.
  • Each Story defines default args, which users can override via controls in the right panel.

Organizing Stories with Titles and Tags

Use the title field in Meta to organize stories hierarchically in the Storybook sidebar:

const meta = {
component: Button,
title: 'Components/Forms/Button', // Creates nested "Components > Forms > Button"
tags: ['autodocs'],
} satisfies Meta<typeof Button>;

The slash-separated title creates a folder structure in the sidebar. Adding tags: ['autodocs'] tells Storybook to auto-generate a documentation page from your component's TypeScript JSDoc and controls.

For discoverability, tag related stories with custom tags:

const meta = {
component: Button,
tags: ['autodocs', 'interactive', 'forms'],
} satisfies Meta<typeof Button>;

How Do You Document Component Props with JSDoc?

When your Button component exports JSDoc, Storybook's autodocs feature pulls those comments into the docs page automatically. Write JSDoc above your component definition:

/**
* A reusable button component supporting multiple variants and sizes.
* Use `variant="primary"` for call-to-action buttons; `variant="secondary"` for less prominent actions.
*
* @example
* <Button variant="primary" label="Click me" onClick={() => alert('clicked')} />
*/
export const Button: React.FC<ButtonProps> = ({ label, variant, size, disabled, onClick }) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{label}
</button>
);
};

/**
* Defines the visual appearance of the button.
* - `primary`: blue, filled button for main actions.
* - `secondary`: gray, outlined button for secondary actions.
* - `danger`: red, filled button for destructive actions.
*/
interface ButtonProps {
/** The text displayed on the button. */
label: string;
/** The visual variant applied to the button. */
variant: 'primary' | 'secondary' | 'danger';
/** The size of the button. */
size: 'small' | 'medium' | 'large';
/** Whether the button is disabled. */
disabled?: boolean;
/** Callback fired when the button is clicked. */
onClick?: () => void;
}

With tags: ['autodocs'], Storybook reads this JSDoc and generates a "Docs" tab showing component props, descriptions, default values, and all stories as live examples.

Creating Template-Based Stories for Variants

For components with many prop combinations, use a template pattern to avoid repetition:

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

export const Primary = Template.bind({});
Primary.args = {
label: 'Primary',
variant: 'primary',
size: 'medium',
};

export const Small = Template.bind({});
Small.args = {
label: 'Small button',
size: 'small',
variant: 'secondary',
};

export const Large = Template.bind({});
Large.args = {
label: 'Large button',
size: 'large',
variant: 'primary',
};

The .bind({}) pattern copies the template function and allows you to set story-specific args. This reduces boilerplate when you have 5+ stories for a single component.

Key Takeaways

  • Meta + Story exports: Meta holds component metadata and global args; Story exports are individual use cases with args overrides.
  • Args power controls: Storybook infers control types from arg values and TypeScript types; explicitly define argTypes for custom control UIs.
  • Hierarchical organization: Use slash-separated title to create sidebar folders; use tags for discoverability and filtering.
  • JSDoc + autodocs: Write JSDoc above your component; enable tags: ['autodocs'] to auto-generate a docs page with prop descriptions.
  • Template pattern reduces code: For components with multiple variants, use a template function and .bind({}) to reuse logic across stories.

Frequently Asked Questions

How do I add a decorator (like a theme provider) to all stories?

Define decorators in .storybook/preview.ts for global application or in Meta's decorators array for story-file scope. For example: decorators: [(Story) => <ThemeProvider theme={theme}><Story /></ThemeProvider>]. Global decorators wrap all stories; file-level decorators apply to stories in that file only.

Can I preview complex components that require external data or API calls?

Yes. Use the loaders parameter to fetch data before rendering. In your story: loaders: [async () => ({ users: await fetch('/api/users').then(r => r.json()) })]. Storybook passes the loader result to your story as the first argument.

How do I version a story file for backward compatibility?

Storybook stories are not versioned directly. If your component API changes, create a new story file (e.g., Button.v2.stories.tsx) and organize both with title hierarchies (e.g., Components/Button/v1 and Components/Button/v2). Document breaking changes in your component's JSDoc and CHANGELOG.

What's the difference between args and argTypes?

args are the actual prop values passed to the component in a story instance. argTypes define metadata about each arg: its control type (select, radio, text), description, and default value. You can infer argTypes from TypeScript types alone, but explicitly defining them lets you customize controls and descriptions.

Further Reading