@k8ordo/color-scheme

切り替えのボタンを作る

配色を切り替えるボタンは、useColorScheme()が返す値で作ります。このページでは、ライトとダークを行き来するトグルと、「システム」を含む3択の作り方を紹介します。あわせて、既定値の変え方と、ハイドレーションの前にサーバーの推測を表示しない方法も説明します。

このページの内容

useColorScheme()が返すもの

Client ComponentでuseColorScheme()を呼ぶと、ルートレイアウトのプロバイダが決めた値が3つ返ります。

  • scheme:画面に出ている配色で、'light'か'dark'です。訪問者の選択か、プロバイダの既定値か、OSの設定のどれかから決まります。
  • preference:訪問者が選んだもので、'light'か'dark'か'system'です。何も選んでいないときは'system'になります。
  • setPreference:選んだものを保存する関数です。'system'を渡すと保存していた選択を消し、プロバイダの既定値に戻ります。

フックはプロバイダが決めた値を読むだけで、<html>のクラスにもlocalStorageにも触れません。ページの中に切り替えのボタンがいくつあっても、決めているのは1つのプロバイダなので、表示が食い違うことはありません。

2択のトグルを作る

ライトとダークを行き来するだけなら、schemeを見て反対の値を保存します。

scheme-toggle.tsx
'use client';

import { useColorScheme } from '@k8ordo/color-scheme';

export function SchemeToggle() {
  const { scheme, setPreference } = useColorScheme();
  const next = scheme === 'dark' ? 'light' : 'dark';

  return (
    <button
      onClick={() => {
        setPreference(next);
      }}
      type="button"
    >
      Switch to {next}
    </button>
  );
}

preferenceではなくschemeを見るのは、何も選んでいない間のpreferenceが'system'で、次にどちらへ切り替えればよいかが分からないからです。

トグルを一度押すと選択が保存され、それ以降はOSの設定を変えても配色は変わりません。OSの設定に従う状態へ戻れるようにしたいなら、次の3択にします。このサイトのヘッダーにある切り替えのボタンも、このトグルです。

「システム」を含む3択にする

OSの設定に従う状態も選べるようにするなら、preferenceをそのまま選択肢の値にします。

scheme-select.tsx
'use client';

import { useColorScheme } from '@k8ordo/color-scheme';
import type { ColorSchemePreference } from '@k8ordo/color-scheme';

export function SchemeSelect() {
  const { scheme, preference, setPreference } = useColorScheme();

  return (
    <select
      onChange={(event) => {
        setPreference(
          event.currentTarget.value as ColorSchemePreference,
        );
      }}
      value={preference}
    >
      <option value="system">System ({scheme})</option>
      <option value="light">Light</option>
      <option value="dark">Dark</option>
    </select>
  );
}

setPreference('system')は、保存していた選択を消します。そのためpreferenceは、一度も選んでいない訪問者でも、選んだあとで「システム」に戻した訪問者でも'system'になります。

「システム」の選択肢にschemeを添えておくと、OSの設定からいまどちらの配色になっているかが分かります。

既定値を変える

何も選んでいない訪問者に出す配色は、プロバイダのdefaultPreferenceで決めます。指定しなければ'system'で、OSの設定に従います。

layout.tsx
<ColorSchemeProvider defaultPreference="dark">
  {children}
</ColorSchemeProvider>

既定値は保存されません。あとで既定値を変えると、まだ選んでいない訪問者はみな新しい既定値で表示され、選んだことのある訪問者は自分の選択のままです。インラインスクリプトにも同じ既定値が入るので、最初の描画から新しい既定値で描かれます。

既定値が'system'でないときは、3択の「システム」という呼び方が合わなくなります。preferenceの'system'は「OSに従う」ではなく「何も選んでいない」という意味なので、選択肢の文言も「既定」などにしてください。

警告

落とし穴

