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/reactAdd 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:buildOutputs 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.tsxwithGlobalProviderwraps 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 buildproduces a static site deployable to any CDN- Ladle is React-only; for Vue or Svelte, use Histoire or Storybook