Toggle between light and dark mode in Storybook
—
Event-based theme control and integration system for manual theme management and custom component integration using Storybook's addon channel.
Event names for listening to and triggering theme changes.
/** Event emitted when theme changes, payload: boolean (true = dark, false = light) */
const DARK_MODE_EVENT_NAME: 'DARK_MODE';
/** Event that can be emitted to trigger theme change */
const UPDATE_DARK_MODE_EVENT_NAME: 'UPDATE_DARK_MODE';Listen for theme changes using Storybook's addon channel for custom theme integration.
Usage Examples:
import React, { useState, useEffect } from 'react';
import addons from '@storybook/addons';
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
// Hook-based event listener
function useThemeEventListener() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const channel = addons.getChannel();
channel.on(DARK_MODE_EVENT_NAME, setIsDark);
return () => channel.off(DARK_MODE_EVENT_NAME, setIsDark);
}, []);
return isDark;
}
// Component using event listener
function EventBasedThemeWrapper({ children }) {
const [isDark, setDark] = useState(false);
useEffect(() => {
const channel = addons.getChannel();
// Listen to DARK_MODE event
channel.on(DARK_MODE_EVENT_NAME, setDark);
return () => channel.off(DARK_MODE_EVENT_NAME, setDark);
}, []);
// Apply theme to context provider
return (
<ThemeContext.Provider value={isDark ? darkTheme : lightTheme}>
{children}
</ThemeContext.Provider>
);
}Programmatically trigger theme changes by emitting events to the addon channel.
/**
* Trigger theme change programmatically
* @param mode - Optional specific mode, or undefined to toggle
*/
function triggerThemeChange(mode?: 'light' | 'dark'): void;Usage Examples:
import addons from '@storybook/addons';
import { UPDATE_DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
// Get the addon channel
const channel = addons.getChannel();
// Toggle theme (switch to opposite of current)
channel.emit(UPDATE_DARK_MODE_EVENT_NAME);
// Set specific theme
channel.emit(UPDATE_DARK_MODE_EVENT_NAME, 'dark');
channel.emit(UPDATE_DARK_MODE_EVENT_NAME, 'light');
// Example: Custom theme toggle button
function CustomThemeToggle() {
const handleToggle = () => {
const channel = addons.getChannel();
channel.emit(UPDATE_DARK_MODE_EVENT_NAME);
};
return (
<button onClick={handleToggle}>
Toggle Theme
</button>
);
}Special integration for Storybook docs mode where the toolbar is not visible, allowing custom theme controls.
import React from 'react';
import addons from '@storybook/addons';
import { DocsContainer } from '@storybook/addon-docs';
import { themes } from '@storybook/theming';
import {
DARK_MODE_EVENT_NAME,
UPDATE_DARK_MODE_EVENT_NAME
} from 'storybook-dark-mode';
const channel = addons.getChannel();
// Custom docs container with theme control
function CustomDocsContainer({ children, ...props }) {
const [isDark, setIsDark] = React.useState(false);
React.useEffect(() => {
// Listen for theme changes
channel.on(DARK_MODE_EVENT_NAME, setIsDark);
return () => channel.off(DARK_MODE_EVENT_NAME, setIsDark);
}, []);
const toggleTheme = () => {
channel.emit(UPDATE_DARK_MODE_EVENT_NAME);
};
return (
<DocsContainer
{...props}
theme={isDark ? themes.dark : themes.light}
>
<button
onClick={toggleTheme}
style={{
position: 'fixed',
top: 10,
right: 10,
zIndex: 1000
}}
>
{isDark ? '☀️' : '🌙'}
</button>
{children}
</DocsContainer>
);
}
export const parameters = {
docs: {
container: CustomDocsContainer
}
};Event payloads and their types for proper TypeScript integration.
/** DARK_MODE_EVENT_NAME payload */
type DarkModeEventPayload = boolean; // true = dark mode, false = light mode
/** UPDATE_DARK_MODE_EVENT_NAME payload */
type UpdateDarkModeEventPayload = 'light' | 'dark' | undefined; // undefined = toggleimport React, { createContext, useContext, useEffect, useState } from 'react';
import addons from '@storybook/addons';
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
// Create theme context
const ThemeEventContext = createContext(false);
// Provider component
function ThemeEventProvider({ children }) {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const channel = addons.getChannel();
channel.on(DARK_MODE_EVENT_NAME, setIsDark);
return () => channel.off(DARK_MODE_EVENT_NAME, setIsDark);
}, []);
return (
<ThemeEventContext.Provider value={isDark}>
{children}
</ThemeEventContext.Provider>
);
}
// Hook for consuming theme state
function useThemeEvent() {
return useContext(ThemeEventContext);
}import React, { useEffect, useState } from 'react';
import addons from '@storybook/addons';
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
// Story that changes based on theme
export const ThemeAwareStory = () => {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const channel = addons.getChannel();
channel.on(DARK_MODE_EVENT_NAME, setIsDark);
return () => channel.off(DARK_MODE_EVENT_NAME, setIsDark);
}, []);
if (isDark) {
return <DarkModeComponent />;
}
return <LightModeComponent />;
};Event listeners should include proper error handling and cleanup:
import addons from '@storybook/addons';
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
function safeEventListener() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const channel = addons.getChannel();
const handleThemeChange = (newIsDark) => {
try {
setIsDark(newIsDark);
// Additional theme change logic
} catch (error) {
console.error('Theme change error:', error);
}
};
channel.on(DARK_MODE_EVENT_NAME, handleThemeChange);
return () => {
try {
channel.off(DARK_MODE_EVENT_NAME, handleThemeChange);
} catch (error) {
console.error('Cleanup error:', error);
}
};
}, []);
return isDark;
}Install with Tessl CLI
npx tessl i tessl/npm-storybook-dark-mode