Rork Max ダークモード・テーマ設計ガイド
ユーザーの好みはユーザーごとに異なります。ダークモード対応は、もはやオプション機能ではなく、必須機能となっています。夜間使用時の目の疲労軽減、バッテリー省電力、個人の美的好みなど、ダークモード需要は多岐にわたります。
デザイントークンの基礎
カラーパレット管理
テーマ機能を実装する第一歩は、デザイントークン(カラー、フォントサイズなど)を一元管理することです。
// theme/tokens.js
const lightTheme = {
colors: {
primary: '#0066CC',
secondary: '#FF6B6B',
background: '#FFFFFF',
surface: '#F5F5F5',
text: '#222222',
textSecondary: '#666666',
border: '#DDDDDD',
success: '#22C55E',
warning: '#FBBF24',
error: '#EF4444',
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
},
fontSize: {
sm: 12,
base: 14,
lg: 16,
xl: 20,
'2xl': 24,
},
};
const darkTheme = {
colors: {
primary: '#4A9EFF',
secondary: '#FF8A9B',
background: '#1A1A1A',
surface: '#2D2D2D',
text: '#FFFFFF',
textSecondary: '#CCCCCC',
border: '#404040',
success: '#4ADE80',
warning: '#FACC15',
error: '#F87171',
},
spacing: lightTheme.spacing,
fontSize: lightTheme.fontSize,
};
export { lightTheme, darkTheme };デザイントークンを集約することで、テーマ切り替えが簡潔になります。
React Context でのテーマ管理
グローバルテーマ状態管理
テーマ状態をアプリケーション全体で共有するには、React Context を使用します。
import { createContext, useState, useContext, useEffect } from 'react';
import { useColorScheme } from 'react-native';
import { lightTheme, darkTheme } from './tokens';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const systemColorScheme = useColorScheme();
const [isDarkMode, setIsDarkMode] = useState(
systemColorScheme === 'dark'
);
// システム設定を監視
useEffect(() => {
if (systemColorScheme) {
setIsDarkMode(systemColorScheme === 'dark');
console.log(`システムテーマ検出: ${systemColorScheme}`);
// 期待出力: システムテーマ検出: dark
}
}, [systemColorScheme]);
const theme = isDarkMode ? darkTheme : lightTheme;
const toggleTheme = () => {
setIsDarkMode(!isDarkMode);
console.log(`テーマ切り替え: ${!isDarkMode ? 'ダーク' : 'ライト'}`);
// 期待出力: テーマ切り替え: ダーク
};
const value = {
isDarkMode,
theme,
toggleTheme,
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}テーマの実装と使用
コンポーネント内でのテーマ適用
テーマトークンをコンポーネントで使用する方法を示します。
import { useTheme } from './ThemeProvider';
import { View, Text, StyleSheet } from 'react-native';
export function ThemedCard({ title, description }) {
const { theme } = useTheme();
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.surface,
borderColor: theme.colors.border,
borderWidth: 1,
borderRadius: 8,
padding: theme.spacing.md,
marginBottom: theme.spacing.md,
},
title: {
color: theme.colors.text,
fontSize: theme.fontSize.lg,
fontWeight: 'bold',
marginBottom: theme.spacing.sm,
},
description: {
color: theme.colors.textSecondary,
fontSize: theme.fontSize.base,
lineHeight: 20,
},
});
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.description}>{description}</Text>
</View>
);
}このアプローチにより、テーマ変更時に自動的にコンポーネントが更新されます。
ユーザープリファレンスの永続化
テーマ設定の保存
ユーザーが選択したテーマをローカルストレージに保存し、次回起動時に復元します。
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useEffect } from 'react';
const THEME_PREFERENCE_KEY = '@rork_theme_preference';
export function usePersistentTheme() {
const { isDarkMode, toggleTheme } = useTheme();
// 初期化時に保存されたテーマを読み込む
useEffect(() => {
loadThemePreference();
}, []);
// テーマ変更時に保存
useEffect(() => {
saveThemePreference(isDarkMode);
}, [isDarkMode]);
const loadThemePreference = async () => {
try {
const savedTheme = await AsyncStorage.getItem(THEME_PREFERENCE_KEY);
if (savedTheme !== null) {
const isDark = savedTheme === 'dark';
if (isDark !== isDarkMode) {
toggleTheme();
}
console.log(`保存されたテーマを読み込み: ${savedTheme}`);
// 期待出力: 保存されたテーマを読み込み: dark
}
} catch (error) {
console.error('テーマ読み込みエラー:', error);
}
};
const saveThemePreference = async (isDark) => {
try {
const themeValue = isDark ? 'dark' : 'light';
await AsyncStorage.setItem(THEME_PREFERENCE_KEY, themeValue);
} catch (error) {
console.error('テーマ保存エラー:', error);
}
};
}ダークモード対応のベストプラクティス
アクセシビリティの考慮
ダークモード実装時は、コントラスト比に注意が必要です。
// コントラスト比が十分であることを確認
const theme = {
colors: {
// Light mode
lightText: '#222222', // 背景#FFFFFF との比: 17:1 (WCAG AAA)
lightBg: '#FFFFFF',
// Dark mode
darkText: '#FFFFFF', // 背景#1A1A1A との比: 16:1 (WCAG AAA)
darkBg: '#1A1A1A',
},
};
export function AccessibleText({ children, isDark }) {
const color = isDark ? theme.colors.darkText : theme.colors.lightText;
return (
<Text style={{ color }}>
{children}
</Text>
);
}WCAG 2.1 のガイドラインでは、通常テキストのコントラスト比は 4.5:1 以上が推奨されます。
カスタムテーマの作成
ユーザー定義テーマ
上級ユーザーのために、カスタムテーマ機能を提供できます。
import { useState } from 'react';
export function useCustomTheme() {
const [customThemes, setCustomThemes] = useState([]);
const createCustomTheme = (name, colors) => {
const newTheme = {
id: Date.now(),
name,
colors: {
primary: colors.primary,
secondary: colors.secondary,
background: colors.background,
surface: colors.surface,
text: colors.text,
textSecondary: colors.textSecondary,
},
};
setCustomThemes([...customThemes, newTheme]);
console.log(`カスタムテーマ作成: ${name}`);
// 期待出力: カスタムテーマ作成: MyCyan
return newTheme;
};
const deleteCustomTheme = (themeId) => {
setCustomThemes(customThemes.filter(t => t.id !== themeId));
};
return {
customThemes,
createCustomTheme,
deleteCustomTheme,
};
}
export function CustomThemeEditor() {
const [themeName, setThemeName] = useState('');
const [primaryColor, setPrimaryColor] = useState('#0066CC');
const { createCustomTheme } = useCustomTheme();
const handleCreate = () => {
if (themeName) {
createCustomTheme(themeName, {
primary: primaryColor,
secondary: '#FF6B6B',
background: '#FFFFFF',
surface: '#F5F5F5',
text: '#222222',
textSecondary: '#666666',
});
setThemeName('');
}
};
return (
<div style={{ padding: '20px' }}>
<input
type="text"
value={themeName}
onChange={(e) => setThemeName(e.target.value)}
placeholder="テーマ名"
/>
<input
type="color"
value={primaryColor}
onChange={(e) => setPrimaryColor(e.target.value)}
/>
<button onClick={handleCreate}>テーマを作成</button>
</div>
);
}テーマ情報の継承
ダークモード実装後、カメラ・ギャラリー機能でのUI 統一についてはRork Max カメラ・ギャラリー機能ガイドを参照してください。
また、デザインシステムの統一のため、Figma との連携方法についてはRork Max Figma 連携ガイドもご覧ください。さらに、アニメーション効果をダークモード対応させる方法についてはRork Max アニメーション・ジェスチャーガイドを参照してください。
12年の個人開発で見えてきたこと
リリース直後にチェックしているもの
- クラッシュレポートの上位エラーを24時間ごとに確認しているか
- AdMob/StoreKit の収益ダッシュボードに異常検知が出ていないか
- ユーザーレビューに技術的な指摘が含まれていないか
12年の個人開発から見る Rork の使いどころ
リリース直後にチェックしているもの
- クラッシュレポートの上位エラーを24時間ごとに確認しているか
- AdMob/StoreKit の収益ダッシュボードに異常検知が出ていないか
- ユーザーレビューに技術的な指摘が含まれていないか