Emotion でダークモード・テーマ切替を実装する

ユーザー体験を向上させるダークモード機能。多くの開発者が憧れる機能の一つですが、実装の複雑さに躊躇してしまうこともあるでしょう。Emotion を使えば、この複雑さをシンプルに解決できます。
本記事では、Emotion のテーマシステムを活用して、美しく使いやすいダークモード・テーマ切替機能を実装する方法をご紹介します。初心者の方でも理解しやすいよう、段階的に進めていきましょう。
Emotion のテーマシステムの基礎
CSS-in-JS と Emotion の特徴
Emotion は、JavaScript 内で CSS を記述できる CSS-in-JS ライブラリです。従来の CSS ファイルとは異なり、動的なスタイリングが可能で、テーマ切り替えに最適な特徴を持っています。
Emotion の最大の魅力は、コンポーネントとスタイルが密接に結びついていることです。これにより、テーマの変更が即座に反映され、ユーザーにスムーズな体験を提供できます。
javascript// Emotionの基本的な使い方
import styled from '@emotion/styled';
const Button = styled.button`
background-color: ${(props) => props.theme.primary};
color: ${(props) => props.theme.text};
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background-color: ${(props) =>
props.theme.primaryHover};
}
`;
このように、テーマオブジェクトから値を取得してスタイルを動的に変更できます。これが Emotion のテーマシステムの核となる機能です。
テーマオブジェクトの設計思想
テーマオブジェクトは、アプリケーション全体のデザインシステムを定義する重要な要素です。適切に設計することで、一貫性のある UI を構築できます。
javascript// テーマオブジェクトの基本構造
const lightTheme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
background: '#ffffff',
surface: '#f8f9fa',
text: '#212529',
textSecondary: '#6c757d',
border: '#dee2e6',
error: '#dc3545',
success: '#28a745',
warning: '#ffc107',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '12px',
},
shadows: {
sm: '0 2px 4px rgba(0,0,0,0.1)',
md: '0 4px 8px rgba(0,0,0,0.15)',
lg: '0 8px 16px rgba(0,0,0,0.2)',
},
};
テーマオブジェクトの設計で重要なのは、意味のある命名と階層構造です。色だけでなく、スペーシングやボーダーラジウスなども含めることで、一貫性のあるデザインシステムを構築できます。
グローバルスタイルとテーマの連携
Emotion の Global コンポーネントを使うことで、アプリケーション全体に適用されるスタイルを定義できます。これにより、テーマの変更が即座に全体に反映されます。
javascriptimport { Global, css } from '@emotion/react';
const GlobalStyles = ({ theme }) => (
<Global
styles={css`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: ${theme.colors.background};
color: ${theme.colors.text};
font-family: -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, sans-serif;
transition: background-color 0.3s ease, color 0.3s
ease;
}
a {
color: ${theme.colors.primary};
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
`}
/>
);
このグローバルスタイルにより、テーマ切り替え時のスムーズな遷移効果も実現できます。
ダークモード実装の準備
プロジェクトの初期設定
まず、新しい React プロジェクトを作成し、Emotion をセットアップしましょう。
bash# 新しいプロジェクトを作成
yarn create react-app emotion-theme-app --template typescript
# プロジェクトディレクトリに移動
cd emotion-theme-app
# Emotionの必要なパッケージをインストール
yarn add @emotion/react @emotion/styled
必要なパッケージのインストール
Emotion でテーマ機能を実装するために必要なパッケージをインストールします。
bash# 型定義とユーティリティ
yarn add @emotion/types @emotion/utils
# 開発用ツール(オプション)
yarn add -D @emotion/babel-plugin
テーマ定義の構造設計
プロジェクトの構造を整理し、テーマ関連のファイルを配置します。
csssrc/
├── themes/
│ ├── index.ts
│ ├── lightTheme.ts
│ ├── darkTheme.ts
│ └── types.ts
├── components/
│ ├── ThemeProvider.tsx
│ └── ThemeToggle.tsx
├── hooks/
│ └── useTheme.ts
└── App.tsx
この構造により、テーマ関連のコードが整理され、メンテナンスしやすくなります。
テーマコンテキストの構築
React Context を使ったテーマ管理
React Context を使うことで、アプリケーション全体でテーマの状態を管理できます。これにより、どのコンポーネントからでもテーマにアクセスできるようになります。
typescript// src/themes/types.ts
export interface Theme {
colors: {
primary: string;
secondary: string;
background: string;
surface: string;
text: string;
textSecondary: string;
border: string;
error: string;
success: string;
warning: string;
};
spacing: {
xs: string;
sm: string;
md: string;
lg: string;
xl: string;
};
borderRadius: {
sm: string;
md: string;
lg: string;
};
shadows: {
sm: string;
md: string;
lg: string;
};
}
export type ThemeMode = 'light' | 'dark';
typescript// src/themes/ThemeContext.tsx
import React, {
createContext,
useContext,
useState,
useEffect,
} from 'react';
import { ThemeProvider as EmotionThemeProvider } from '@emotion/react';
import { lightTheme, darkTheme } from './index';
import { Theme, ThemeMode } from './types';
interface ThemeContextType {
theme: Theme;
themeMode: ThemeMode;
toggleTheme: () => void;
setThemeMode: (mode: ThemeMode) => void;
}
const ThemeContext = createContext<
ThemeContextType | undefined
>(undefined);
export const useThemeContext = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error(
'useThemeContext must be used within a ThemeProvider'
);
}
return context;
};
export const ThemeProvider: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const [themeMode, setThemeMode] =
useState<ThemeMode>('light');
const theme =
themeMode === 'light' ? lightTheme : darkTheme;
const toggleTheme = () => {
setThemeMode((prev) =>
prev === 'light' ? 'dark' : 'light'
);
};
return (
<ThemeContext.Provider
value={{
theme,
themeMode,
toggleTheme,
setThemeMode,
}}
>
<EmotionThemeProvider theme={theme}>
{children}
</EmotionThemeProvider>
</ThemeContext.Provider>
);
};
テーマ切り替えの状態管理
テーマの状態を適切に管理することで、ユーザーの設定を保持し、一貫した体験を提供できます。
typescript// src/hooks/useTheme.ts
import { useState, useEffect } from 'react';
import { ThemeMode } from '../themes/types';
const THEME_STORAGE_KEY = 'theme-mode';
export const useTheme = () => {
const [themeMode, setThemeMode] = useState<ThemeMode>(
() => {
// ローカルストレージから初期値を取得
const saved = localStorage.getItem(THEME_STORAGE_KEY);
if (saved === 'light' || saved === 'dark') {
return saved;
}
// システム設定を確認
if (
window.matchMedia('(prefers-color-scheme: dark)')
.matches
) {
return 'dark';
}
return 'light';
}
);
useEffect(() => {
// テーマ変更時にローカルストレージに保存
localStorage.setItem(THEME_STORAGE_KEY, themeMode);
// システム設定の変更を監視
const mediaQuery = window.matchMedia(
'(prefers-color-scheme: dark)'
);
const handleChange = (e: MediaQueryListEvent) => {
if (!localStorage.getItem(THEME_STORAGE_KEY)) {
setThemeMode(e.matches ? 'dark' : 'light');
}
};
mediaQuery.addEventListener('change', handleChange);
return () =>
mediaQuery.removeEventListener(
'change',
handleChange
);
}, [themeMode]);
return { themeMode, setThemeMode };
};
ローカルストレージでの永続化
ユーザーのテーマ設定をローカルストレージに保存することで、ページをリロードしても設定が保持されます。
typescript// src/utils/themeStorage.ts
import { ThemeMode } from '../themes/types';
const STORAGE_KEY = 'theme-mode';
export const saveThemeMode = (mode: ThemeMode): void => {
try {
localStorage.setItem(STORAGE_KEY, mode);
} catch (error) {
console.warn(
'Failed to save theme mode to localStorage:',
error
);
}
};
export const loadThemeMode = (): ThemeMode | null => {
try {
const saved = localStorage.getItem(STORAGE_KEY);
return saved === 'light' || saved === 'dark'
? saved
: null;
} catch (error) {
console.warn(
'Failed to load theme mode from localStorage:',
error
);
return null;
}
};
export const clearThemeMode = (): void => {
try {
localStorage.removeItem(STORAGE_KEY);
} catch (error) {
console.warn(
'Failed to clear theme mode from localStorage:',
error
);
}
};
コンポーネントでのテーマ活用
styled-components でのテーマ適用
Emotion の styled-components を使うことで、テーマを活用した動的なスタイリングが可能になります。
typescript// src/components/styled/Button.tsx
import styled from '@emotion/styled';
import { Theme } from '../../themes/types';
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'small' | 'medium' | 'large';
}
const getButtonStyles = (
theme: Theme,
variant: string,
size: string
) => {
const baseStyles = `
border: none;
border-radius: ${theme.borderRadius.md};
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
`;
const variantStyles = {
primary: `
background-color: ${theme.colors.primary};
color: white;
&:hover:not(:disabled) {
background-color: ${theme.colors.primary}dd;
transform: translateY(-1px);
box-shadow: ${theme.shadows.md};
}
`,
secondary: `
background-color: ${theme.colors.secondary};
color: white;
&:hover:not(:disabled) {
background-color: ${theme.colors.secondary}dd;
transform: translateY(-1px);
box-shadow: ${theme.shadows.md};
}
`,
outline: `
background-color: transparent;
color: ${theme.colors.primary};
border: 2px solid ${theme.colors.primary};
&:hover:not(:disabled) {
background-color: ${theme.colors.primary};
color: white;
}
`,
};
const sizeStyles = {
small: `
padding: ${theme.spacing.xs} ${theme.spacing.sm};
font-size: 14px;
`,
medium: `
padding: ${theme.spacing.sm} ${theme.spacing.md};
font-size: 16px;
`,
large: `
padding: ${theme.spacing.md} ${theme.spacing.lg};
font-size: 18px;
`,
};
return `
${baseStyles}
${variantStyles[variant as keyof typeof variantStyles]}
${sizeStyles[size as keyof typeof sizeStyles]}
`;
};
export const Button = styled.button<ButtonProps>`
${({ theme, variant = 'primary', size = 'medium' }) =>
getButtonStyles(theme, variant, size)}
`;
条件分岐を使ったスタイル切り替え
テーマの状態に応じて、動的にスタイルを変更できます。
typescript// src/components/styled/Card.tsx
import styled from '@emotion/styled';
import { Theme } from '../../themes/types';
interface CardProps {
elevation?: 'low' | 'medium' | 'high';
}
export const Card = styled.div<CardProps>`
background-color: ${({ theme }) => theme.colors.surface};
border: 1px solid ${({ theme }) => theme.colors.border};
border-radius: ${({ theme }) => theme.borderRadius.lg};
padding: ${({ theme }) => theme.spacing.lg};
box-shadow: ${({ theme, elevation = 'low' }) => {
switch (elevation) {
case 'high':
return theme.shadows.lg;
case 'medium':
return theme.shadows.md;
default:
return theme.shadows.sm;
}
}};
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: ${({ theme }) => theme.shadows.lg};
}
`;
export const CardHeader = styled.div`
margin-bottom: ${({ theme }) => theme.spacing.md};
border-bottom: 1px solid ${({ theme }) =>
theme.colors.border};
padding-bottom: ${({ theme }) => theme.spacing.sm};
`;
export const CardTitle = styled.h3`
color: ${({ theme }) => theme.colors.text};
margin: 0;
font-size: 1.25rem;
font-weight: 600;
`;
export const CardContent = styled.div`
color: ${({ theme }) => theme.colors.textSecondary};
line-height: 1.6;
`;
アニメーション効果の実装
テーマ切り替え時にスムーズなアニメーション効果を追加することで、ユーザー体験を向上させます。
typescript// src/components/ThemeToggle.tsx
import styled, { keyframes } from '@emotion/styled';
import { useThemeContext } from '../themes/ThemeContext';
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const ToggleButton = styled.button`
background: none;
border: none;
cursor: pointer;
padding: ${({ theme }) => theme.spacing.sm};
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
color: ${({ theme }) => theme.colors.text};
&:hover {
background-color: ${({ theme }) => theme.colors.border};
transform: scale(1.1);
}
&:active {
transform: scale(0.95);
}
`;
const Icon = styled.span<{ isAnimating: boolean }>`
font-size: 1.5rem;
animation: ${({ isAnimating }) =>
isAnimating ? `${rotate} 0.5s ease` : 'none'};
`;
export const ThemeToggle: React.FC = () => {
const { themeMode, toggleTheme } = useThemeContext();
const [isAnimating, setIsAnimating] = useState(false);
const handleToggle = () => {
setIsAnimating(true);
toggleTheme();
setTimeout(() => {
setIsAnimating(false);
}, 500);
};
return (
<ToggleButton
onClick={handleToggle}
aria-label='テーマを切り替え'
>
<Icon isAnimating={isAnimating}>
{themeMode === 'light' ? '🌙' : '☀️'}
</Icon>
</ToggleButton>
);
};
実践的な実装例
ボタン、カード、ナビゲーションの例
実際のプロジェクトで使用できるコンポーネントの例をご紹介します。
typescript// src/components/Navigation.tsx
import styled from '@emotion/styled';
import { ThemeToggle } from './ThemeToggle';
const Nav = styled.nav`
background-color: ${({ theme }) => theme.colors.surface};
border-bottom: 1px solid ${({ theme }) =>
theme.colors.border};
padding: ${({ theme }) => theme.spacing.md} 0;
position: sticky;
top: 0;
z-index: 1000;
backdrop-filter: blur(10px);
background-color: ${({ theme }) =>
theme.colors.surface}ee;
`;
const NavContainer = styled.div`
max-width: 1200px;
margin: 0 auto;
padding: 0 ${({ theme }) => theme.spacing.lg};
display: flex;
align-items: center;
justify-content: space-between;
`;
const Logo = styled.h1`
color: ${({ theme }) => theme.colors.primary};
margin: 0;
font-size: 1.5rem;
font-weight: 700;
`;
const NavItems = styled.div`
display: flex;
align-items: center;
gap: ${({ theme }) => theme.spacing.md};
`;
const NavLink = styled.a`
color: ${({ theme }) => theme.colors.text};
text-decoration: none;
padding: ${({ theme }) => theme.spacing.sm} ${({
theme,
}) => theme.spacing.md};
border-radius: ${({ theme }) => theme.borderRadius.md};
transition: all 0.2s ease;
&:hover {
background-color: ${({ theme }) => theme.colors.border};
color: ${({ theme }) => theme.colors.primary};
}
`;
export const Navigation: React.FC = () => {
return (
<Nav>
<NavContainer>
<Logo>MyApp</Logo>
<NavItems>
<NavLink href='#home'>ホーム</NavLink>
<NavLink href='#about'>概要</NavLink>
<NavLink href='#contact'>お問い合わせ</NavLink>
<ThemeToggle />
</NavItems>
</NavContainer>
</Nav>
);
};
フォーム要素のテーマ対応
フォーム要素もテーマに合わせてスタイリングすることで、一貫性のある UI を実現できます。
typescript// src/components/styled/Form.tsx
import styled from '@emotion/styled';
export const Form = styled.form`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing.md};
`;
export const FormGroup = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing.xs};
`;
export const Label = styled.label`
color: ${({ theme }) => theme.colors.text};
font-weight: 500;
font-size: 0.9rem;
`;
export const Input = styled.input`
padding: ${({ theme }) => theme.spacing.sm} ${({
theme,
}) => theme.spacing.md};
border: 1px solid ${({ theme }) => theme.colors.border};
border-radius: ${({ theme }) => theme.borderRadius.md};
background-color: ${({ theme }) =>
theme.colors.background};
color: ${({ theme }) => theme.colors.text};
font-size: 1rem;
transition: all 0.2s ease;
&:focus {
outline: none;
border-color: ${({ theme }) => theme.colors.primary};
box-shadow: 0 0 0 3px ${({ theme }) =>
theme.colors.primary}33;
}
&::placeholder {
color: ${({ theme }) => theme.colors.textSecondary};
}
`;
export const TextArea = styled.textarea`
padding: ${({ theme }) => theme.spacing.sm} ${({
theme,
}) => theme.spacing.md};
border: 1px solid ${({ theme }) => theme.colors.border};
border-radius: ${({ theme }) => theme.borderRadius.md};
background-color: ${({ theme }) =>
theme.colors.background};
color: ${({ theme }) => theme.colors.text};
font-size: 1rem;
font-family: inherit;
resize: vertical;
min-height: 100px;
transition: all 0.2s ease;
&:focus {
outline: none;
border-color: ${({ theme }) => theme.colors.primary};
box-shadow: 0 0 0 3px ${({ theme }) =>
theme.colors.primary}33;
}
&::placeholder {
color: ${({ theme }) => theme.colors.textSecondary};
}
`;
export const Select = styled.select`
padding: ${({ theme }) => theme.spacing.sm} ${({
theme,
}) => theme.spacing.md};
border: 1px solid ${({ theme }) => theme.colors.border};
border-radius: ${({ theme }) => theme.borderRadius.md};
background-color: ${({ theme }) =>
theme.colors.background};
color: ${({ theme }) => theme.colors.text};
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
&:focus {
outline: none;
border-color: ${({ theme }) => theme.colors.primary};
box-shadow: 0 0 0 3px ${({ theme }) =>
theme.colors.primary}33;
}
option {
background-color: ${({ theme }) =>
theme.colors.background};
color: ${({ theme }) => theme.colors.text};
}
`;
レスポンシブデザインとの組み合わせ
テーマシステムとレスポンシブデザインを組み合わせることで、あらゆるデバイスで美しい UI を提供できます。
typescript// src/components/styled/Grid.tsx
import styled from '@emotion/styled';
interface GridProps {
columns?: number;
gap?: string;
}
export const Grid = styled.div<GridProps>`
display: grid;
grid-template-columns: repeat(
${({ columns = 1 }) => columns},
1fr
);
gap: ${({ gap, theme }) => gap || theme.spacing.lg};
@media (max-width: 768px) {
grid-template-columns: 1fr;
gap: ${({ theme }) => theme.spacing.md};
}
`;
export const Container = styled.div`
max-width: 1200px;
margin: 0 auto;
padding: 0 ${({ theme }) => theme.spacing.lg};
@media (max-width: 768px) {
padding: 0 ${({ theme }) => theme.spacing.md};
}
`;
export const Flex = styled.div<{
direction?: 'row' | 'column';
align?: string;
justify?: string;
}>`
display: flex;
flex-direction: ${({ direction = 'row' }) => direction};
align-items: ${({ align = 'stretch' }) => align};
justify-content: ${({ justify = 'flex-start' }) =>
justify};
gap: ${({ theme }) => theme.spacing.md};
@media (max-width: 768px) {
flex-direction: ${({ direction }) =>
direction === 'row' ? 'column' : direction};
}
`;
パフォーマンス最適化
テーマ切り替え時の再レンダリング対策
テーマ切り替え時に不要な再レンダリングが発生しないよう、適切な最適化を行います。
typescript// src/hooks/useMemoizedTheme.ts
import { useMemo } from 'react';
import { useThemeContext } from '../themes/ThemeContext';
export const useMemoizedTheme = () => {
const { theme, themeMode, toggleTheme, setThemeMode } =
useThemeContext();
// テーマオブジェクトをメモ化して再レンダリングを防ぐ
const memoizedTheme = useMemo(
() => theme,
[
theme.colors,
theme.spacing,
theme.borderRadius,
theme.shadows,
]
);
return {
theme: memoizedTheme,
themeMode,
toggleTheme,
setThemeMode,
};
};
CSS 変数との併用テクニック
CSS 変数と Emotion を併用することで、パフォーマンスを向上させつつ、柔軟なテーマシステムを構築できます。
typescript// src/themes/cssVariables.ts
import { css } from '@emotion/react';
import { Theme } from './types';
export const createCSSVariables = (theme: Theme) => css`
:root {
--color-primary: ${theme.colors.primary};
--color-secondary: ${theme.colors.secondary};
--color-background: ${theme.colors.background};
--color-surface: ${theme.colors.surface};
--color-text: ${theme.colors.text};
--color-text-secondary: ${theme.colors.textSecondary};
--color-border: ${theme.colors.border};
--color-error: ${theme.colors.error};
--color-success: ${theme.colors.success};
--color-warning: ${theme.colors.warning};
--spacing-xs: ${theme.spacing.xs};
--spacing-sm: ${theme.spacing.sm};
--spacing-md: ${theme.spacing.md};
--spacing-lg: ${theme.spacing.lg};
--spacing-xl: ${theme.spacing.xl};
--border-radius-sm: ${theme.borderRadius.sm};
--border-radius-md: ${theme.borderRadius.md};
--border-radius-lg: ${theme.borderRadius.lg};
--shadow-sm: ${theme.shadows.sm};
--shadow-md: ${theme.shadows.md};
--shadow-lg: ${theme.shadows.lg};
}
`;
// CSS変数を使用したコンポーネント
export const OptimizedButton = styled.button`
background-color: var(--color-primary);
color: white;
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: var(--border-radius-md);
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: var(--color-primary);
opacity: 0.9;
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
`;
メモ化による最適化
React.memo と useMemo を活用して、不要な再レンダリングを防ぎます。
typescript// src/components/OptimizedCard.tsx
import React, { memo } from 'react';
import styled from '@emotion/styled';
interface CardProps {
title: string;
content: string;
elevation?: 'low' | 'medium' | 'high';
}
const StyledCard = styled.div<{ elevation: string }>`
background-color: ${({ theme }) => theme.colors.surface};
border: 1px solid ${({ theme }) => theme.colors.border};
border-radius: ${({ theme }) => theme.borderRadius.lg};
padding: ${({ theme }) => theme.spacing.lg};
box-shadow: ${({ theme, elevation }) => {
switch (elevation) {
case 'high':
return theme.shadows.lg;
case 'medium':
return theme.shadows.md;
default:
return theme.shadows.sm;
}
}};
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: ${({ theme }) => theme.shadows.lg};
}
`;
const CardTitle = styled.h3`
color: ${({ theme }) => theme.colors.text};
margin: 0 0 ${({ theme }) => theme.spacing.sm} 0;
font-size: 1.25rem;
font-weight: 600;
`;
const CardContent = styled.p`
color: ${({ theme }) => theme.colors.textSecondary};
margin: 0;
line-height: 1.6;
`;
export const OptimizedCard = memo<CardProps>(
({ title, content, elevation = 'low' }) => {
return (
<StyledCard elevation={elevation}>
<CardTitle>{title}</CardTitle>
<CardContent>{content}</CardContent>
</StyledCard>
);
}
);
OptimizedCard.displayName = 'OptimizedCard';
よくあるエラーと解決方法
Emotion でテーマ実装を行う際によく遭遇するエラーとその解決方法をご紹介します。
エラー 1: ThemeProvider が見つからない
bashError: ThemeProvider not found. Please ensure that you are using @emotion/react's ThemeProvider
解決方法:
typescript// App.tsx
import { ThemeProvider } from '@emotion/react';
import { ThemeProvider as CustomThemeProvider } from './themes/ThemeContext';
function App() {
return (
<CustomThemeProvider>
<ThemeProvider theme={theme}>
{/* アプリケーションの内容 */}
</ThemeProvider>
</CustomThemeProvider>
);
}
エラー 2: テーマオブジェクトの型エラー
bashType 'Theme' is not assignable to type 'Theme' from '@emotion/react'
解決方法:
typescript// src/themes/types.ts
import '@emotion/react';
declare module '@emotion/react' {
export interface Theme {
colors: {
primary: string;
secondary: string;
background: string;
surface: string;
text: string;
textSecondary: string;
border: string;
error: string;
success: string;
warning: string;
};
spacing: {
xs: string;
sm: string;
md: string;
lg: string;
xl: string;
};
borderRadius: {
sm: string;
md: string;
lg: string;
};
shadows: {
sm: string;
md: string;
lg: string;
};
}
}
エラー 3: スタイルが適用されない
bashWarning: React does not recognize the `theme` prop on a DOM element
解決方法:
typescript// 正しい書き方
const Button = styled.button`
background-color: ${({ theme }) => theme.colors.primary};
`;
// 間違った書き方
const Button = styled.button<{ theme: Theme }>`
background-color: ${({ theme }) => theme.colors.primary};
`;
まとめ
Emotion を使ったダークモード・テーマ切替の実装について、実装手法に焦点を当てて詳しく解説しました。
今回学んだポイントを振り返ると、テーマシステムの設計思想が最も重要です。適切なテーマオブジェクトの構造設計により、一貫性のある UI を構築でき、ユーザー体験を大幅に向上させることができます。
また、パフォーマンス最適化も忘れてはいけません。メモ化や CSS 変数の活用により、スムーズなテーマ切り替えを実現できます。
Emotion の魅力は、その柔軟性と開発体験にあります。JavaScript の力を使って動的なスタイリングが可能で、型安全性も確保できます。
今回の実装例を参考に、あなたのプロジェクトでも美しいテーマシステムを構築してみてください。ユーザーの心に響く、使いやすいアプリケーションがきっと作れるはずです。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来