CSPでインラインスクリプトをハッシュで許可しているなら、colorSchemeScriptHash()にも同じ既定値を渡します。既定値はスクリプトの文字列に入るので、既定値を変えるとハッシュも変わるからです。

ハイドレーションの前にサーバーの推測を出さない

サーバーはlocalStorageを読めないので、プロバイダは既定値で描きます。既定値が'system'なら、サーバーが描くschemeは'light'です。そのためschemeから選んだアイコンや文言は、ハイドレーションが終わるまでサーバーの推測を表示します。

ページの色そのものは、インラインスクリプトが付けたdarkクラスに従うので、最初の描画から正しく出ます。食い違うのは、Reactの描画で選んだマークアップだけです。直し方は2つあります。

両方を描いてCSSで片方を隠す

CSSで出し分けられるものは、両方の状態を描いておき、dark:バリアントで片方を隠します。クラスは最初の描画の前に付いているので、ハイドレーションを待たずに正しいほうが見えます。

scheme-toggle.tsx
export function SchemeToggle() {
  const { scheme, setPreference } = useColorScheme();

  return (
    <button
      onClick={() => {
        setPreference(scheme === 'dark' ? 'light' : 'dark');
      }}
      type="button"
    >
      <span className="dark:hidden">Switch to dark</span>
      <span className="hidden dark:inline">Switch to light</span>
    </button>
  );
}
情報

メモ

このdark:は、<html>のクラスを読むように宣言したバリアントです。@k8ordo/uiのtailwind.cssはすでに宣言していますが、Tailwind CSSの既定のdark:はOSの設定を読みます。 Tailwind CSSだけで使うときの宣言

ブラウザで描くまで待つ

アイコンの差し替えのように、CSSでは出し分けにくいものもあります。その場合は、そのマークアップを描くコンポーネントでuse(browser())を呼び、<Suspense>の中に置きます。browserはreact-domからimportします。サーバーは推測の代わりにfallbackをHTMLに書き、中身はブラウザで描かれます。

scheme-toggle.tsx
'use client';

import { useColorScheme } from '@k8ordo/color-scheme';
import { Suspense, use } from 'react';
import { browser } from 'react-dom';

import { MoonIcon, SunIcon } from './icons';

function SchemeIcon() {
  use(browser('the stored preference is in localStorage'));
  const { scheme } = useColorScheme();
  return scheme === 'dark' ? <MoonIcon /> : <SunIcon />;
}

export function SchemeToggle() {
  const { scheme, setPreference } = useColorScheme();

  return (
    <button
      aria-label="Toggle colour scheme"
      onClick={() => {
        setPreference(scheme === 'dark' ? 'light' : 'dark');
      }}
      type="button"
    >
      <Suspense fallback={<span className="size-5" />}>
        <SchemeIcon />
      </Suspense>
    </button>
  );
}

fallbackには、アイコンと同じ大きさの空の要素を置きます。アイコンに差し替わったときに、ボタンの大きさが変わらないようにするためです。ボタンそのものはサーバーのHTMLに入っているので、ハイドレーションが終わればすぐに押せます。

警告

落とし穴

<Suspense>は省けません。上に<Suspense>が無いと、サーバーでの描画はfallbackを置く場所が無いので失敗します。

Playground

このサイトの配色で試す

トグルと3択を、どちらもこのサイトのプロバイダにつないでいます。どちらを操作しても、このサイト全体の配色が変わります。

試してみる

  1. 3択で「システム」を選ぶと、preferenceはsystemになり、schemeにはOSの設定から決まった配色が入ります。
  2. 「ダークにする」か「ライトにする」のボタンを押すと、画面に出ていた配色の反対が保存され、3択の選択も「ダーク」か「ライト」に移ります。
  3. 「システム」を選び直してからOSの外観の設定を切り替えると、schemeだけが変わり、preferenceはsystemのままです。
k8ordo

Baselineに入った機能を、制限なく使うReactのライブラリ群。

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2