Article

ダークモード対応リニューアル:夜間閲覧者への配慮とブランド性の両立

高田晃太郎
ダークモード対応リニューアル:夜間閲覧者への配慮とブランド性の両立

各種調査やアクセスログの傾向を突き合わせると、夜間帯のセッション比率は年々増えています。とはいえ、公開された一貫した統計で一概に断定できるわけではないため、プロダクトごとの実測で傾向確認するのが実務的です。さらにCSSの機能対応状況を集計する「Can I use」では、2025年時点でprefers-color-scheme(OSやブラウザのダーク/ライト設定に追従する仕組み)のブラウザ対応が世界シェアの大半を占めると報告されています²。加えてGoogleの発表では、OLEDディスプレイ環境でのダークテーマ利用時に高輝度条件下で電力消費が大幅に低減するケースが観測されています³。視認性の基準としてはWCAG 2.1の通常テキスト4.5:1、太字大きめテキスト3:1のコントラスト比が実務での最低ラインです¹。なお、ダークテーマの省電力効果は表示輝度やパネル特性に強く依存し、平均的な条件では中〜小程度の削減にとどまるという報告もあります⁴。つまり、ダークモードは“好み”の域を越え、視環境と消費電力、ひいては利用継続に関わるUX(ユーザー体験)の要件になりつつあるのです。技術面ではトークン駆動の設計とサーバー・クライアント双方でのテーマ判定、そしてフォールバック戦略が鍵になります。ブランド面では色相を守りつつ輝度を再定義し、ノイズのない陰影表現に置き換えることが肝要です。

夜間閲覧の現実とROI:ダークモードは“快適性×ブランド信頼”の投資

ダークモード対応は短期の装飾変更ではなく、累積的な収益機会の最適化です。夜間に輝度の高いUIを見続ける負担は離脱の直接要因になり得ますし、OLED環境では背景の発光量が直に電力コストに跳ね返ります。研究データではOLEDで黒ベースのUIが高輝度条件で顕著な省電力効果を示すことがあり³、自発光ディスプレイでは暗色ほど消費電力が下がる傾向が知られています。一方で、測定条件(特に画面輝度)によって効果は大きく変動し、一般的な明るさでは数%台にとどまるという報告もあります⁴。長時間の視聴や読書系プロダクトでは平均セッション時間の押し上げにつながる余地があります。ビジネス的には、夜間帯の滞在時間が延びれば広告インプレッション、課金導線の露出、カスタマーサクセスとの接点まで裾野が広がります。サポートコストにも影響します。高コントラストで眩しさの少ない配色は、ユーザーの目の疲労と主観的な「使いづらさ」訴えを減らし、問い合わせ削減と満足度の底上げに寄与します。ここで重要なのは、ダークモードを「別サイトのように作り直す」のではなく、設計段階から意味論的トークン(役割に紐づくデザイントークン)を中心に据え、ライトとダークは単一のデザインシステムの振る舞いの差分だと捉えることです。ブランドの色相やタイポグラフィは維持しつつ、輝度とコントラスト、そして余白と境界の表現を構造的に切り替える。この方針が長期保守に耐え、機能追加の速度も落としません。

ブランドを壊さずに暗所最適化するための視点

まず色相は守り、明度と彩度を調律します。たとえばライトテーマでブランドアクセントが#0A84FF相当の鮮やかさでも、ダークでは背景が暗くなる分だけ輝度差が過剰に感じやすく、相対的なまぶしさが増します。色相は固定しつつ、相対輝度で10〜20%ほど抑え、同時にホバー・アクティブ時の階調差で手触り感を補います。背景は完全な#000ではなく#0A0A0A〜#121212の“ソフトブラック”を基準に据えると、スミつぶれを避けてテキストのにじみも抑えられます。影はノイズになりやすいので、シャドウよりも1pxの分割線や微妙な面のトーン差で段差表現を置き換えるのが無難です。タイポグラフィはウェイトを1段軽くし、文字色は#EAEAEA〜#F2F2F2程度に留めると、純白の眩しさを避けながらコントラストを確保できます¹。これらを全てハードコードせず、semantic tokensで管理することが運用の安定に直結します。

コントラストの基準と評価軸

