切り替えのボタンを作る
配色を切り替えるボタンは、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.tsxexport 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を置く場所が無いので失敗します。
このサイトの配色で試す
トグルと3択を、どちらもこのサイトのプロバイダにつないでいます。どちらを操作しても、このサイト全体の配色が変わります。
試してみる
- 3択で「システム」を選ぶと、
preferenceはsystemになり、schemeにはOSの設定から決まった配色が入ります。 - 「ダークにする」か「ライトにする」のボタンを押すと、画面に出ていた配色の反対が保存され、3択の選択も「ダーク」か「ライト」に移ります。
- 「システム」を選び直してからOSの外観の設定を切り替えると、
schemeだけが変わり、preferenceはsystemのままです。