Ladle: Ultra-Fast Storybook Alternative for React Component Development

Ladle: Ultra-Fast Storybook Alternative for React Component Development

Ladle is a fast, lightweight alternative to Storybook for developing and documenting React components. It uses Vite instead of Webpack, which means near-instant startup and hot module replacement measured in milliseconds rather than seconds. Ladle is compatible with the CSF (Component Story Format) that Storybook uses, so migrating existing stories is often a file rename and a small import change.

Why Ladle?

Storybook is feature-rich but slow. For large codebases, Storybook startup times of 30–90 seconds are common. Ladle trades Storybook's addon ecosystem for startup times under 2 seconds and a minimal footprint.

When Ladle fits:

  • Teams that want fast iteration on React components
  • Projects already using Vite
  • Lightweight documentation needs without complex addons

When to stick with Storybook:

  • You rely on Storybook addons (Chromatic, a11y, Controls with complex type inference)
  • Your org has invested in Storybook infrastructure
  • You need non-React framework support (Ladle is React-only)

Installation

npm install --save-dev @ladle/react

Add a script to package.json:

{
  "scripts": {
    "ladle": "ladle serve",
    "ladle:build": "ladle build"
  }
}

Run npm run ladle — Ladle discovers all *.stories.tsx files in your project automatically. No configuration file required.

Writing Stories

Ladle uses CSF3 format — the same format as Storybook 6.4+:

// Button.stories.tsx
import type { Story } from '@ladle/react';
import { Button } from './Button';

export default {
  title: 'Components/Button',
};

export const Primary: Story = () => (
  <Button variant="primary" onClick={() => {}}>
    Click me
  </Button>
);

export const Disabled: Story = () => (
  <Button variant="primary" disabled>
    Disabled
  </Button>
);

export const WithIcon: Story = () => (
  <Button variant="primary" icon="arrow-right">
    Continue
  </Button>
);

Args and Controls

Use args to make stories interactive:

import type { Story } from '@ladle/react';
import { Button, ButtonProps } from './Button';

export default {
  title: 'Components/Button',
  args: {
    label: 'Click me',
    variant: 'primary',
    disabled: false,
  },
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: ['primary', 'secondary', 'danger'],
    },
  },
};

export const Default: Story<ButtonProps> = ({ label, variant, disabled }) => (
  <Button variant={variant} disabled={disabled}>
    {label}
  </Button>
);

Ladle renders a Controls panel where you can change arg values in real time.

Decorators

Decorators wrap all stories in a component — useful for theme providers, routers, or global styles:

// .ladle/components.tsx
import { GlobalProvider } from '@ladle/react';
import { ThemeProvider } from '../src/theme';

export const Provider: GlobalProvider = ({ children, globalState }) => (
  <ThemeProvider theme={globalState.theme === 'dark' ? darkTheme : lightTheme}>
    {children}
  </ThemeProvider>
);

.ladle/components.tsx is Ladle's configuration entry point. The GlobalProvider wraps every story automatically.

Story-Level Decorators

For decorator logic that applies only to specific stories:

export const WithAuthContext: Story = () => <UserProfile />;
WithAuthContext.decorators = [
  (Story) => (
    <AuthProvider user={{ name: 'Alice', role: 'admin' }}>
      <Story />
    </AuthProvider>
  ),
];

Ladle Configuration

Create .ladle/config.mjs for project-level settings:

/** @type {import('@ladle/react').UserConfig} */
export default {
  stories: 'src/**/*.stories.{tsx,jsx}',  // default pattern
  port: 61000,
  outDir: '.ladle/build',
  viteConfig: './vite.config.ts',  // reuse your existing Vite config
  base: '/components/',            // base URL for deployed storybook
};

Using with Vitest for Component Tests

Ladle stories can double as Vitest test cases:

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

test('renders button with label', () => {
  render(<Default label="Submit" variant="primary" disabled={false} />);
  expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
});

test('calls onClick when not disabled', () => {
  const onClick = vi.fn();
  render(<Default label="Submit" variant="primary" disabled={false} onClick={onClick} />);
  fireEvent.click(screen.getByRole('button'));
  expect(onClick).toHaveBeenCalledOnce();
});

Importing stories into tests gives you consistency — the same component configuration used for development is used in tests.

Building Static Documentation

npm run ladle:build

Outputs a static site to .ladle/build/ that you can deploy to any static host. The build uses Vite's production build, so assets are optimized and tree-shaken.

Snapshot Testing with Ladle

// snapshot.test.tsx
import { render } from '@testing-library/react';
import * as ButtonStories from './Button.stories';

describe('Button snapshots', () => {
  Object.entries(ButtonStories).forEach(([name, Story]) => {
    if (name === 'default') return;
    test(name, () => {
      const { container } = render(<Story />);
      expect(container).toMatchSnapshot();
    });
  });
});

This generates a snapshot for every story export automatically. Add a story, get a snapshot test.

Accessibility Checks

Ladle doesn't ship an a11y addon, but you can use axe-core directly in stories:

import { useEffect } from 'react';
import axe from 'axe-core';

export const AccessibleButton: Story = () => {
  useEffect(() => {
    axe.run('#root').then((results) => {
      if (results.violations.length > 0) {
        console.error('a11y violations:', results.violations);
      }
    });
  }, []);

  return <Button variant="primary">Accessible</Button>;
};

Or run axe in Vitest tests on story renders.

Key Points

  • Ladle starts in under 2 seconds thanks to Vite — no Webpack overhead
  • Uses CSF3 format compatible with Storybook stories (often just an import change to migrate)
  • .ladle/components.tsx with GlobalProvider wraps all stories in providers
  • Args + argTypes enable interactive Controls without addons
  • Import stories directly into Vitest tests for consistency between dev and test environments
  • ladle build produces a static site deployable to any CDN
  • Ladle is React-only; for Vue or Svelte, use Histoire or Storybook

Read more