実務ではWCAG 2.1の4.5:1を最低ラインに置きつつ、UIコンポーネントは状態間の差分も合わせて検証します¹。テキストと背景だけでなく、ボタンの通常・ホバー・アクティブ、ボーダーと面、アイコンの線密度のバランスも見ます。将来的にはAPCA(Advanced Perceptual Contrast Algorithm。WCAG 3の候補)に基づく視認性評価の導入を視野に入れると、暗所での実感と数値の整合性が高まりやすくなります。トグル切り替え時のちらつきはUXを壊すので、サーバーサイドで初期テーマを確定しクライアントでの再描画を最小化する方針が必要です⁵。

設計原則:トークン駆動と“意味”の分離で壊れないダークテーマ

色・スペーシング・影・境界・タイポの各要素は、コンポーネント固有値ではなくsemantic tokensとして宣言し、テーマの切り替えはトークンの解決先を差し替えるだけにとどめます。これにより、例えばbrand.primaryはライトでは彩度の高いブルー、ダークではやや抑制した同色相に自動で切り替わります。境界もshadow.elevation.1のような表現的な名前ではなく、surface.separator.mutedのような機能名で指定すると、シャドウをボーダーに置き換える判断がテーマ切り替えに伴ってスムーズに適用できます。色空間はOKLCHやLABの利用が望ましく(知覚的に一貫した明るさ調整がしやすい)、相対輝度の一貫性が取りやすくなります。ブランドのキーアートやロゴはSVGのcurrentColor対応や色反転版の用意をしておくと、ダークでの視認性確保が容易です。

Style Dictionaryでのトークン管理例

// tokens/color.json
{
  "color": {
    "bg": { "default": { "value": "#FFFFFF" }, "dark": { "value": "#121212" } },
    "text": { "primary": { "default": { "value": "#111111" }, "dark": { "value": "#EDEDED" } } },
    "brand": { "primary": { "default": { "value": "#0A84FF" }, "dark": { "value": "#66AFFF" } } },
    "border": { "subtle": { "default": { "value": "#E6E6E6" }, "dark": { "value": "#2A2A2A" } } }
  }
}
// build.js (Style Dictionary)
import StyleDictionary from 'style-dictionary';

const sd = StyleDictionary.extend({
  source: ['tokens/**/*.json'],
  platforms: {
    css: {
      transformGroup: 'css',
      buildPath: 'dist/css/',
      files: [
        { destination: 'tokens.css', format: 'css/variables' }
      ]
    }
  }
});

sd.buildAllPlatforms();

生成されたCSS変数をテーマごとにスコープすることで、設計と実装の境界が明確になります。

実装:CSS変数×メディアクエリ×SSRで高速に、確実に

ダークモードの実装は、CSS変数(カスタムプロパティ)を土台に据えつつ、ユーザー設定とシステム設定の優先順位を定義し、サーバー初期描画(SSR: Server-Side Rendering)の段階でテーマを確定させる構成が安定します。優先順位は一般にユーザー選択を最上位、未選択時はprefers-color-schemeに追従させます⁵。初期描画時のチラつきを避けるため、HTMLタグにdata-theme属性をSSRで付与します⁵。

CSS変数とprefers-color-schemeの基本形

/* base.css */
:root {
  --bg: #FFFFFF;
  --text: #111111;
  --brand: #0A84FF;
  --border-subtle: #E6E6E6;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #121212;
    --text: #EDEDED;
    --brand: #66AFFF;
    --border-subtle: #2A2A2A;
  }
}

html[data-theme="light"] {
  --bg: #FFFFFF;
  --text: #111111;
  --brand: #0A84FF;
  --border-subtle: #E6E6E6;
}

html[data-theme="dark"] {
  --bg: #121212;
  --text: #EDEDED;
  --brand: #66AFFF;
  --border-subtle: #2A2A2A;
}

body {
  background: var(--bg);
  color: var(--text);
}
.button-primary {
  background: var(--brand);
  color: #0A0A0A;
}

ユーザー選択があればdata-themeで上書きし、未選択ならメディアクエリの解決値が使われます。この二層構造がもっとも副作用が少ない形です⁵。

フロントエンドのトグルとフォールバック

// theme.ts
export type Theme = 'light' | 'dark';

const KEY = 'theme';

export function getSystemTheme(): Theme {
  try {
    if (typeof window === 'undefined' || !window.matchMedia) return 'light';
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  } catch {
    return 'light';
  }
}

export function getStoredTheme(): Theme | null {
  try {
    const v = localStorage.getItem(KEY);
    return v === 'light' || v === 'dark' ? v : null;
  } catch {
    return null;
  }
}

export function applyTheme(theme: Theme) {
  if (typeof document === 'undefined') return;
  document.documentElement.setAttribute('data-theme', theme);
}

