dark-mode について
dark-modeは、ダークテーマを実装するデザイナーやフロントエンド開発者向けのスキルです。色、サーフェス階層、コントラスト、実装方法を扱い、単純な色反転を避けます。
主な機能
- 01デザインコンテキストからダークテーマを導出
- 02サーフェス、文字、境界線の階層を定義
- 03彩度を調整しコントラストを確認
- 04CSS、テーマ切り替え、Tailwindの方式
- 05テーマのちらつき、動き、描画結果を確認
活用シーン
- 01既存のライトUIにダークテーマを追加
- 02システム設定または手動切り替えに対応
- 03ダークUIの文字やコントロールのコントラスト確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ arnavpuri/ designskills --skill dark-modeインストール後、Claude Code で/dark-modeを入力して読み込まれたか確認します。
詳細
ダークモードのデザイン
始める前に:デザインコンテキストを読み込む
.agents/design-context.mdを読みます(design-contextスキルによって作成されています)。存在しない場合は、tailwind.config.*または:rootの CSS 変数からトークンを探します。何も見つからない場合は、デフォルト(Primary#2563EB、Secondary#7C3AED、Accent#F59E0B、Tailwind のグレーニュートラル、見出しと本文には Inter、スタイルは Minimal)を使用し、デフォルトを使用したことをユーザーに伝えます —design-contextの実行を提案してください。- トークンを CSS 変数に一度だけ対応付け、以降は変数のみを使用します:
--color-primary、--color-primary-light、--color-primary-dark、--color-secondary、--color-accent、--color-neutral-50…--color-neutral-900、--color-success/-warning/-error、--font-heading、--font-body、--font-mono、--radius、および 4px を基準としたスペーシングスケール。 - Tailwind: 同じ変数を
primary、primary-light、primary-dark、secondary、accent、neutral-*、font-heading、font-bodyとして公開します(v3 ではtheme.extend、v4 では@theme)。以下の例にあるindigo-*/gray-*クラスは、これらのトークンを示す仮の値です。 - Figma: ユーザーが Figma の URL を提示した場合は、Figma MCP(
get_design_context、get_screenshot)で取得してデザインを再現します。そのデザインについては、コンテキストファイルより優先されます。 - コンテキストの色はライトテーマ用です。新しいパレットを作るのではなく、セクション 3 に従って明度を上げ、彩度を下げてダークモード用の値を導き出します。ダークモードを先に設計するのか、既存のライト UI にダークモードを追加するのかを確認してください。
1. 基本ルール:単純に反転しない
色を反転すると、刺激が強く読みにくいインターフェースになります。ダークモードでは、意図的なデザイン上の判断が必要です。
変わる要素
| 要素 | ライトモード | ダークモード |
|---|---|---|
| 背景 | 白(#fff) | ダークグレー(純粋な黒ではありません) |
| テキスト | ほぼ黒 | ほぼ白(純粋な白ではありません) |
| 境界線 | Gray-200 | 不透明度 8〜12% の白 |
| 影 | 不透明度 5〜15% の黒 | ほぼ見えない、またはなし |
| プライマリカラー | 彩度を最大に | 彩度を下げる |
| 画像 | 明るさを最大に | やや暗くする |
| 高さ | 影 | より明るいサーフェスカラー |
避けること
- 純粋な黒
#000の背景に純粋な白#fffのテキストを使うこと(コントラスト比 21:1)。多くの読者、特に乱視のある人にとって、ハレーションやまぶしさの原因になります。 - デフォルトの背景に純粋な黒を使うこと:高さの違いを表現できず、OLED ではスクロール中に残像が生じる場合があります。真の黒は、明示的な「OLED/真の黒」オプション用に取っておきます。
- ライトモードと同じ影の値を使うこと — ダークなサーフェスでは影がほとんど見えません。
2. 輝度による高さの表現
ライトモードでは影で高さを示します。ダークモードでは、高さのあるサーフェスをより明るくします。
サーフェスの階層
:root {
/* Light mode */
--bg-base: oklch(100% 0 0); /* white */
--bg-surface: oklch(100% 0 0); /* white */
--bg-elevated: oklch(100% 0 0); /* white + shadow */
--bg-overlay: oklch(100% 0 0); /* white + deeper shadow */
}
[data-theme="dark"] {
/* Dark mode — each level gets progressively lighter */
--bg-base: oklch(13% 0.01 265); /* darkest: page background */
--bg-surface: oklch(18% 0.01 265); /* cards, sidebar */
--bg-elevated: oklch(23% 0.01 265); /* dropdowns, popovers */
--bg-overlay: oklch(28% 0.01 265); /* modals, tooltips */
}
実用例
.card {
background: var(--bg-surface);
border: 1px solid var(--border);
}
.dropdown {
background: var(--bg-elevated);
border: 1px solid var(--border);
}
.modal {
background: var(--bg-overlay);
}
Material Design のオーバーレイモデル(M2)
高さが増すごとに、#121212 のベースに白のオーバーレイを重ねます:
- 0dp(ベース):0% · 1dp(カード):5% · 4dp(アプリバー):9%
- 8dp(メニュー):12% · 16dp(ナビゲーションドロワー):15% · 24dp(ダイアログ):16%
Material 3 ではオーバーレイを、色調に基づく surface-container のロール(最も低いものから最も高いものへ)に置き換えています。上記のサーフェストークンも同じ考え方に沿っています。
3. 色の彩度に関するルール
ダークな背景では彩度を下げる
ダークな背景に明るく彩度の高い色を使うと、視覚的なちらつきや眼精疲労の原因になります。
:root {
--color-primary: oklch(60% 0.25 265); /* Light mode: full saturation */
--color-success: oklch(55% 0.20 155);
--color-error: oklch(55% 0.22 25);
}
[data-theme="dark"] {
--color-primary: oklch(72% 0.18 265); /* Dark mode: lighter, less saturated */
--color-success: oklch(70% 0.15 155);
--color-error: oklch(70% 0.16 25);
}
ルール
- ダークモードでは明度を 10〜15% 上げます。
- 彩度を 15〜25% 下げます。
- バッジなど、色の付いた背景には不透明度を低く設定します。
- 調整後にコントラストを再確認します。白い背景ではコントラスト要件を満たすプライマリカラーでも、ダークモードで白いテキストを載せると要件を満たさないことがよくあります。代わりに、塗りつぶしボタンでは、より明るいプライマリカラーにダークなテキストを使います(セクション 8 を参照)。
/* Light mode badge */
.badge-success {
background: oklch(95% 0.05 155);
color: oklch(35% 0.15 155);
}
/* Dark mode badge */
[data-theme="dark"] .badge-success {
background: oklch(35% 0.08 155);
color: oklch(80% 0.12 155);
}
4. テキストのコントラストレベル
暗い背景向けの白色不透明度スケール
暗い背景でテキストに階層を付けるには、不透明度を変えた白を使用します。
| 役割 | 不透明度 | 使用例 |
|---|---|---|
| 強調度:高 | 87–90% (oklch(100% 0 0 / 0.87)) | 見出し、主要なテキスト |
| 強調度:中 | 60–70% (oklch(100% 0 0 / 0.70)) | 本文、補助的なテキスト、控えめなテキスト |
| 無効状態のみ | 38% (oklch(100% 0 0 / 0.38)) | 無効化されたコントロール(コントラスト規則の対象外) |
控えめなキャプションや情報を含むプレースホルダーなど、ユーザーが読む必要のあるものはすべて、背景面に対して 4.5:1 のコントラスト比が必要です。一般的な暗い背景では、白の不透明度が約55%を下回ると基準を満たしません。
実装
以下の完全なトークンシステムにある --text-primary / --text-secondary / --text-muted トークンを使用します(--text-muted は明度62%で、ベースカラーと背景面の色に対して4.5:1を保てる最低値です)。
Tailwind での方法
<h1 class="text-white/90">Main Heading</h1>
<p class="text-white/70">Body text content here.</p>
<span class="text-white/60">Last updated 2 hours ago</span>
5. ボーダーと区切り線の扱い
暗い背景での明るいボーダー
ダークモードのボーダーにはグレー値ではなく、半透明の白を使用します。
[data-theme="dark"] {
--border: oklch(100% 0 0 / 0.08); /* subtle */
--border-strong: oklch(100% 0 0 / 0.15); /* emphasized */
--divider: oklch(100% 0 0 / 0.06); /* section dividers */
}
これにより、ボーダーは下地となる背景面の色にかかわらず、適切に表示されます。装飾的な区切り線はこれほど薄くてもかまいませんが、入力欄とコントロールのボーダーは背景面に対して3:1 のコントラスト比が必要です(WCAG 1.4.11)。フォームフィールドには oklch(100% 0 0 / 0.3) 以上の濃さを使用してください。
6. ダークモードでの画像の扱い
画像の明るさを下げる
[data-theme="dark"] img:not([data-no-dim]) {
filter: brightness(0.85);
}
/* Restore on hover for galleries */
[data-theme="dark"] img:hover {
filter: brightness(1);
}
ヒーロー画像に暗いオーバーレイを適用する
<div class="relative">
<img src="/hero.jpg" alt="" class="w-full" />
<div class="absolute inset-0 bg-gray-950/30 dark:bg-gray-950/50"></div>
</div>
SVG アイコンの適応
アイコンには currentColor を使用し、テキストの色を自動的に継承させます。
<svg class="w-5 h-5 text-gray-600 dark:text-gray-400" fill="currentColor">
</svg>
ロゴの扱い
ライトモード用とダークモード用に別々のロゴを用意します。
<img src="/logo-dark.svg" alt="Logo" class="dark:hidden" />
<img src="/logo-light.svg" alt="Logo" class="hidden dark:block" />
7. CSS による実装
常に設定する:color-scheme プロパティ
ブラウザーにネイティブコントロール、スクロールバー、フォームフィールド、デフォルトのキャンバスを適切なテーマで表示するよう指示します。これを設定しないと、ダークモードでも <select> やスクロールバーが明るいままになります。
:root { color-scheme: light dark; } /* follow the system */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }
また、CSS の読み込み前にページの背景色を正しく設定できるよう、<head> に <meta name="color-scheme" content="light dark"> も追加します。
方法 1:prefers-color-scheme(システム設定)
:root {
--bg: oklch(100% 0 0);
--text: oklch(15% 0 0);
--surface: oklch(100% 0 0);
--border: oklch(90% 0 0);
}
@media (prefers-color-scheme: dark) {
/* :not() lets a manual "light" choice (Method 2) override the system */
:root:not([data-theme="light"]) {
--bg: oklch(13% 0.01 265);
--text: oklch(95% 0 0);
--surface: oklch(18% 0.01 265);
--border: oklch(100% 0 0 / 0.08);
}
}
方法 2:CSS カスタムプロパティとクラスの切り替え(ユーザー設定)
:root {
--bg: oklch(100% 0 0);
--text: oklch(15% 0 0);
}
[data-theme="dark"] {
--bg: oklch(13% 0.01 265);
--text: oklch(95% 0 0);
}
body {
background: var(--bg);
color: var(--text);
}
// Toggle implementation — keeps data-theme, the Tailwind `dark` class, and the button state in sync
function applyTheme(theme) {
const root = document.documentElement;
root.setAttribute('data-theme', theme);
root.classList.toggle('dark', theme === 'dark');
document.querySelectorAll('[data-theme-toggle]')
.forEach((btn) => btn.setAttribute('aria-pressed', String(theme === 'dark')));
}
function toggleTheme() {
const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
applyTheme(next);
try { localStorage.setItem('theme', next); } catch (e) { /* storage blocked: session-only */ }
}
// Follow system changes until the user makes an explicit choice
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
let saved = null;
try { saved = localStorage.getItem('theme'); } catch (e) {}
if (!saved) applyTheme(e.matches ? 'dark' : 'light');
});
初期テーマは、読み込み後に実行される関数ではなく、以下のインライン <head> スクリプトで設定します。3択のコントロール(ライト/ダーク/システム)を検討してください。「システム」を選ぶと、保存済みの値がクリアされます。
方法 3:Tailwind の dark: バリアント
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100
border-gray-200 dark:border-white/10">
<h2 class="text-gray-950 dark:text-white">Title</h2>
<p class="text-gray-600 dark:text-gray-400">Description</p>
</div>
クラスベースのダークモードに対応する Tailwind の設定:
// tailwind.config.js (v3.4.1+)
module.exports = {
darkMode: 'selector', // `.dark` on <html>
// darkMode: ['selector', '[data-theme="dark"]'], // or drive it from data-theme
}
/* Tailwind v4: in your main CSS file */
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
誤ったテーマのちらつき(FOUC)を防ぐ
スタイルシートより前の <head> に、次のスクリプトをインラインで、読み込みをブロックする形で追加します(defer/async を付けたスクリプトやバンドル済みモジュールは、初回描画後に実行されるため使用しません)。
<script>
(function () {
var theme = null;
try { theme = localStorage.getItem('theme'); } catch (e) {}
if (!theme) theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
var root = document.documentElement;
root.setAttribute('data-theme', theme);
root.classList.toggle('dark', theme === 'dark');
})();
</script>
SSR フレームワーク(Next.js など)を使用する場合は、<html> の属性がサーバーでのレンダリング時と異なるため、<html> に suppressHydrationWarning を追加してください。
8. コンポーネント別のダークモードパターン
入力フィールド
<input class="bg-white border-gray-300 text-gray-900 placeholder:text-gray-400
focus:border-indigo-500 focus:ring-indigo-500/20
dark:bg-white/5 dark:border-white/30 dark:text-gray-100 dark:placeholder:text-gray-400
dark:focus:border-indigo-400 dark:focus:ring-indigo-400/20" />
カード
<div class="bg-white border border-gray-200 shadow-sm
dark:bg-white/5 dark:border-white/10 dark:shadow-none">
ボタン
<button class="bg-indigo-600 text-white hover:bg-indigo-500
dark:bg-indigo-400 dark:text-indigo-950 dark:hover:bg-indigo-300">
Primary
</button>
<button class="bg-gray-100 text-gray-700 hover:bg-gray-200
dark:bg-white/10 dark:text-gray-200 dark:hover:bg-white/15">
Secondary
</button>
ドロップダウン/ポップオーバー
<div class="bg-white border border-gray-200 shadow-lg rounded-xl
dark:bg-gray-800 dark:border-white/10 dark:shadow-2xl dark:shadow-black/20">
テーブル
<tr class="border-b border-gray-100 hover:bg-gray-50
dark:border-white/5 dark:hover:bg-white/5">
9. モードの滑らかな切り替え
モード間の CSS トランジション
CSS に * { transition: … } のルールを常時設定しないでください。すべてのコンポーネント固有のトランジション(ボタンの変形、シートのスライドなど)が上書きされ、初回描画時に色がアニメーションしてしまいます。切り替え中だけ一時的にクラスを適用してください。
@media (prefers-reduced-motion: no-preference) {
.theme-transition,
.theme-transition *,
.theme-transition *::before,
.theme-transition *::after {
transition: background-color 200ms ease, border-color 200ms ease,
color 200ms ease, fill 200ms ease, stroke 200ms ease !important;
}
}
// In toggleTheme(), wrap the switch:
const root = document.documentElement;
root.classList.add('theme-transition');
applyTheme(next);
setTimeout(() => root.classList.remove('theme-transition'), 250);
代替方法として、対応ブラウザーでは、クロスフェードを実現するために document.startViewTransition(() => applyTheme(next)) を使用できます。
切り替えボタンの UI
<button type="button" data-theme-toggle aria-pressed="false" aria-label="Dark mode"
onclick="toggleTheme()" class="relative w-14 h-7 rounded-full
bg-gray-500 dark:bg-indigo-500 transition-colors
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500">
<span class="absolute top-0.5 left-0.5 w-6 h-6 rounded-full bg-white shadow
motion-safe:transition-transform dark:translate-x-7 flex items-center justify-center">
<svg class="w-4 h-4 text-amber-500 dark:hidden" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"/>
</svg>
<svg class="w-4 h-4 text-indigo-500 hidden dark:block" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
</svg>
</span>
</button>
完全なトークンシステム
推奨ダークモードパレット
すべての UI スキルで共有されます(dashboard-design でも再利用します)。--color-primary とステータスカラーは、セクション3のデザインコンテキストの値から導出してください。以下の oklch 値は、Primary #2563EB のデフォルト値です。
[data-theme="dark"] {
color-scheme: dark;
/* Surfaces */
--bg-base: oklch(13% 0.01 265);
--bg-surface: oklch(18% 0.01 265);
--bg-elevated: oklch(23% 0.01 265);
--bg-overlay: oklch(28% 0.01 265);
/* Text */
--text-primary: oklch(95% 0 0);
--text-secondary: oklch(75% 0 0);
--text-muted: oklch(62% 0 0); /* ≥ 4.5:1 on base and surface */
/* Borders */
--border: oklch(100% 0 0 / 0.08); /* decorative dividers */
--border-strong: oklch(100% 0 0 / 0.15);
--border-input: oklch(100% 0 0 / 0.30); /* form controls: ≥ 3:1 */
/* Interactive */
--color-primary: oklch(72% 0.18 265);
--color-primary-hover: oklch(78% 0.16 265);
/* Status */
--color-success: oklch(70% 0.15 155);
--color-warning: oklch(75% 0.15 80);
--color-error: oklch(70% 0.16 25);
--color-info: oklch(72% 0.15 230);
}
検証:レンダリングして確認
確認していないコードを引き渡さないでください。生成したら、次の手順を行います。
- モバイルとデスクトップの幅でスクリーンショットを撮影します。例:
npx playwright screenshot --full-page --viewport-size=390,844 file://$PWD/index.html mobile.pngを実行し、続けて--viewport-size=1440,900を指定します(または開発サーバーの URL を指定します)。 - 両方の画像を開いて確認します。横スクロールや、テキストの切り詰め・重なりがないこと、階層が一目でわかること、ブランドのトークンが適用されていること、画像が正しいアスペクト比で読み込まれていることを確認します。
- Tab キーで一巡し、すべてのインタラクティブ要素に論理的な順序で視認可能なフォーカスリングが表示されることを確認します。
- 両方のテーマのスクリーンショットを撮影します(Playwright の
--color-scheme=dark/lightを使用)。ダークモードで再読み込みし、ライトテーマが一瞬表示されないことを確認します。ダークモードで控えめなテキストと境界線のコントラストも確認します。 - 見つかった問題を修正し、完了を報告する前にもう一度スクリーンショットを撮影します。
ダークモードのチェックリスト
- 背景はダークグレー、テキストはオフホワイトにする(純粋な#fffを純粋な#000に重ねない)
- ネイティブコントロールの表示を合わせるため、
color-schemeを設定する(CSS プロパティと meta タグ) - 立体感には影ではなく、より明るい面を使う
- プライマリカラーは彩度を下げて明るくする
- 境界線には半透明の白を使う
- 画像を少し暗くする
- ロゴにはライト版を用意する
- 誤ったテーマが読み込み時に一瞬表示されないようにする(インラインのブロッキング
<head>スクリプト) - システム設定を検出して尊重する
- ユーザー設定をlocalStorageに保存する
- テーマの切り替えは一時的なものにする(グローバルな
*ルールは使わない)。また、動きを減らす設定が有効な場合は切り替えを省略する - ダークモードでもコントラストが WCAG AA を満たすこと:テキスト(控えめなテキストを含む)は4.5:1、入力欄の境界線、アイコン、フォーカスリングは3:1
- 切り替えには、アクセシブルな名前と
aria-pressedを持つ実際の<button>を使う - 両方のテーマをレンダリングしてスクリーンショットを撮影する(「検証」を参照)