ダークモードトグル
太陽↔月にモーフするトグルで昼夜を切替。ミニカードの背景・文字色・アイコンがアニメ遷移する今風のテーマスイッチ。
プレビュー
操作クリック/タップで動きます。
使いどころ
デモ用のミニカード1枚だけを対象にライト/ダークを切り替える見本で、ページ全体のテーマ切替の完成品ではない点に注意が必要。実際のアプリで使うなら、この見た目(太陽↔月のノブが動くトグル)自体は「頻繁に使う設定」「常に見える位置に置きたい設定」に向いており、ヘッダーやフッターの固定位置に置くテーマ切替ボタンとして適している。逆に、設定ページの中の一項目として他の設定と並べる場合は、このカード付きの大きな見た目は場所を取りすぎるため、track+knobの部分だけを流用してシンプルなrole=switchのトグルにする方が収まりがよい。またこのサンプルはOSのprefers-color-scheme設定もlocalStorageへの保存も行っていないため、実際に使うには初期状態の決定とページ全体への適用ロジックを別途足す必要がある。
仕組み
toggle要素はrole="switch"とaria-checkedで状態を持ち、クリックのたびにaria-checkedの真偽を反転させてCSSの属性セレクタ([aria-checked="true"])でtrack・knob・sun/moonアイコンの見た目を切り替える、いわゆるARIA switchパターンで実装されている。knobの移動はtransform: translateX(32px)、太陽/月のアイコン切り替えはopacityとtransform(rotate+scale)のクロスフェードで、いずれもtransformとopacityだけで完結するためコンポジタ処理で軽い。カード側(tm-card)はJSがtm-darkクラスを付け外しするだけの独立したCSS連動で、トグルの状態そのものはtoggle要素のaria-checked属性が単一の情報源になっている。ラベル文言(ライト/ダーク)もJSでtextContentを書き換えているため、視覚のテキストとaria-checkedの状態が常に一致するよう1つのクリックハンドラの中で同時に更新されている。
実装の注意
最大の注意点は、このコンポーネントが実際のテーマを何一つ変更していないこと。tm-darkクラスが付け外しされるのはtm-card要素だけで、documentのdata-theme属性やCSS変数、localStorageへの保存は一切行われていない。実際のアプリに組み込む際は、クリックハンドラの中でdocument.documentElement.setAttribute("data-theme", ...)のような実際のテーマ適用と、次回訪問時に復元するための永続化を追加し忘れると、「切り替わったように見えるが実は何も変わっていない」バグになりやすい。次に、初期状態がaria-checked="false"に固定されていて、OSやユーザーの過去の選択を反映する初期化コードが無い点も見落としやすい。ページ読み込み時にprefers-color-schemeやlocalStorageを読んでaria-checkedとクラスの初期値を合わせる処理を、ハードコードされたfalseの代わりに入れる必要がある。
アクセシビリティ
role="switch"とaria-checked、固定のaria-label="ダークモード切替"を最初から備えており、ON/OFFの二値をトグルボタンとして扱う型はARIAのSwitchパターンに沿っている(aria-pressedを使うトグルボタン方式との違いは、switchの方が「設定のON/OFF」という意味を明確に持つ点で、この用途にはswitchの選択が妥当)。ラベル文言(ライト/ダーク)がテキストとしても存在するため、色だけに頼らず状態が伝わる点もよい。一方で、prefers-reduced-motionの分岐が実装に無く、knobのtranslateXやアイコンのrotate+scaleといった動きのあるtransitionが常に発生する。もう一つの欠落は、このトグルがOS側のprefers-color-scheme(ユーザーがシステムでダークモードを選択している場合の既定値)を初期化時に一切参照していないことで、視覚的な設定と実際の見た目の一貫性という観点でも、初回表示時にOS設定を反映する処理を足すべきである。
調整して使う
調整しやすいのは3つ。ノブの移動距離とtrackの幅(既定64×32px)で、より大きなタップ領域が必要なモバイル向けには72×36px程度に拡大しても比例して調整すれば破綻しない。次にアイコンで、太陽/月の組み合わせ以外にも晴れ/曇りのような別のメタファーに差し替え可能だが、その場合もopacity+transformのクロスフェードの構造はそのまま流用できる。最後に、このサンプルはtm-card1枚だけに適用しているが、実際にページ全体へ適用するにはCSS変数(--bg, --text等)をdata-theme属性に応じて切り替える形に設計を広げ、tm-cardへの直接クラス付与のロジックをdocument.documentElementへのdata-theme切替に置き換えるのが実用上の変更点になる。トグルのUI自体(track/knob/アイコン)は変更せずに使い回せる。
コード(コピーして使えます)
<div class="tm-wrap" id="tmWrap">
<button class="tm-toggle" id="tmToggle" role="switch" aria-checked="false" aria-label="ダークモード切替">
<span class="tm-track">
<span class="tm-knob" aria-hidden="true">
<svg class="tm-icon tm-sun" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="5" fill="currentColor"/>
<line x1="12" y1="2" x2="12" y2="5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="12" y1="19" x2="12" y2="22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="2" y1="12" x2="5" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="19" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="4.22" y1="4.22" x2="6.34" y2="6.34" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="17.66" y1="17.66" x2="19.78" y2="19.78" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="4.22" y1="19.78" x2="6.34" y2="17.66" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="17.66" y1="6.34" x2="19.78" y2="4.22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<svg class="tm-icon tm-moon" viewBox="0 0 24 24" fill="none">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" fill="currentColor"/>
<circle cx="17" cy="8" r="1.2" fill="#0d1f3a" opacity=".5"/>
<circle cx="14" cy="13" r="0.8" fill="#0d1f3a" opacity=".4"/>
<circle cx="19" cy="12" r="0.6" fill="#0d1f3a" opacity=".3"/>
</svg>
</span>
</span>
<span class="tm-label" id="tmLabel">ライト</span>
</button>
<div class="tm-card" id="tmCard">
<div class="tm-card-icon" id="tmCardIcon" aria-hidden="true">
<svg viewBox="0 0 36 36" fill="none">
<circle cx="18" cy="18" r="16" fill="currentColor" opacity=".15"/>
<circle cx="18" cy="14" r="5" fill="currentColor" opacity=".7"/>
<path d="M8 30c0-5.52 4.48-10 10-10s10 4.48 10 10" fill="currentColor" opacity=".5"/>
</svg>
</div>
<div class="tm-card-text">
<p class="tm-card-name" id="tmCardName">田中 太郎</p>
<p class="tm-card-sub" id="tmCardSub">テーマを切り替えてみよう</p>
</div>
</div>
</div>