theming について
theming は、Expo Router アプリに共通のセマンティックカラーシステムを導入するスキルです。プラットフォーム別の色解決やライト・ダーク切り替えに対応し、ナビゲーションテーマの統合方法も案内します。
主な機能
- 01共通のセマンティックカラートークン
- 02iOS・Android のネイティブカラー解決
- 03ライト・ダークモードへの自動対応
- 04カラー・ブランド用フック
- 05ナビゲーションテーマ統合
活用シーン
- 01Expo Router アプリの色の統一
- 02ナビゲーション表示とアプリカラーの連動
- 03ライト・ダーク表示への対応
- 04ネイティブカラーまたは固定値の利用
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ code-with-beto/ skills --skill themingインストール後、Claude Code で/themingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/colors.tsconfig.tsThemeContext.tsx詳細
Expo 統合テーマ設定
iOS と Android の両方で同時に使える単一のカラーパレットを設定します。アプリ全体に Color.ios.* や Color.android.dynamic.*(またはハードコードされた 16 進数カラー)を散りばめる代わりに、ユーザーはセマンティックトークン(background、text、link など)を使います。これらはプラットフォームごとに適切なネイティブカラーに解決され、ダーク/ライトに自動で切り替わり、単一の useColors() フックから読み取れます。
これは Platano テンプレートのパターンを、あらゆる Expo Router アプリに導入できるようパッケージ化したものです。
インストールされるもの
3 つのファイル(assets/ に含まれています。プロジェクトに合わせて調整してください):
config.ts— 唯一の信頼できる情報源です。セマンティックシステムトークンを"native"または 16 進数カラーに設定し、ブランドカラーも定義します。日々編集するのはこのファイルだけです。colors.ts— トークンをプラットフォームごとの実際のカラーに解決し、getSystemColor、useSystemColors、ブランドヘルパー、getNavigationThemeを公開します。ThemeContext.tsx— React のuseを使って構築されたColorsProviderと、useColors()/useTheme()/useBrand()フックです。
さらに、プロバイダーを接続するためにルートの _layout.tsx を少し編集します。
これを機能させる 2 つの考え方
ユーザーには次の点を説明してください。これがこの仕組みの要点であり、間違えやすい 2 つのポイントです。
-
テーマ変更時に再レンダリングする。 ネイティブカラーと Material You カラーはプラットフォームのカラーオブジェクトであり、React が差分を検出できる文字列ではありません。テーマが変わったことを知らせるのは
useColorScheme()です。そのため、リゾルバーは値を参照しない場合でも、引数としてカラースキームを受け取ります。これによりダーク/ライトが明示的な依存関係となり、ユーザーが切り替えたときに値が再計算されます。ColorsProviderはルート付近でuseColorScheme()を一度読み取るため、切り替えによってすべてのコンシューマーが再レンダリングされます。 -
2 つの ThemeProvider は役割が異なる。
ColorsProvider(このスキル)は、コンポーネントにuseColors()を提供します。ナビゲーションのThemeProviderはナビゲーションの UI(ヘッダー、タブバー、デフォルトの背景)にテーマを適用します。どちらもルートに配置します。ナビゲーション側はgetNavigationTheme(dark)で接続し、UI をトークンに合わせてください。混同したり、どちらかを省いたりしないでください。Expo SDK 56 以降では、ナビゲーションのテーマ関連要素(ThemeProvider、DarkTheme、DefaultTheme、Theme)をexpo-router/react-navigationからインポートします。それ以前の SDK では、代わりに@react-navigation/nativeからインポートします(ステップ 1 を参照)。
ステップ 1:プロジェクト、SDK、ネイティブの Color API を確認する
このスキルは Expo SDK 56 以降の Expo Router アプリを対象とします。SDK 56 では、テンプレートが使う Color API(iOS のシステムカラーと Android の Material You)と expo-router/react-navigation による再エクスポートの両方が導入されました。"native" トークンを使う前に、SDK と Color API を確認してください。
# Find the project's app dir and root layout (root-level or under src/)
ls app/_layout.tsx src/app/_layout.tsx 2>/dev/null
# Check the installed Expo SDK major version
node -e "console.log('expo', require('expo/package.json').version)" 2>/dev/null
# Confirm expo-router exports the Color API
node -e "console.log('Color export:', !!require('expo-router').Color)" 2>/dev/null
grep -rl "android.dynamic\|ios.systemBackground" node_modules/expo-router/build 2>/dev/null | head -1
確認できた内容に応じて分岐します。
- SDK 56 以降(対象): テンプレートをそのまま使用します。ナビゲーションのテーマ関連要素は
expo-router/react-navigationからインポートします。 - SDK 56 未満: 2 つの調整が必要です。
colors.tsと_layout.tsxの両方で、ThemeProvider、DarkTheme、DefaultTheme、type Themeをexpo-router/react-navigationではなく@react-navigation/nativeからインポートします。(以前の Expo Router はこれらを再エクスポートしていないため、React Navigation から直接インポートします。)ColorAPI が存在しない可能性があります。SDK 56 へのアップグレードを勧めるか、16 進数カラーのフォールバックを使います。ファイルはそのままにして、config.tsのsystemトークンを"native"ではなく 16 進数カラーに設定します(Material You は使えませんが、それ以外はすべて動作します)。getBrandColorsは引き続き動作し、Colorがない場合はisAndroidDynamicがfalseに解決されます。
また、tsconfig.json からプロジェクトのインポートエイリアスを検出します(一般的には @/* → src/* またはプロジェクトのルートです)。インポートを書く際は、そのエイリアスに合わせてください。エイリアスがない場合は、相対パスを使います。
ステップ 2:ファイルの配置場所を決める
3 つのファイルを 1 つのフォルダーにまとめて配置し、相対インポート
(./config、./colors)がそのまま機能するようにします。プロジェクトの構成に合わせてください。
- アプリが
src/app/にある場合 →src/theme/を作成します。 - アプリがルートの
app/にある場合 → ルートにtheme/を作成します。
既存のテーマ/カラーのモジュールや ThemeProvider がないか、先に確認してください。すでにアプリにテーマ設定がある場合は、上書きしないでください。既存の内容を確認し、重複する部分をユーザーに説明してから、既存の構成に統合するか、これらのファイルを名前で用途が明確なフォルダーに配置し、ユーザー自身で移行できるようにします。決して確認せずに上書きしないでください。
ステップ 3:ファイルを作成する
同梱の 3 つのテンプレートを選んだフォルダーにコピーし、次の点を調整します。
- インポートエイリアス/相対パスをプロジェクトに合わせます。
config.tsのsystemトークンとbrandカラーをユーザーのブランドに合わせます(ブランドのプライマリカラー/アクセントカラーを尋ねるか、適切なデフォルト値を使い、変更箇所を伝えます)。
各アセットを読み込み、プロジェクト内に書き込んでください(シンボリックリンクは使わないでください。これらはユーザー自身のファイルになります)。
assets/config.ts→<theme>/config.tsassets/colors.ts→<theme>/colors.tsassets/ThemeContext.tsx→<theme>/ThemeContext.tsx
コメントは残してください。再レンダーの仕組みやブランド/動的な動作について説明しており、学習者が自身のコードベースで使うために必要な内容です。
ステップ 4:ルートレイアウトを接続する
既存の _layout.tsx を最小限の変更で編集し、既存のプロバイダー、画面、オプションをすべて維持してください。目標とする構成では、ツリーを ColorsProvider で囲み、カラースキームを読み取る内部コンポーネントからナビゲーションにテーマを適用します。
// SDK 56+: from "expo-router/react-navigation". Older SDKs: "@react-navigation/native".
import { ThemeProvider } from "expo-router/react-navigation";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useColorScheme } from "react-native";
import { getBrandColors, getNavigationTheme } from "@/theme/colors";
import { ColorsProvider } from "@/theme/ThemeContext";
export default function RootLayout() {
return (
<ColorsProvider>
{/* keep any existing providers (QueryClient, RevenueCat, gestures...) here */}
<RootLayoutInner />
</ColorsProvider>
);
}
function RootLayoutInner() {
const dark = useColorScheme() === "dark";
const { primary } = getBrandColors(dark);
return (
<>
<ThemeProvider value={getNavigationTheme(dark, primary)}>
{/* keep the project's existing <Stack> / screens exactly as they were */}
<Stack />
</ThemeProvider>
<StatusBar style={dark ? "light" : "dark"} />
</>
);
}
統合ルール:
ColorsProviderは、useColors()を呼び出すものより上位に配置する必要があります。RootLayoutの最上部か、その近くに置くのが最も安全です。- ファイルがすでに
RootLayoutと内部コンポーネントに分かれている場合は、新しい内部コンポーネントを作らず、そこにThemeProviderとuseColorScheme()を追加します。 expo-status-barがインストールされておらず、プロジェクトでステータスバーをすでに管理していない場合は、StatusBarの行を省略してかまいません。
ステップ 5:検証する
- 利用できる場合は型チェックを実行します:
npx tsc --noEmit(3 つのファイルに新たなエラーがないことを確認します。最もよくある問題は、インポートエイリアスの誤りです)。 - アプリを実行してから、OS の外観設定(またはシミュレーターのダーク/ライト設定)を切り替え、カラーとナビゲーションの表示がリアルタイムで更新されることをユーザーに確認してもらいます。Android の実機では、壁紙を変更すると Material You のパレットが変わります。
ステップ 6:使い方を示す
ユーザーに短い使用例を示します(すぐに動作を確認できるよう、必要であれば画面の 1 つにテーマを適用した例を追加することも提案してください)。
import { View, Text } from "react-native";
import { useColors, useBrand } from "@/theme/ThemeContext";
export function Example() {
const { background, text, secondaryText, separator } = useColors();
const { primary } = useBrand();
return (
<View style={{ flex: 1, backgroundColor: background, padding: 16 }}>
<Text style={{ color: text, fontSize: 20, fontWeight: "600" }}>
Unified theming
</Text>
<Text style={{ color: secondaryText }}>
Same tokens on iOS and Android.
</Text>
<View style={{ height: 1, backgroundColor: separator, marginVertical: 12 }} />
<Text style={{ color: primary }}>Brand accent</Text>
</View>
);
}
React の外部(トーストや単発のスタイルなど)では、フックを使わないアクセサーを使用します。
import { getSystemColor } from "@/theme/colors";
const card = getSystemColor("secondaryBackground") as string;
カスタマイズ
- すべての箇所のカラーを変更する:
config.tsを編集します。トークンに 16 進数カラーを設定すると固定され、"native"に戻すと OS に従います。 - Android でブランドカラーに Material You を使用しない:
brand.useAndroidDynamic: falseに設定すると、すべてのプラットフォームでブランドカラーの 16 進数値が適用されます。 - トークンを追加する:
config.tsのsystemに追加し、colors.tsのgetNativeDefaultに iOS/Android/デフォルトの各エントリーを追加したうえで、resolveSystemColorsにも 1 行追加します。抜けているものがあれば TypeScript が知らせてくれます。
注意事項
- これはカラーとブランドのみを設定します。余白や角丸のプリセット、永続化機能は意図的に追加していません。ユーザーから求められない限り、範囲を広げないでください。
- これらのファイルはユーザー自身のコードになります。ブラックボックスとして扱わず、コメントを読み、調整するよう促してください。このスキルが提供するレッスンの目的は、ユーザーが学べるようにすることです。