CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-storybook-dark-mode

Toggle between light and dark mode in Storybook

Pending
Overview
Eval results
Files

event-system.mddocs/

Event System

Event-based theme control and integration system for manual theme management and custom component integration using Storybook's addon channel.

Capabilities

Event Constants

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';

Event Listener Integration

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>
  );
}

Manual Theme Control

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>
  );
}

Docs Mode Integration

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 Payload Structure

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 = toggle

Advanced Integration Patterns

Multi-Component Theme Synchronization

import 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);
}

Conditional Story Rendering

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 />;
};

Error Handling

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

docs

configuration.md

event-system.md

index.md

internal-apis.md

react-integration.md

tile.json