export function setTheme(theme: Theme) {
  applyTheme(theme);
  try { localStorage.setItem(KEY, theme); } catch {}
}

export function initTheme() {
  const t = getStoredTheme() ?? getSystemTheme();
  applyTheme(t);
}
// App.tsx (React)
import React, { useEffect, useState } from 'react';
import { initTheme, setTheme, getStoredTheme, getSystemTheme, Theme } from './theme';

export function ThemeToggle() {
  const [theme, setState] = useState<Theme>('light');

  useEffect(() => {
    initTheme();
    setState(getStoredTheme() ?? getSystemTheme());
  }, []);

  function onToggle() {
    const next = theme === 'dark' ? 'light' : 'dark';
    setTheme(next);
    setState(next);
  }

  return <button onClick={onToggle} aria-pressed={theme === 'dark'}>Toggle Theme</button>;
}

ローカルストレージの読み書きはtry/catchで保護し、SSRやプライベートモードでもクラッシュしないようにします。初期化時はシステム設定とユーザー保存値の合成で決め打ちし、描画直後に反映する形が一般的です⁵。

Next.js(App Router)でのSSR適用

// app/layout.tsx
import './base.css';
import { cookies } from 'next/headers';
import type { ReactNode } from 'react';

function resolveTheme(): 'light' | 'dark' {
  const c = cookies().get('theme')?.value;
  if (c === 'light' || c === 'dark') return c;
  return 'light'; // SSRでは安全側に倒す。クライアントで上書き
}

export default function RootLayout({ children }: { children: ReactNode }) {
  const theme = resolveTheme();
  return (
    <html lang="ja" data-theme={theme}>
      <body>{children}</body>
    </html>
  );
}
// middleware.ts (任意: クッキー初期化)
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';

export function middleware(req: NextRequest) {
  const res = NextResponse.next();
  const t = req.cookies.get('theme')?.value;
  if (t !== 'light' && t !== 'dark') {
    res.cookies.set('theme', 'light', { path: '/', httpOnly: false });
  }
  return res;
}

SSR段階でdata-themeを打ち、クライアントでユーザー選択があれば追従します。これによりFOUC(読み込み時のスタイル未適用によるチラ見え)や色反転のチラつきを最小化できます⁵。

TailwindとCSS変数の併用

// tailwind.config.js
export default {
  darkMode: ['class', '[data-theme="dark"]'],
  theme: {
    extend: {
      colors: {
        bg: 'var(--bg)',
        text: 'var(--text)',
        brand: 'var(--brand)',
        border: 'var(--border-subtle)'
      }
    }
  }
};
// component example
export function Card({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <div className="bg-bg text-text border border-border rounded-lg p-4">
      <h3 className="text-lg font-semibold">{title}</h3>
      <div>{children}</div>
    </div>
  );
}

TailwindのユーティリティはCSS変数を参照させ、テーマはdata属性で切り替えます。設計の責務は変数側、実装のスピードはユーティリティ側と役割分担できます。

ロゴ・画像・イラストの暗所最適化

写真やイラストは暗所で沈みやすいため、オーバーレイやトーンカーブの別版を用意するか、SVGはcurrentColorに対応させると安定します。

<!-- ロゴ例 -->
<svg viewBox="0 0 100 20" xmlns="http://www.w3.org/2000/svg" aria-label="Brand">
  <path fill="currentColor" d="M...Z" />
</svg>
/* 背景に応じてロゴ色を調整 */
header .logo { color: var(--text); }

currentColorを使うと、テキスト色の見直し一発でロゴの視認性も同時に担保できます。

品質・パフォーマンス検証:視認性、チラつき、消費電力を数字で見る

品質はテイストではなく計測で管理します。まずコントラストは自動テストに組み込み、主要画面の重要テキストが基準を下回らないことをCIで担保します¹。次にパフォーマンスは初回描画時のRecalculate Style(スタイル再計算)とPaint(描画)を最小化し、テーマ切り替えは変数の置換だけにとどめる構成にします。これにより、JSリフローやDOM書き換えを最小化し、インタラクション中のフレーム落ちを防げます。CLS(Cumulative Layout Shift)はテーマ切り替えで動くことが本来ないため、フォントやスクロールバーの幅変化に注意を払い、CLS≈0.00を目標に据えます。モーションは好みが分かれる部分なので、prefers-reduced-motionを尊重し、色のクロスフェードは100〜150ms程度に抑えると違和感が少なくなります。消費電力は端末・パネル・輝度に依存し⁴⁵、長文閲覧や常時点灯画面では暗色UIで一定の削減が見込まれます³⁴。

