Choosing the Right Global Theme Decorator in Storybook: A Practical Decision Guide
When you need a consistent theme across all Storybook stories, which decorator strategy should you use? This guide compares preview.js, main.js, and Docs‑addon approaches, explains trade‑offs, and shows a concrete implementation and validation steps for React 18 and modern Storybook.
14 May 2026, 04:57 UTC

Problem Statement
In a component library that uses Storybook, developers often want every story to render under the same visual theme. Adding a ThemeProvider wrapper to each story is tedious and error‑prone. The challenge is to inject the provider globally without breaking Storybook’s build or runtime, while keeping the solution maintainable for React 18 and Storybook v8.
The decision boils down to three supported options:
- Global decorator in
preview.js - Decorator array in
main.js - Docs‑addon parameters
Constraints
- React 18 compatibility – hooks and context must work with concurrent features.
- No per‑story boilerplate – the theme should apply automatically to every story.
- Dynamic theming – the provider should be able to react to user‑selected themes at runtime.
- Minimal impact on Storybook’s build performance.
Option Comparison
| Approach | Where to Define | Runtime Timing | Dynamic Context Support | Impact on Docs | Pros | Cons |
|---|---|---|---|---|---|---|
| preview.js decorator | src/.storybook/preview.js | Before any story renders (client side) | Full – can read URL params or localStorage to switch themes | Applies to both preview and Docs | Simple, widely adopted, no build‑time code | Requires correct import paths for provider; risk of circular imports if placed in a story file |
| main.js decorators array | src/.storybook/main.js | Build‑time – decorators are serialized into the bundle | Limited – cannot depend on runtime values unless wrapped in a component that reads them | Applies to preview only; Docs parameters are separate | Centralized configuration; no need to touch preview.js | Less flexible for dynamic themes; harder to debug context errors |
| Docs addon parameters | src/.storybook/preview.js or component MDX files | Only for Docs pages | None – static parameters | Does not affect interactive preview | Great for documentation styling | Not useful when you need preview consistency |
Recommended Path: preview.js Global Decorator
The preview.js decorator is the most straightforward, flexible, and widely supported method. It runs on the client before any story mounts, so the theme context is available to all components. Below is a concrete example using a styled‑components ThemeProvider that supports runtime theme switching.
Step 1 – Install Dependencies
# If you use styled-components
npm install styled-components @storybook/addon-essentials
Step 2 – Create a Theme Context (optional)
For dynamic switching you might expose a useTheme hook that reads from localStorage or a URL query.
// src/theme/useTheme.js
import { useState, useEffect } from 'react';
export const useTheme = () => {
const [theme, setTheme] = useState('light');
useEffect(() => {
const urlTheme = new URLSearchParams(window.location.search).get('theme');
if (urlTheme) setTheme(urlTheme);
else setTheme(localStorage.getItem('theme') || 'light');
}, []);
return theme;
};
Step 3 – Implement the Decorator
// src/.storybook/preview.js
import { addDecorator } from '@storybook/react';
import { ThemeProvider } from 'styled-components';
import { useTheme } from '../theme/useTheme';
import { GlobalStyle } from '../theme/globalStyle'; // optional global CSS
const withTheme = (Story) => {
const theme = useTheme();
const themeObject = theme === 'dark' ? darkTheme : lightTheme;
return (
);
};
addDecorator(withTheme);
Key points:
addDecoratoris the classic API; in Storybook v8 you can also exportdecorators: [withTheme]from the same file.- The decorator is a React component that receives the story as a child, ensuring the provider wraps the entire tree.
- Because
useThemereads runtime data, the theme can change without rebuilding Storybook.
Step 4 – Validate the Setup
- Run Storybook:
npm run storybook - Open a story and inspect the DOM. Look for the CSS variable or class that your theme sets (e.g.,
body[data-theme="dark"]). - Check the console for no warnings about missing context or provider errors.
- Toggle the theme by adding
?theme=darkto the URL and verify the UI updates without a reload. - Use the “Show code” panel in Storybook to confirm that
withThemeappears only once in the preview configuration, not duplicated per story.
Alternative: main.js Decorator
If you prefer to keep all configuration in main.js:
// src/.storybook/main.js
module.exports = {
// ...other config
decorators: [
(Story) => (
),
],
};
Be aware that this decorator is serialized at build time, so it cannot use runtime hooks like useTheme unless you wrap it in a component that internally handles the hook.
Docs‑Addon Parameters (Not for Preview Consistency)
Adding a theme to Docs pages can be done via the parameters object, but it only affects the MDX rendering, not the interactive preview:
// src/.storybook/preview.js
export const parameters = {
docs: {
theme: { mode: 'dark' },
},
};
Use this only if you want a different theme in the Docs view.
Common Pitfalls and How to Avoid Them
- Circular imports: Do not place the decorator in the same file as a story component. Keep it in
preview.jsor a dedicateddecoratorsfolder. - Shared provider state: Ensure each decorator call creates a new
ThemeProviderinstance. Reusing the same instance across multiple decorators can lead to stale context values. - Async data in provider: If your theme depends on an API call, wrap the decorator in
React.Suspenseor provide a fallback to avoid rendering glitches. - Mismatched context keys: Verify that the context key your component library expects matches the one you expose in the provider.
Conclusion
For a robust, maintainable global theme in Storybook, the preview.js global decorator is the optimal choice. It offers runtime flexibility, applies to both preview and Docs, and keeps story files clean. Use the main.js decorator only when you need to centralize configuration without touching preview.js, and reserve Docs parameters for documentation‑specific styling.
Follow the validation steps to confirm the theme is active and no context warnings appear. With this setup, every component you develop will consistently reflect the intended design system across all stories.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.