CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-storybook--addon-themes

Storybook addon that enables theme switching functionality within the preview environment with support for multiple theming strategies

Pending
Overview
Eval results
Files

custom-decorators.mddocs/

Custom Decorators

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.

Capabilities

DecoratorHelpers Namespace

Collection of utility functions for creating custom theme decorators.

namespace DecoratorHelpers {
  function pluckThemeFromContext(context: StoryContext): string;
  function initializeThemeState(themeNames: string[], defaultTheme: string): void;
  function useThemeParameters(context?: StoryContext): ThemesParameters;
}

pluckThemeFromContext

Extracts the currently selected theme name from the Storybook story context.

/**
 * Extracts the currently selected theme name from story context
 * @param context - Storybook story context object
 * @returns The name of the currently selected theme
 */
function pluckThemeFromContext(context: StoryContext): string;

Usage Example:

import { DecoratorHelpers } from '@storybook/addon-themes';

const { pluckThemeFromContext } = DecoratorHelpers;

export const myCustomDecorator = ({ themes, defaultTheme }) => {
  return (storyFn, context) => {
    const selectedTheme = pluckThemeFromContext(context);
    const currentTheme = selectedTheme || defaultTheme;
    
    // Apply theme-specific logic
    document.documentElement.style.setProperty('--current-theme', currentTheme);
    
    return storyFn();
  };
};

initializeThemeState

Registers themes with the addon state, enabling the theme switcher UI in the Storybook toolbar.

/**
 * Registers themes with the addon state for toolbar integration
 * @param themeNames - Array of theme names to register
 * @param defaultTheme - Name of the default theme
 */
function initializeThemeState(themeNames: string[], defaultTheme: string): void;

Usage Example:

import { DecoratorHelpers } from '@storybook/addon-themes';

const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers;

export const withCustomTheming = ({ themes, defaultTheme }) => {
  // Register themes with the addon
  initializeThemeState(Object.keys(themes), defaultTheme);
  
  return (storyFn, context) => {
    const selectedTheme = pluckThemeFromContext(context);
    const theme = themes[selectedTheme] || themes[defaultTheme];
    
    // Custom theming logic here
    applyCustomTheme(theme);
    
    return storyFn();
  };
};

useThemeParameters (Deprecated)

⚠️ Deprecated: This function is deprecated and will log a deprecation warning when used. Access theme parameters directly via the context instead.

/**
 * @deprecated Access parameters via context.parameters.themes instead
 * Returns theme parameters for the current story
 * @param context - Optional story context
 * @returns Theme parameters object
 */
function useThemeParameters(context?: StoryContext): ThemesParameters;

Modern Alternative:

// Instead of useThemeParameters()
const { themeOverride } = context.parameters.themes ?? {};

// Old deprecated way
const { themeOverride } = useThemeParameters(context);

Custom Decorator Examples

CSS Custom Properties Decorator

import { DecoratorHelpers } from '@storybook/addon-themes';

const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers;

export const withCSSCustomProperties = ({ themes, defaultTheme }) => {
  initializeThemeState(Object.keys(themes), defaultTheme);
  
  return (storyFn, context) => {
    const selectedTheme = pluckThemeFromContext(context);
    const { themeOverride } = context.parameters.themes ?? {};
    
    const currentTheme = themeOverride || selectedTheme || defaultTheme;
    const themeValues = themes[currentTheme];
    
    // Apply CSS custom properties
    const root = document.documentElement;
    Object.entries(themeValues).forEach(([key, value]) => {
      root.style.setProperty(`--theme-${key}`, String(value));
    });
    
    return storyFn();
  };
};

// Usage
export const decorators = [
  withCSSCustomProperties({
    themes: {
      light: { background: '#ffffff', text: '#000000' },
      dark: { background: '#000000', text: '#ffffff' },
    },
    defaultTheme: 'light',
  }),
];

Vuetify Theme Decorator

