◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-03-14初級

Rork Max ダークモード・テーマ設計ガイド — アプリの見た目を自在にカスタマイズ

Rork Maxで作るアプリのダークモード対応とテーマ設計を解説します。デザイントークンの基礎、React Contextによるテーマ管理、ユーザー設定の永続化、カスタムテーマの作成まで、目に優しく省電力にも効くモダンなUIをコード付きで実装していきます。

rork-max41themingdark-modedesign-tokensui-design3

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 の収益ダッシュボードに異常検知が出ていないか
  • ユーザーレビューに技術的な指摘が含まれていないか

まとめ — 次の改善サイクル

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⬡ 開発ツール2026-10-15
EXPO_PUBLIC_ の値が、開発では読めて本番だけ undefined になる書き方を、変換結果で見分けます
API の URL が開発中は読めるのに、ビルドした途端に undefined になる。原因は process.env の書き方でした。babel-preset-expo の変換結果を開発・本番で並べ、置き換わる書き方と残ってしまう書き方を表にしました。
⬡ 開発ツール2026-10-14
起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、読み込みより先に過ぎていたためです
Android 版の起動広告が、アプリを完全に終了してから開く回だけ一度も出ていませんでした。「前面に戻った」合図を読み込みの完了と切り離し、順番ごとにテストできる形へ組み直した記録です。
⬡ 開発ツール2026-10-13
リリース前に、書き出した Expo バンドルの中から自分の鍵を探してみる
ソースに鍵がなくても、出来上がったアプリの中に鍵が残ることがあります。Expo SDK 57 で実際に expo export を実行し、バンドルの中身を grep と 60 行ほどの Node スクリプトで調べて、見つかる鍵と見つからない鍵の違いを確かめました。