Storybook のテーマカスタマイズ術

Storybook で UI コンポーネントを開発していると、必ずと言っていいほど直面するのが「テーマの統一」という課題です。
デフォルトのテーマでは物足りない、ブランドカラーを反映したい、ダークモードに対応したい...そんな悩みを抱えている方も多いのではないでしょうか。
この記事では、Storybook のテーマカスタマイズを徹底的に解説します。基本から応用まで、実際のコード例と共に、あなたの Storybook をより魅力的で実用的なものにする方法をお伝えします。
テーマカスタマイズの基本概念
Storybook のテーマカスタマイズとは、UI コンポーネントの見た目を統一し、ブランドの一貫性を保つための仕組みです。
テーマの重要性
テーマカスタマイズが重要な理由は 3 つあります:
- ブランドの一貫性: 企業のブランドカラーやデザインガイドラインを反映
- 開発効率の向上: 共通のスタイルを一箇所で管理
- ユーザー体験の向上: 統一された UI で使いやすいインターフェースを提供
テーマの構成要素
Storybook のテーマは以下の要素で構成されています:
typescriptinterface Theme {
// カラーパレット
color: {
primary: string;
secondary: string;
background: string;
text: string;
};
// タイポグラフィ
typography: {
fontFamily: string;
fontSize: Record<string, string>;
fontWeight: Record<string, number>;
};
// スペーシング
spacing: Record<string, string>;
// その他のデザイントークン
borderRadius: Record<string, string>;
shadows: Record<string, string>;
}
Storybook のテーマシステムを理解する
Storybook には、柔軟で強力なテーマシステムが組み込まれています。
テーマの階層構造
Storybook のテーマは階層的に管理されます:
- グローバルテーマ: アプリケーション全体に適用
- ストーリーレベルテーマ: 特定のストーリーにのみ適用
- コンポーネントレベルテーマ: 個別コンポーネントに適用
テーマプロバイダーの仕組み
テーマプロバイダーは、React の Context API を活用してテーマ情報を子コンポーネントに提供します。
typescript// .storybook/preview.tsx
import { ThemeProvider } from '@storybook/theming';
const lightTheme = {
base: 'light',
colorPrimary: '#007acc',
colorSecondary: '#585c6d',
// ... その他のテーマ設定
};
const darkTheme = {
base: 'dark',
colorPrimary: '#007acc',
colorSecondary: '#585c6d',
// ... その他のテーマ設定
};
export const decorators = [
(Story) => (
<ThemeProvider theme={lightTheme}>
<Story />
</ThemeProvider>
),
];
基本的なテーマ設定の実装
まずは、基本的なテーマ設定から始めましょう。
1. テーマ設定ファイルの作成
.storybook/theme.ts
ファイルを作成して、テーマの定義を行います。
typescript// .storybook/theme.ts
export const lightTheme = {
base: 'light',
// カラーパレット
colorPrimary: '#007acc',
colorSecondary: '#585c6d',
// UI要素の色
appBg: '#ffffff',
appContentBg: '#ffffff',
barBg: '#f8f9fa',
barTextColor: '#333333',
// テキストカラー
textColor: '#333333',
textInverseColor: '#ffffff',
// ボーダーとシャドウ
barSelectedColor: '#007acc',
inputBg: '#ffffff',
inputBorder: '#e1e5e9',
inputTextColor: '#333333',
};
2. Preview ファイルでの適用
作成したテーマを.storybook/preview.tsx
で適用します。
typescript// .storybook/preview.tsx
import { Preview } from '@storybook/react';
import { lightTheme } from './theme';
const preview: Preview = {
parameters: {
docs: {
theme: lightTheme,
},
backgrounds: {
default: 'light',
values: [
{
name: 'light',
value: '#ffffff',
},
{
name: 'dark',
value: '#333333',
},
],
},
},
};
export default preview;
3. よくあるエラーと解決策
エラー 1: ThemeProvider が見つからない
bashModule not found: Can't resolve '@storybook/theming'
解決策: 必要なパッケージをインストールします。
bashyarn add @storybook/theming
エラー 2: テーマが適用されない
typescript// 間違った例
export const parameters = {
theme: lightTheme, // これは機能しません
};
// 正しい例
export const parameters = {
docs: {
theme: lightTheme,
},
};
カスタムテーマの作成手順
ブランドに合わせたカスタムテーマを作成してみましょう。
1. デザイントークンの定義
まず、デザイントークンを定義します。
typescript// .storybook/design-tokens.ts
export const colors = {
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
900: '#1e3a8a',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
500: '#6b7280',
900: '#111827',
},
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
};
export const typography = {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
},
};
2. カスタムテーマの構築
デザイントークンを使用してカスタムテーマを構築します。
typescript// .storybook/custom-theme.ts
import { create } from '@storybook/theming/create';
import { colors, typography } from './design-tokens';
export const customTheme = create({
base: 'light',
// ブランド情報
brandTitle: 'My Company Design System',
brandUrl: 'https://mycompany.com',
brandImage: '/logo.svg',
// カラーパレット
colorPrimary: colors.primary[500],
colorSecondary: colors.primary[600],
// UI要素
appBg: colors.gray[50],
appContentBg: '#ffffff',
barBg: '#ffffff',
barTextColor: colors.gray[900],
barSelectedColor: colors.primary[500],
// テキスト
textColor: colors.gray[900],
textInverseColor: '#ffffff',
// フォーム要素
inputBg: '#ffffff',
inputBorder: colors.gray[100],
inputTextColor: colors.gray[900],
// フォント
fontBase: typography.fontFamily.sans.join(', '),
fontCode: typography.fontFamily.mono.join(', '),
});
3. テーマの適用
カスタムテーマを Storybook に適用します。
typescript// .storybook/preview.tsx
import { Preview } from '@storybook/react';
import { customTheme } from './custom-theme';
const preview: Preview = {
parameters: {
docs: {
theme: customTheme,
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;
ダークモード・ライトモードの切り替え
ユーザーの好みに応じてテーマを切り替えられるようにしましょう。
1. ダークテーマの作成
typescript// .storybook/dark-theme.ts
import { create } from '@storybook/theming/create';
import { colors, typography } from './design-tokens';
export const darkTheme = create({
base: 'dark',
// ブランド情報(ライトテーマと同じ)
brandTitle: 'My Company Design System',
brandUrl: 'https://mycompany.com',
brandImage: '/logo-white.svg',
// ダークモード用カラーパレット
colorPrimary: colors.primary[400],
colorSecondary: colors.primary[300],
// ダークモード用UI要素
appBg: colors.gray[900],
appContentBg: colors.gray[800],
barBg: colors.gray[800],
barTextColor: colors.gray[100],
barSelectedColor: colors.primary[400],
// ダークモード用テキスト
textColor: colors.gray[100],
textInverseColor: colors.gray[900],
// ダークモード用フォーム要素
inputBg: colors.gray[700],
inputBorder: colors.gray[600],
inputTextColor: colors.gray[100],
// フォント(ライトテーマと同じ)
fontBase: typography.fontFamily.sans.join(', '),
fontCode: typography.fontFamily.mono.join(', '),
});
2. テーマ切り替え機能の実装
typescript// .storybook/ThemeToggle.tsx
import React, { useState } from 'react';
import { useGlobals } from '@storybook/manager-api';
export const ThemeToggle = () => {
const [globals, updateGlobals] = useGlobals();
const [isDark, setIsDark] = useState(
globals.theme === 'dark'
);
const toggleTheme = () => {
const newTheme = isDark ? 'light' : 'dark';
setIsDark(!isDark);
updateGlobals({ theme: newTheme });
};
return (
<button
onClick={toggleTheme}
style={{
padding: '8px 16px',
border: '1px solid #ccc',
borderRadius: '4px',
background: isDark ? '#333' : '#fff',
color: isDark ? '#fff' : '#333',
cursor: 'pointer',
}}
>
{isDark ? '🌞 Light' : '🌙 Dark'}
</button>
);
};
3. 動的テーマ切り替えの実装
typescript// .storybook/preview.tsx
import { Preview } from '@storybook/react';
import { customTheme } from './custom-theme';
import { darkTheme } from './dark-theme';
const preview: Preview = {
globalTypes: {
theme: {
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
title: 'Theme',
icon: 'circlehollow',
items: ['light', 'dark'],
dynamicTitle: true,
},
},
},
parameters: {
docs: {
theme: customTheme, // デフォルトはライトテーマ
},
},
decorators: [
(Story, context) => {
const theme =
context.globals.theme === 'dark'
? darkTheme
: customTheme;
return (
<div
style={{
background: theme.appBg,
color: theme.textColor,
padding: '20px',
minHeight: '100vh',
}}
>
<Story />
</div>
);
},
],
};
export default preview;
ブランドカラーの統合方法
企業のブランドカラーを Storybook に統合する方法を解説します。
1. ブランドカラーパレットの定義
typescript// .storybook/brand-colors.ts
export const brandColors = {
// メインブランドカラー
primary: {
main: '#1a73e8', // Google Blue
light: '#4285f4',
dark: '#0d47a1',
contrast: '#ffffff',
},
// セカンダリブランドカラー
secondary: {
main: '#34a853', // Google Green
light: '#66bb6a',
dark: '#2e7d32',
contrast: '#ffffff',
},
// アクセントカラー
accent: {
main: '#ea4335', // Google Red
light: '#ef5350',
dark: '#c62828',
contrast: '#ffffff',
},
// ニュートラルカラー
neutral: {
50: '#fafafa',
100: '#f5f5f5',
200: '#eeeeee',
300: '#e0e0e0',
400: '#bdbdbd',
500: '#9e9e9e',
600: '#757575',
700: '#616161',
800: '#424242',
900: '#212121',
},
};
2. ブランドテーマの作成
typescript// .storybook/brand-theme.ts
import { create } from '@storybook/theming/create';
import { brandColors } from './brand-colors';
export const brandTheme = create({
base: 'light',
// ブランド情報
brandTitle: 'Google Design System',
brandUrl: 'https://design.google',
brandImage: '/google-logo.svg',
// ブランドカラーの適用
colorPrimary: brandColors.primary.main,
colorSecondary: brandColors.secondary.main,
// UI要素にブランドカラーを適用
appBg: brandColors.neutral[50],
appContentBg: '#ffffff',
barBg: '#ffffff',
barTextColor: brandColors.neutral[900],
barSelectedColor: brandColors.primary.main,
// テキストカラー
textColor: brandColors.neutral[900],
textInverseColor: '#ffffff',
// フォーム要素
inputBg: '#ffffff',
inputBorder: brandColors.neutral[300],
inputTextColor: brandColors.neutral[900],
// フォント
fontBase: '"Google Sans", "Roboto", sans-serif',
fontCode: '"JetBrains Mono", monospace',
});
3. CSS 変数との連携
ブランドカラーを CSS 変数として定義し、コンポーネントで使用できるようにします。
css/* .storybook/preview.css */
:root {
/* ブランドカラー */
--color-primary: #1a73e8;
--color-primary-light: #4285f4;
--color-primary-dark: #0d47a1;
--color-secondary: #34a853;
--color-secondary-light: #66bb6a;
--color-secondary-dark: #2e7d32;
--color-accent: #ea4335;
--color-accent-light: #ef5350;
--color-accent-dark: #c62828;
/* ニュートラルカラー */
--color-neutral-50: #fafafa;
--color-neutral-100: #f5f5f5;
--color-neutral-500: #9e9e9e;
--color-neutral-900: #212121;
/* スペーシング */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* タイポグラフィ */
--font-family-sans: 'Google Sans', 'Roboto', sans-serif;
--font-family-mono: 'JetBrains Mono', monospace;
}
フォントとタイポグラフィのカスタマイズ
読みやすく美しいタイポグラフィを実現しましょう。
1. フォントファミリーの設定
typescript// .storybook/typography.ts
export const typographyConfig = {
fontFamily: {
sans: [
'Inter',
'-apple-system',
'BlinkMacSystemFont',
'Segoe UI',
'Roboto',
'sans-serif',
],
serif: ['Georgia', 'Times New Roman', 'serif'],
mono: [
'JetBrains Mono',
'Fira Code',
'Consolas',
'monospace',
],
},
fontSize: {
xs: '0.75rem', // 12px
sm: '0.875rem', // 14px
base: '1rem', // 16px
lg: '1.125rem', // 18px
xl: '1.25rem', // 20px
'2xl': '1.5rem', // 24px
'3xl': '1.875rem', // 30px
'4xl': '2.25rem', // 36px
},
fontWeight: {
light: 300,
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
extrabold: 800,
},
lineHeight: {
tight: 1.25,
normal: 1.5,
relaxed: 1.75,
},
};
2. タイポグラフィテーマの適用
typescript// .storybook/typography-theme.ts
import { create } from '@storybook/theming/create';
import { typographyConfig } from './typography';
export const typographyTheme = create({
base: 'light',
// フォントファミリー
fontBase: typographyConfig.fontFamily.sans.join(', '),
fontCode: typographyConfig.fontFamily.mono.join(', '),
// その他のテーマ設定
colorPrimary: '#1a73e8',
colorSecondary: '#34a853',
appBg: '#ffffff',
appContentBg: '#ffffff',
barBg: '#f8f9fa',
barTextColor: '#333333',
textColor: '#333333',
textInverseColor: '#ffffff',
});
3. フォントの読み込み
Google Fonts やその他の Web フォントを読み込みます。
html<!-- .storybook/preview-head.html -->
<link
rel="preconnect"
href="https://fonts.googleapis.com"
/>
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin
/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
4. タイポグラフィユーティリティの作成
typescript// .storybook/typography-utils.ts
import { typographyConfig } from './typography';
export const createTypographyStyle = (
size: keyof typeof typographyConfig.fontSize,
weight: keyof typeof typographyConfig.fontWeight = 'normal',
family: keyof typeof typographyConfig.fontFamily = 'sans'
) => ({
fontFamily:
typographyConfig.fontFamily[family].join(', '),
fontSize: typographyConfig.fontSize[size],
fontWeight: typographyConfig.fontWeight[weight],
lineHeight: typographyConfig.lineHeight.normal,
});
コンポーネント固有のテーマ設定
特定のコンポーネントにのみ適用されるテーマを設定する方法を解説します。
1. コンポーネントレベルテーマの定義
typescript// .storybook/component-themes.ts
export const buttonThemes = {
primary: {
backgroundColor: '#1a73e8',
color: '#ffffff',
border: 'none',
borderRadius: '8px',
padding: '12px 24px',
fontSize: '16px',
fontWeight: 500,
cursor: 'pointer',
transition: 'all 0.2s ease',
'&:hover': {
backgroundColor: '#1557b0',
transform: 'translateY(-1px)',
},
'&:active': {
backgroundColor: '#0d47a1',
transform: 'translateY(0)',
},
},
secondary: {
backgroundColor: 'transparent',
color: '#1a73e8',
border: '2px solid #1a73e8',
borderRadius: '8px',
padding: '10px 22px',
fontSize: '16px',
fontWeight: 500,
cursor: 'pointer',
transition: 'all 0.2s ease',
'&:hover': {
backgroundColor: '#1a73e8',
color: '#ffffff',
},
},
danger: {
backgroundColor: '#ea4335',
color: '#ffffff',
border: 'none',
borderRadius: '8px',
padding: '12px 24px',
fontSize: '16px',
fontWeight: 500,
cursor: 'pointer',
transition: 'all 0.2s ease',
'&:hover': {
backgroundColor: '#d32f2f',
},
},
};
2. ストーリーレベルでのテーマ適用
typescript// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
import { buttonThemes } from '../.storybook/component-themes';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
// プライマリボタンのストーリー
export const Primary: Story = {
args: {
children: 'Primary Button',
variant: 'primary',
},
decorators: [
(Story) => (
<div style={buttonThemes.primary}>
<Story />
</div>
),
],
};
// セカンダリボタンのストーリー
export const Secondary: Story = {
args: {
children: 'Secondary Button',
variant: 'secondary',
},
decorators: [
(Story) => (
<div style={buttonThemes.secondary}>
<Story />
</div>
),
],
};
// 危険ボタンのストーリー
export const Danger: Story = {
args: {
children: 'Danger Button',
variant: 'danger',
},
decorators: [
(Story) => (
<div style={buttonThemes.danger}>
<Story />
</div>
),
],
};
3. テーマプロバイダーを使用したコンポーネントテーマ
typescript// .storybook/ComponentThemeProvider.tsx
import React, { createContext, useContext } from 'react';
interface ComponentTheme {
button: typeof buttonThemes;
card: any;
input: any;
}
const ComponentThemeContext =
createContext<ComponentTheme | null>(null);
export const ComponentThemeProvider: React.FC<{
children: React.ReactNode;
theme: ComponentTheme;
}> = ({ children, theme }) => {
return (
<ComponentThemeContext.Provider value={theme}>
{children}
</ComponentThemeContext.Provider>
);
};
export const useComponentTheme = () => {
const context = useContext(ComponentThemeContext);
if (!context) {
throw new Error(
'useComponentTheme must be used within ComponentThemeProvider'
);
}
return context;
};
テーマの動的切り替え機能
ユーザーがリアルタイムでテーマを切り替えられる機能を実装しましょう。
1. テーマ切り替えコンポーネントの作成
typescript// .storybook/ThemeSwitcher.tsx
import React from 'react';
import { useGlobals } from '@storybook/manager-api';
interface ThemeOption {
name: string;
value: string;
icon: string;
}
const themeOptions: ThemeOption[] = [
{ name: 'Light', value: 'light', icon: '☀️' },
{ name: 'Dark', value: 'dark', icon: '🌙' },
{ name: 'Brand', value: 'brand', icon: '🎨' },
{
name: 'High Contrast',
value: 'high-contrast',
icon: '🔍',
},
];
export const ThemeSwitcher: React.FC = () => {
const [globals, updateGlobals] = useGlobals();
const currentTheme = globals.theme || 'light';
const handleThemeChange = (themeValue: string) => {
updateGlobals({ theme: themeValue });
};
return (
<div
style={{
display: 'flex',
gap: '8px',
padding: '16px',
borderBottom: '1px solid #e1e5e9',
}}
>
{themeOptions.map((option) => (
<button
key={option.value}
onClick={() => handleThemeChange(option.value)}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '8px 12px',
border: `1px solid ${
currentTheme === option.value
? '#1a73e8'
: '#e1e5e9'
}`,
borderRadius: '6px',
background:
currentTheme === option.value
? '#1a73e8'
: '#ffffff',
color:
currentTheme === option.value
? '#ffffff'
: '#333333',
cursor: 'pointer',
fontSize: '14px',
transition: 'all 0.2s ease',
}}
>
<span>{option.icon}</span>
<span>{option.name}</span>
</button>
))}
</div>
);
};
2. 動的テーママッピングの実装
typescript// .storybook/dynamic-themes.ts
import { customTheme } from './custom-theme';
import { darkTheme } from './dark-theme';
import { brandTheme } from './brand-theme';
export const themeMap = {
light: customTheme,
dark: darkTheme,
brand: brandTheme,
'high-contrast': {
...customTheme,
colorPrimary: '#000000',
colorSecondary: '#ffffff',
appBg: '#ffffff',
appContentBg: '#ffffff',
textColor: '#000000',
barBg: '#000000',
barTextColor: '#ffffff',
},
};
export const getThemeByName = (themeName: string) => {
return (
themeMap[themeName as keyof typeof themeMap] ||
customTheme
);
};
3. 動的テーマ切り替えの統合
typescript// .storybook/preview.tsx
import { Preview } from '@storybook/react';
import { getThemeByName } from './dynamic-themes';
import { ThemeSwitcher } from './ThemeSwitcher';
const preview: Preview = {
globalTypes: {
theme: {
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
title: 'Theme',
icon: 'circlehollow',
items: ['light', 'dark', 'brand', 'high-contrast'],
dynamicTitle: true,
},
},
},
parameters: {
docs: {
theme: customTheme,
},
},
decorators: [
(Story, context) => {
const themeName = context.globals.theme || 'light';
const theme = getThemeByName(themeName);
return (
<div
style={{
background: theme.appBg,
color: theme.textColor,
minHeight: '100vh',
fontFamily: theme.fontBase,
}}
>
<ThemeSwitcher />
<div style={{ padding: '20px' }}>
<Story />
</div>
</div>
);
},
],
};
export default preview;
パフォーマンス最適化のコツ
テーマカスタマイズでパフォーマンスを落とさないためのベストプラクティスを紹介します。
1. テーマの遅延読み込み
typescript// .storybook/lazy-themes.ts
export const lazyLoadTheme = async (themeName: string) => {
switch (themeName) {
case 'dark':
const { darkTheme } = await import('./dark-theme');
return darkTheme;
case 'brand':
const { brandTheme } = await import('./brand-theme');
return brandTheme;
default:
const { customTheme } = await import(
'./custom-theme'
);
return customTheme;
}
};
2. テーマのメモ化
typescript// .storybook/memoized-theme.ts
import { useMemo } from 'react';
import { create } from '@storybook/theming/create';
export const useMemoizedTheme = (themeConfig: any) => {
return useMemo(() => {
return create(themeConfig);
}, [JSON.stringify(themeConfig)]);
};
3. CSS 変数の最適化
css/* .storybook/optimized-theme.css */
/* テーマ切り替え時のパフォーマンス向上のため、CSS変数を活用 */
.theme-light {
--color-primary: #1a73e8;
--color-secondary: #34a853;
--color-background: #ffffff;
--color-text: #333333;
}
.theme-dark {
--color-primary: #4285f4;
--color-secondary: #66bb6a;
--color-background: #121212;
--color-text: #ffffff;
}
/* コンポーネントでCSS変数を使用 */
.component {
background-color: var(--color-background);
color: var(--color-text);
border: 1px solid var(--color-primary);
}
4. テーマ切り替えの最適化
typescript// .storybook/optimized-theme-switcher.tsx
import React, { useCallback, useMemo } from 'react';
import { useGlobals } from '@storybook/manager-api';
export const OptimizedThemeSwitcher: React.FC = () => {
const [globals, updateGlobals] = useGlobals();
// テーマ切り替えをメモ化
const handleThemeChange = useCallback(
(themeName: string) => {
updateGlobals({ theme: themeName });
},
[updateGlobals]
);
// 現在のテーマをメモ化
const currentTheme = useMemo(() => {
return globals.theme || 'light';
}, [globals.theme]);
return (
<div style={{ display: 'flex', gap: '8px' }}>
{['light', 'dark', 'brand'].map((theme) => (
<button
key={theme}
onClick={() => handleThemeChange(theme)}
style={{
padding: '8px 16px',
border: `1px solid ${
currentTheme === theme ? '#1a73e8' : '#e1e5e9'
}`,
borderRadius: '4px',
background:
currentTheme === theme
? '#1a73e8'
: '#ffffff',
color:
currentTheme === theme
? '#ffffff'
: '#333333',
cursor: 'pointer',
}}
>
{theme.charAt(0).toUpperCase() + theme.slice(1)}
</button>
))}
</div>
);
};
よくあるトラブルと解決策
Storybook のテーマカスタマイズでよく遭遇する問題とその解決策を紹介します。
1. テーマが適用されない問題
エラー: テーマの設定が反映されない
原因: パラメータの設定場所が間違っている
typescript// 間違った例
export const parameters = {
theme: customTheme, // これは機能しません
};
// 正しい例
export const parameters = {
docs: {
theme: customTheme,
},
};
解決策: 正しいパラメータ構造を使用する
2. フォントが読み込まれない問題
エラー: カスタムフォントが表示されない
原因: フォントの読み込みタイミングの問題
html<!-- .storybook/preview-head.html -->
<!-- フォントのプリロード -->
<link
rel="preload"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
as="style"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
/>
解決策: フォントをプリロードして読み込みを最適化する
3. テーマ切り替え時のちらつき問題
エラー: テーマ切り替え時に画面がちらつく
原因: テーマ切り替え時の再レンダリング
typescript// .storybook/smooth-theme-switch.tsx
import React, { useState, useEffect } from 'react';
export const SmoothThemeSwitch: React.FC = () => {
const [isTransitioning, setIsTransitioning] =
useState(false);
const handleThemeChange = (newTheme: string) => {
setIsTransitioning(true);
// トランジション用のCSSクラスを追加
document.body.style.transition = 'all 0.3s ease';
setTimeout(() => {
// テーマを切り替え
updateGlobals({ theme: newTheme });
setTimeout(() => {
setIsTransitioning(false);
document.body.style.transition = '';
}, 300);
}, 50);
};
return (
<div
className={
isTransitioning ? 'theme-transitioning' : ''
}
>
{/* テーマ切り替えUI */}
</div>
);
};
解決策: CSS トランジションを使用してスムーズな切り替えを実現
4. パフォーマンスの問題
エラー: テーマ切り替えが遅い
原因: 不要な再計算や再レンダリング
typescript// .storybook/performance-optimized-theme.ts
import { useMemo, useCallback } from 'react';
export const useOptimizedTheme = (themeConfig: any) => {
// テーマ設定をメモ化
const memoizedConfig = useMemo(
() => themeConfig,
[JSON.stringify(themeConfig)]
);
// テーマ作成をメモ化
const theme = useMemo(() => {
return create(memoizedConfig);
}, [memoizedConfig]);
// テーマ切り替えをコールバック化
const switchTheme = useCallback((newConfig: any) => {
// テーマ切り替えロジック
}, []);
return { theme, switchTheme };
};
解決策: React.memo、useMemo、useCallback を活用してパフォーマンスを最適化
5. TypeScript エラーの解決
エラー: テーマの型定義エラー
typescript// .storybook/theme-types.ts
import { Theme } from '@storybook/theming';
export interface CustomTheme extends Theme {
// カスタムプロパティの型定義
customProperty?: string;
brandColors?: {
primary: string;
secondary: string;
};
}
// 型安全なテーマ作成
export const createTypedTheme = (
config: Partial<CustomTheme>
): CustomTheme => {
return create(config) as CustomTheme;
};
解決策: 適切な型定義を作成して型安全性を確保
まとめ
Storybook のテーマカスタマイズは、UI コンポーネントライブラリの品質を大きく左右する重要な要素です。
この記事で紹介した手法を活用することで、以下のような効果が期待できます:
開発効率の向上
- 一貫したデザインシステムの構築
- ブランドガイドラインの自動適用
- チーム開発での統一感の確保
ユーザー体験の向上
- 美しく統一された UI
- アクセシビリティへの配慮
- ダークモード対応
保守性の向上
- テーマの一元管理
- 変更の影響範囲の明確化
- コードの再利用性向上
テーマカスタマイズは、最初は複雑に感じるかもしれませんが、一度設定してしまえば、その後の開発効率が劇的に向上します。
特に、ブランドカラーの統合やダークモード対応は、ユーザーからの評価に直結する重要な要素です。
今回紹介したベストプラクティスを参考に、あなたの Storybook プロジェクトに最適なテーマシステムを構築してください。
そして、テーマカスタマイズを通じて、より魅力的で使いやすい UI コンポーネントライブラリを作り上げていきましょう。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来