Storybook's core preview API providing hooks, decorators, story composition utilities, and simulation tools for building UI components in isolation
—
System for creating reusable story enhancements and middleware. Decorators wrap stories to provide additional functionality like theming, layouts, data providers, or interactive controls.
Primary function for creating Storybook decorators with advanced configuration options.
/**
* Creates a Storybook decorator with configurable behavior
* @param options - Configuration for the decorator
* @returns Decorator function and configuration utilities
*/
function makeDecorator(options: MakeDecoratorOptions): MakeDecoratorResult;
interface MakeDecoratorOptions {
/** Name of the decorator for identification */
name: string;
/** Parameter key for configuration in story parameters */
parameterName: string;
/** Skip execution if no parameters or options are provided */
skipIfNoParametersOrOptions?: boolean;
/** Core wrapper function that enhances the story */
wrapper: (
storyFn: StoryFn,
context: StoryContext,
settings: { parameters?: any; options?: any }
) => any;
}
interface MakeDecoratorResult {
/** The decorator function to be used in stories */
decorator: DecoratorFunction;
/** Configure the decorator with specific options */
configure: (options: any) => DecoratorFunction;
}
interface StoryFn<TRenderer = any> {
(context: StoryContext<TRenderer>): any;
}
interface DecoratorFunction<TRenderer = any> {
(story: StoryFn<TRenderer>, context: StoryContext<TRenderer>): any;
decoratorName?: string;
}Usage Examples:
import { makeDecorator } from "@storybook/preview-api";
// Create a theme decorator
export const withTheme = makeDecorator({
name: 'withTheme',
parameterName: 'theme',
wrapper: (storyFn, context, { parameters }) => {
const theme = parameters?.theme || 'light';
return (
<div className={`theme-${theme}`} data-theme={theme}>
{storyFn(context)}
</div>
);
}
});
// Create a data provider decorator
export const withData = makeDecorator({
name: 'withData',
parameterName: 'mockData',
skipIfNoParametersOrOptions: true,
wrapper: (storyFn, context, { parameters }) => {
const mockData = parameters?.mockData || {};
return (
<DataProvider data={mockData}>
{storyFn(context)}
</DataProvider>
);
}
});
// Configure decorator with specific options
const withCustomTheme = withTheme.configure({
themes: ['light', 'dark', 'high-contrast']
});Complex decorator implementations using hooks and context management.
// Decorator with state management
export const withCounter = makeDecorator({
name: 'withCounter',
parameterName: 'counter',
wrapper: (storyFn, context, { parameters }) => {
const [count, setCount] = useState(parameters?.initialCount || 0);
const contextWithCounter = {
...context,
args: {
...context.args,
count,
increment: () => setCount(c => c + 1),
decrement: () => setCount(c => c - 1),
reset: () => setCount(parameters?.initialCount || 0)
}
};
return (
<div>
<div>Count: {count}</div>
<button onClick={() => setCount(c => c + 1)}>+</button>
<button onClick={() => setCount(c => c - 1)}>-</button>
{storyFn(contextWithCounter)}
</div>
);
}
});
// Decorator with async data loading
export const withAsyncData = makeDecorator({
name: 'withAsyncData',
parameterName: 'asyncData',
wrapper: (storyFn, context, { parameters }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadData = async () => {
setLoading(true);
try {
const result = await parameters?.dataLoader?.();
setData(result);
} catch (error) {
console.error('Failed to load data:', error);
} finally {
setLoading(false);
}
};
if (parameters?.dataLoader) {
loadData();
} else {
setLoading(false);
}
}, [parameters?.dataLoader]);
if (loading) {
return <div>Loading...</div>;
}
const contextWithData = {
...context,
args: { ...context.args, data }
};
return storyFn(contextWithData);
}
});How to apply decorators to individual stories and components.
import type { Meta, StoryObj } from '@storybook/react';
import { withTheme, withData } from './decorators';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Example/Button',
component: Button,
decorators: [withTheme, withData],
parameters: {
theme: 'dark',
mockData: {
user: { name: 'John', role: 'admin' },
permissions: ['read', 'write']
}
}
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
primary: true,
label: 'Button',
},
// Override decorator parameters for this story
parameters: {
theme: 'light'
}
};
export const WithCustomData: Story = {
args: {
label: 'Custom Button',
},
decorators: [
(Story, context) => {
// Inline decorator for specific story
return (
<div style={{ padding: '20px', border: '2px solid red' }}>
<Story />
</div>
);
}
]
};⚠️ DEPRECATED: This API is deprecated and maintained only for backward compatibility. Use the modern hooks and decorator system instead.
/**
* @deprecated Use modern addon system instead
* Legacy addon store instance
*/
const addons: {
getChannel(): Channel;
addPanel(name: string, panel: any): void;
addDecorator(decorator: DecoratorFunction): void;
};
/**
* Creates mock communication channel for testing
* @deprecated Use mockChannel from testing-simulation.md instead
* @returns Mock channel implementation
*/
function mockChannel(): Channel;
interface Channel {
emit(eventId: string, ...args: any[]): void;
on(eventId: string, listener: Function): void;
off(eventId: string, listener: Function): void;
once(eventId: string, listener: Function): void;
addListener(eventId: string, listener: Function): void;
removeListener(eventId: string, listener: Function): void;
removeAllListeners(eventId?: string): void;
}interface StoryContext<TRenderer = any> {
id: string;
name: string;
title: string;
parameters: Parameters;
args: Args;
argTypes: ArgTypes;
globals: Args;
hooks: HooksContext<TRenderer>;
viewMode: 'story' | 'docs';
loaded: Record<string, any>;
}
interface Parameters {
[key: string]: any;
}
interface Args {
[key: string]: any;
}
interface ArgTypes {
[key: string]: ArgType;
}Install with Tessl CLI
npx tessl i tessl/npm-storybook--preview-api