0
# Custom Decorators
1
2
Utilities for building custom theme decorators when the built-in decorators don't meet specific requirements. These helper functions provide access to the addon's theme state and enable custom theming implementations.
3
4
## Capabilities
5
6
### DecoratorHelpers Namespace
7
8
Collection of utility functions for creating custom theme decorators.
9
10
```typescript { .api }
11
namespace DecoratorHelpers {
12
function pluckThemeFromContext(context: StoryContext): string;
13
function initializeThemeState(themeNames: string[], defaultTheme: string): void;
14
function useThemeParameters(context?: StoryContext): ThemesParameters;
15
}
16
```
17
18
### pluckThemeFromContext
19
20
Extracts the currently selected theme name from the Storybook story context.
21
22
```typescript { .api }
23
/**
24
* Extracts the currently selected theme name from story context
25
* @param context - Storybook story context object
26
* @returns The name of the currently selected theme
27
*/
28
function pluckThemeFromContext(context: StoryContext): string;
29
```
30
31
**Usage Example:**
32
33
```typescript
34
import { DecoratorHelpers } from '@storybook/addon-themes';
35
36
const { pluckThemeFromContext } = DecoratorHelpers;
37
38
export const myCustomDecorator = ({ themes, defaultTheme }) => {
39
return (storyFn, context) => {
40
const selectedTheme = pluckThemeFromContext(context);
41
const currentTheme = selectedTheme || defaultTheme;
42
43
// Apply theme-specific logic
44
document.documentElement.style.setProperty('--current-theme', currentTheme);
45
46
return storyFn();
47
};
48
};
49
```
50
51
### initializeThemeState
52
53
Registers themes with the addon state, enabling the theme switcher UI in the Storybook toolbar.
54
55
```typescript { .api }
56
/**
57
* Registers themes with the addon state for toolbar integration
58
* @param themeNames - Array of theme names to register
59
* @param defaultTheme - Name of the default theme
60
*/
61
function initializeThemeState(themeNames: string[], defaultTheme: string): void;
62
```
63
64
**Usage Example:**
65
66
```typescript
67
import { DecoratorHelpers } from '@storybook/addon-themes';
68
69
const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers;
70
71
export const withCustomTheming = ({ themes, defaultTheme }) => {
72
// Register themes with the addon
73
initializeThemeState(Object.keys(themes), defaultTheme);
74
75
return (storyFn, context) => {
76
const selectedTheme = pluckThemeFromContext(context);
77
const theme = themes[selectedTheme] || themes[defaultTheme];
78
79
// Custom theming logic here
80
applyCustomTheme(theme);
81
82
return storyFn();
83
};
84
};
85
```
86
87
### useThemeParameters (Deprecated)
88
89
**⚠️ Deprecated**: This function is deprecated and will log a deprecation warning when used. Access theme parameters directly via the context instead.
90
91
```typescript { .api }
92
/**
93
* @deprecated Access parameters via context.parameters.themes instead
94
* Returns theme parameters for the current story
95
* @param context - Optional story context
96
* @returns Theme parameters object
97
*/
98
function useThemeParameters(context?: StoryContext): ThemesParameters;
99
```
100
101
**Modern Alternative:**
102
103
```typescript
104
// Instead of useThemeParameters()
105
const { themeOverride } = context.parameters.themes ?? {};
106
107
// Old deprecated way
108
const { themeOverride } = useThemeParameters(context);
109
```
110
111
## Custom Decorator Examples
112
113
### CSS Custom Properties Decorator
114
115
```typescript
116
import { DecoratorHelpers } from '@storybook/addon-themes';
117
118
const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers;
119
120
export const withCSSCustomProperties = ({ themes, defaultTheme }) => {
121
initializeThemeState(Object.keys(themes), defaultTheme);
122
123
return (storyFn, context) => {
124
const selectedTheme = pluckThemeFromContext(context);
125
const { themeOverride } = context.parameters.themes ?? {};
126
127
const currentTheme = themeOverride || selectedTheme || defaultTheme;
128
const themeValues = themes[currentTheme];
129
130
// Apply CSS custom properties
131
const root = document.documentElement;
132
Object.entries(themeValues).forEach(([key, value]) => {
133
root.style.setProperty(`--theme-${key}`, String(value));
134
});
135
136
return storyFn();
137
};
138
};
139
140
// Usage
141
export const decorators = [
142
withCSSCustomProperties({
143
themes: {
144
light: { background: '#ffffff', text: '#000000' },
145
dark: { background: '#000000', text: '#ffffff' },
146
},
147
defaultTheme: 'light',
148
}),
149
];
150
```
151
152
### Vuetify Theme Decorator
153
154
```typescript
155
import { DecoratorHelpers } from '@storybook/addon-themes';
156
import { useTheme } from 'vuetify';
157
158
const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers;
159
160
export const withVuetifyTheme = ({ themes, defaultTheme }) => {
161
initializeThemeState(Object.keys(themes), defaultTheme);
162
163
return (story, context) => {
164
const selectedTheme = pluckThemeFromContext(context);
165
const { themeOverride } = context.parameters.themes ?? {};
166
167
const selected = themeOverride || selectedTheme || defaultTheme;
168
169
return {
170
components: { story },
171
setup() {
172
const theme = useTheme();
173
theme.global.name.value = themes[selected];
174
return { theme };
175
},
176
template: `<v-app><story /></v-app>`,
177
};
178
};
179
};
180
181
// Usage in .storybook/preview.js
182
export const decorators = [
183
withVuetifyTheme({
184
themes: {
185
light: 'light',
186
dark: 'dark',
187
'high contrast': 'highContrast',
188
},
189
defaultTheme: 'light',
190
}),
191
];
192
```
193
194
### Body Class Decorator with Animation
195
196
```typescript
197
import { DecoratorHelpers } from '@storybook/addon-themes';
198
import { useEffect } from 'react';
199
200
const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers;
201
202
export const withAnimatedBodyClass = ({ themes, defaultTheme, transitionDuration = 300 }) => {
203
initializeThemeState(Object.keys(themes), defaultTheme);
204
205
return (storyFn, context) => {
206
const selectedTheme = pluckThemeFromContext(context);
207
const { themeOverride } = context.parameters.themes ?? {};
208
209
useEffect(() => {
210
const currentTheme = themeOverride || selectedTheme || defaultTheme;
211
const body = document.body;
212
213
// Add transition
214
body.style.transition = `background-color ${transitionDuration}ms ease, color ${transitionDuration}ms ease`;
215
216
// Remove old theme classes
217
Object.values(themes).forEach(className => {
218
body.classList.remove(className as string);
219
});
220
221
// Add new theme class
222
body.classList.add(themes[currentTheme]);
223
224
return () => {
225
body.style.transition = '';
226
};
227
}, [themeOverride, selectedTheme]);
228
229
return storyFn();
230
};
231
};
232
```
233
234
### Multi-Context Theme Decorator
235
236
```typescript
237
import { DecoratorHelpers } from '@storybook/addon-themes';
238
239
const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers;
240
241
export const withMultiContextTheme = ({ themes, defaultTheme, contexts }) => {
242
initializeThemeState(Object.keys(themes), defaultTheme);
243
244
return (storyFn, context) => {
245
const selectedTheme = pluckThemeFromContext(context);
246
const { themeOverride } = context.parameters.themes ?? {};
247
248
const currentTheme = themeOverride || selectedTheme || defaultTheme;
249
const themeConfig = themes[currentTheme];
250
251
// Apply theme to multiple contexts
252
contexts.forEach(({ selector, property, value }) => {
253
const elements = document.querySelectorAll(selector);
254
elements.forEach(element => {
255
element.style[property] = themeConfig[value];
256
});
257
});
258
259
return storyFn();
260
};
261
};
262
263
// Usage
264
export const decorators = [
265
withMultiContextTheme({
266
themes: {
267
light: { bg: '#fff', text: '#000', accent: '#007bff' },
268
dark: { bg: '#000', text: '#fff', accent: '#66aaff' },
269
},
270
defaultTheme: 'light',
271
contexts: [
272
{ selector: 'body', property: 'backgroundColor', value: 'bg' },
273
{ selector: 'body', property: 'color', value: 'text' },
274
{ selector: '.accent', property: 'color', value: 'accent' },
275
],
276
}),
277
];
278
```
279
280
## Theme Override Support
281
282
All custom decorators should support theme overrides at the story level:
283
284
```typescript
285
export const MyStoryWithOverride = {
286
parameters: {
287
themes: {
288
themeOverride: 'dark' // Force this story to use dark theme
289
}
290
}
291
};
292
```
293
294
Access the override in your decorator:
295
296
```typescript
297
const { themeOverride } = context.parameters.themes ?? {};
298
const finalTheme = themeOverride || selectedTheme || defaultTheme;
299
```