自動テスト例:Playwright × axe-core

// tests/a11y.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

async function check(page) {
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa'])
    .analyze();
  expect(results.violations).toEqual([]);
}

test('light/darkでコントラスト基準を満たす', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await check(page);
  await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark'));
  await page.waitForTimeout(50);
  await check(page);
});

視覚的回帰テストとちらつき対策

ダーク切り替え時のちらつきは、初期HTMLにdata-themeを付けるSSR戦略でほぼ解消できます⁵。さらにスナップショットテストでライトとダークの差分を比較すれば、トークン変更が予期せぬ影響を及ぼしていないかを機械的に検知できます。パフォーマンス面では、クライアント起動前に一瞬ライトが表示されるFOUCを避けるため、インラインスクリプトで最小限のテーマ適用を先出しする手もあります⁵。

<!-- _document.tsx もしくは index.html にインライン -->
<script>
  try {
    var t = localStorage.getItem('theme');
    if (t === 'light' || t === 'dark') {
      document.documentElement.setAttribute('data-theme', t);
    }
  } catch {}
</script>

消費電力の実地検証と指標化

バッテリー消費は端末や輝度に強く依存しますが⁴、ダークテーマでの長文閲覧や常時点灯画面では一定の削減が見込まれます³⁵。実務では、同一画面を10分間表示した際の電池残量推移をログ取りし、平均化してプロダクト間や改修前後で比較する、といった簡易手法が使われます。統計的な有意差の厳密性だけを狙うのではなく、リニューアル前後での傾向差を捉えて意思決定につなげます。夜間の完読率、直帰率、滞在時間、コンバージョンの時系列を合わせて見ると、快適性の改善が直接・間接に収益へ寄与しているかが読み解きやすくなります。なお、読書性や視覚快適性は個人差や環境差があり、ライト/ダーク双方の使い分けを可能にする配慮が推奨されます⁶。

エラーや例外への備え

ユーザーの環境ではストレージが無効化されていたり、ヘッドレスブラウザがメディアクエリを正しく解決しないこともあります。実装では例外を握りつぶすのではなく、try/catchで安全側に倒し、テーマ未決定の状態を作らないのがポイントです。アクセシビリティAPIに対する配慮として、トグルボタンにはaria-pressedや適切なラベルを付し、フォーカスインジケータはライトとダーク双方で十分なコントラストを確保します¹。

まとめ:光の量を減らし、ブランドの意味を濃くする

ダークモード対応は、単に黒背景に反転する作業ではありません。輝度を設計し直し、境界と面の階調でUIを構成し、意味論的トークンで一貫性を担保することで、夜間でもブランドの声量を落とさずに語り続けられます。技術的にはCSS変数×prefers-color-scheme×SSRの三点セットが基盤で、ReactやNext.jsの実装を通じてチラつきのない切り替えを達成できます⁵。品質は自動テストと可観測性で管理し、ROIは夜間の滞在と完読、電力消費の傾向で捉えるのが現実的です³⁴⁵。あなたのプロダクトは、夜間のユーザーにどんな疲れ方をさせ、どんな集中を支えているでしょうか。次のスプリントでは、アクセント色の輝度とコントラスト、初期描画のテーマ確定、そしてロゴのcurrentColor対応から手をつけてみてください。光を減らすことは、伝えたい意味を濃くすることでもある——その変化は数値と体験の両面で、静かに確かな差となって現れます。

参考文献

  1. W3C WAI. Understanding Success Criterion 1.4.3: Contrast (Minimum) (WCAG 2.1)
  2. Can I use. prefers-color-scheme
  3. Bohn D. Google confirms dark mode is a huge help for battery life on Android. The Verge (2018-11-08). https://www.theverge.com/2018/11/8/18076502/google-dark-mode-android-battery-life
  4. Purdue University ECE. Shedding light on dark mode to save energy (2022). https://engineering.purdue.edu/ECE/News/2022/shedding-light-on-dark-mode-to-save-energy
  5. web.dev (Google Developers). prefers-color-scheme. https://web.dev/articles/prefers-color-scheme
  6. Wang C-H et al. Effects of Dark Mode and Display Polarity on Visual Fatigue and Readability: Implications for Screen Settings for Visual Comfort. Int J Environ Res Public Health. 22(4):609. https://www.mdpi.com/1660-4601/22/4/609