import { DecoratorHelpers } from '@storybook/addon-themes';
import { useTheme } from 'vuetify';

const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers;

export const withVuetifyTheme = ({ themes, defaultTheme }) => {
  initializeThemeState(Object.keys(themes), defaultTheme);

  return (story, context) => {
    const selectedTheme = pluckThemeFromContext(context);
    const { themeOverride } = context.parameters.themes ?? {};

    const selected = themeOverride || selectedTheme || defaultTheme;

    return {
      components: { story },
      setup() {
        const theme = useTheme();
        theme.global.name.value = themes[selected];
        return { theme };
      },
      template: `<v-app><story /></v-app>`,
    };
  };
};

// Usage in .storybook/preview.js
export const decorators = [
  withVuetifyTheme({
    themes: {
      light: 'light',
      dark: 'dark',
      'high contrast': 'highContrast',
    },
    defaultTheme: 'light',
  }),
];

Body Class Decorator with Animation

import { DecoratorHelpers } from '@storybook/addon-themes';
import { useEffect } from 'react';

const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers;

export const withAnimatedBodyClass = ({ themes, defaultTheme, transitionDuration = 300 }) => {
  initializeThemeState(Object.keys(themes), defaultTheme);
  
  return (storyFn, context) => {
    const selectedTheme = pluckThemeFromContext(context);
    const { themeOverride } = context.parameters.themes ?? {};
    
    useEffect(() => {
      const currentTheme = themeOverride || selectedTheme || defaultTheme;
      const body = document.body;
      
      // Add transition
      body.style.transition = `background-color ${transitionDuration}ms ease, color ${transitionDuration}ms ease`;
      
      // Remove old theme classes
      Object.values(themes).forEach(className => {
        body.classList.remove(className as string);
      });
      
      // Add new theme class
      body.classList.add(themes[currentTheme]);
      
      return () => {
        body.style.transition = '';
      };
    }, [themeOverride, selectedTheme]);
    
    return storyFn();
  };
};

Multi-Context Theme Decorator

import { DecoratorHelpers } from '@storybook/addon-themes';

const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers;

export const withMultiContextTheme = ({ themes, defaultTheme, contexts }) => {
  initializeThemeState(Object.keys(themes), defaultTheme);
  
  return (storyFn, context) => {
    const selectedTheme = pluckThemeFromContext(context);
    const { themeOverride } = context.parameters.themes ?? {};
    
    const currentTheme = themeOverride || selectedTheme || defaultTheme;
    const themeConfig = themes[currentTheme];
    
    // Apply theme to multiple contexts
    contexts.forEach(({ selector, property, value }) => {
      const elements = document.querySelectorAll(selector);
      elements.forEach(element => {
        element.style[property] = themeConfig[value];
      });
    });
    
    return storyFn();
  };
};

// Usage
export const decorators = [
  withMultiContextTheme({
    themes: {
      light: { bg: '#fff', text: '#000', accent: '#007bff' },
      dark: { bg: '#000', text: '#fff', accent: '#66aaff' },
    },
    defaultTheme: 'light',
    contexts: [
      { selector: 'body', property: 'backgroundColor', value: 'bg' },
      { selector: 'body', property: 'color', value: 'text' },
      { selector: '.accent', property: 'color', value: 'accent' },
    ],
  }),
];

Theme Override Support

All custom decorators should support theme overrides at the story level:

export const MyStoryWithOverride = {
  parameters: {
    themes: {
      themeOverride: 'dark' // Force this story to use dark theme
    }
  }
};

Access the override in your decorator:

const { themeOverride } = context.parameters.themes ?? {};
const finalTheme = themeOverride || selectedTheme || defaultTheme;

Install with Tessl CLI

npx tessl i tessl/npm-storybook--addon-themes

docs

css-class-theming.md

custom-decorators.md

data-attribute-theming.md

index.md

jsx-provider-theming.md

tile.json