こんにちは。小学生向けのニュースサイト、こどもニュースをつくっています。
このサイトにはライトとダークの切り替えがあります。「システムに従う」も選べます。
MUI を使っているので配色はテーマで管理できるのですが、テーマの外にも色があります。ルビの点線、スクロールバー、フォーカスのリングです。この 2 つを揃えるのに少し手間が要りました。
MUI の色スキームと data 属性
MUI には色スキームの仕組みがあり、CSS 変数として出力できます。切り替えの目印になるセレクタも指定できます。
ダークテーマは MUI の colorSchemes + CSS 変数で実現する。
colorSchemeSelector: 'data-kn-theme' により、light 変数は `:root, [data-kn-theme="light"]`、
dark 変数は `[data-kn-theme="dark"]` に出力される。
cssVariables: { colorSchemeSelector: 'data-kn-theme' },
<html data-kn-theme="dark"> が付いていればダーク、という形になります。
この属性は、最初のペイント前にブートスクリプトが付けます。静的サイトなので、そうしないと一瞬ライトが見えます。hydration の後は MUI の仕組みが引き継ぎます。
初回ペイント前にブートスクリプトがスタンプし、hydration 後は MUI の CssVarsProvider が引き継ぐ。
同じ属性を 2 つの仕組みが触るので、解決される値が一致している必要があります。ずれると、hydration のタイミングで色が変わります。
テーマの外の色をどうするか
MUI のテーマに載らない色があります。
- ルビの点線(漢字の下に引く線)
- ルビのハイライト(読みが出ている漢字の背景)
- フォーカスのリング
- スクロールバーの色
- 目次のダッシュ
これらは自前の CSS で当てているので、ダークのときの値も自前で持つことになります。
CSS 変数にまとめました。
:root {
--kn-ruby-highlight: #ffffc9;
--kn-ruby-line: …;
--kn-rt-color: #1a1a1a;
--kn-focus-ring: …;
--kn-toc-dash: rgba(26,26,26,0.22);
…
}
[data-kn-theme="dark"] {
--kn-ruby-highlight: #4d4718;
--kn-rt-color: #e9e5dd;
--kn-toc-dash: rgba(242,239,233,0.28);
…
}
同じ詳細度なので、記述順でダークが勝ちます。使う側は変数を参照するだけで、スキームを気にしません。
ruby.kn { border-bottom: 1px dotted var(--kn-ruby-line); }
重複する色を 1 か所にまとめる
問題は、テーマのパレットと CSS 変数に同じ色が現れることでした。
ルビの点線の色は、テーマの成功色と同じ緑です。フォーカスのリングも同じ系統。両方にリテラルで書いていたので、片方だけ変えると食い違います。
/** WebTheme の MUI palette と globalStyles の --kn-* 変数が共有する色トークン。
* 片方だけ変えるとルビ点線・スクロールバー・focus リングが本体テーマとズレるため、
* 両ファイルに重複していたリテラルはここだけで定義する(色名は DESIGN.md に対応)。
* どちらか一方でしか使わない色(--kn-ruby-highlight 等)は従来どおり各ファイルに残す。 */
export const KN_COLORS = {
light: {
meadowGreen: '#00ca48', // success.main / --kn-ruby-line(ルビ点線)
meadowGreenDeep: '#00913a', // success.dark / --kn-focus-ring
stone: '#f2f0ed', // divider / --kn-scrollbar-track
subGray: '#6d6d6d', // text.secondary / --kn-scrollbar-thumb
pageBg: '#fbfaf9', // background.default / layout.tsx の theme-color
…
共有するものだけをここに置き、片方でしか使わない色は元の場所に残しました。
全部をまとめようとすると、この定数が「色の一覧」になって、どこで使うかが分からなくなります。まとめる基準は「2 か所以上で同じ値を使っているか」にしました。
コメントに対応関係を書いてあるので、変えたときの影響範囲が読めます。
モバイルのブラウザ chrome にも渡す
pageBg は、ページの地色であると同時に、モバイルのブラウザの上部バーの色(theme-color)でもあります。
ここが揃っていないと、スクロールしたときに境目が見えます。同じ値を使うので、共有の定数に入れました。
ただし theme-color には制約があります。メタタグなので、サイト内の切り替えには追従しません。OS の設定にだけ反応します。この制約は受け入れて、light と dark の値をメディアクエリで書き分けています。
「サイト内の切り替えに追従しない」ことを知らずに実装すると、バグとして追いかけることになります。仕様上できないことは、コメントに書いておくのが親切でした。
「システムに従う」の扱い
3 択(ライト・ダーク・システム)にすると、保存する値が増えます。
このサイトでは cookie に light / dark / auto を保存しています。auto のときは、OS の設定に従って解決します。
注意したのは、既定値と明示的な選択を区別することでした。以前は既定が auto だったので、保存された auto が「何も選んでいない」なのか「システムを選んだ」なのかが分かりません。
既定を変えるときに、この区別が必要になりました。明示的に選んだ人はそのまま、選んでいない人は新しい既定に、としたいところです。
保存された値だけでは区別が付かないので、移行の処理を入れました。設定の既定を変える可能性があるなら、「未設定」と「明示的に既定と同じ値を選んだ」は別の状態として持つほうが安全です。
まとめ
- MUI の色スキームは data 属性をセレクタにできる。静的サイトではその属性を最初のペイント前に付ける
- テーマに載らない色は CSS 変数にまとめ、スキームごとに値を差し替える
- パレットと CSS 変数で同じ色を使うなら、共有の定数に置く。まとめる基準は「2 か所以上で使うか」
- 定数には対応関係をコメントで書く。変えたときの影響が読める
-
theme-colorはサイト内の切り替えに追従しない。仕様上の制約はコメントに残す - 「システムに従う」を選べるなら、未設定と明示的な選択を区別できるようにしておく