アダプティブで利用しやすいテーマ切り替えコンポーネントを作成する方法の基本的な概要。
この投稿では、ダークモードとライトモードの切り替えコンポーネントを作成する方法について考えを共有します。デモをお試しください。
動画をご覧になる場合は、この投稿の YouTube バージョンをご覧ください。
概要
ウェブサイトによっては、システム設定に完全に依存するのではなく、カラーパターンを管理するための設定を提供している場合があります。つまり、ユーザーがシステム設定以外のモードでブラウジングする可能性があるということです。たとえば、ユーザーのシステムはライトモードを使用しているが、ユーザーがウェブサイトをダークモードで表示することを好む場合です。
この機能の構築にあたっては、ウェブ エンジニアリングについて考慮すべき点がいくつかあります。たとえば、ページの色が点滅しないよう、ブラウザはできるだけ早く設定を認識できるようにする必要があります。また、コントロールをシステムと同期してから、クライアント側の例外を許可する必要があります。
マークアップ
ブラウザが提供するインタラクション イベントや機能(クリック イベントやフォーカス可能性など)を活用できるため、切り替えには <button>
を使用する必要があります。
ボタン
ボタンには、CSS から使用するクラスと JavaScript から使用する ID が必要です。また、ボタンのコンテンツはテキストではなくアイコンであるため、title 属性を追加してボタンの目的に関する情報を提供します。最後に、アイコンボタンの状態を保持する [aria-label]
を追加して、スクリーン リーダーが視覚障がいのあるユーザーとテーマの状態を共有できるようにします。
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
>
…
</button>
aria-label
、aria-live
(ポライト)
aria-label
の変更について通知する必要があることをスクリーン リーダーに示すには、ボタンに aria-live="polite"
を追加します。
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
aria-live="polite"
>
…
</button>
このマークアップの追加により、aria-live="assertive"
ではなく、変更内容をユーザーに丁寧に伝えるようスクリーン リーダーに知らせます。このボタンの場合は、aria-label
の状態に応じて「明るい」または「暗い」と通知します。
SVG(Scalable Vector Graphic)アイコン
SVG を使用すると、最小限のマークアップで、高品質でスケーラブルな図形を作成できます。ボタンを操作すると、ベクターの新しい視覚的な状態がトリガーされるため、SVG はアイコンに適しています。
次の SVG マークアップは、<button>
内に記述します。
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
…
</svg>
aria-hidden
が SVG 要素に追加され、スクリーン リーダーがプレゼンテーション用としてマークされるため、無視できるようになりました。これは、ボタン内のアイコンなど、視覚的な装飾に適しています。要素に必須の viewBox
属性に加えて、画像をインライン サイズにするのと同様の理由により、高さと幅を追加します。
太陽
太陽のグラフィックは円と線で構成されます。これらの線は SVG に適当な形状を持ちます。cx
プロパティと cy
プロパティを 12(ビューポート サイズ(24)の半分)に設定することで <circle>
を中央に配置し、サイズを設定する 6
の半径(r
)を指定します。
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>
また、mask プロパティは、次に作成する SVG 要素の ID を指します。最後に、currentColor
を使用して、ページのテキスト色と一致する塗りつぶし色を指定します。
太陽の光
次に、グループ要素 <g>
グループ内で、円のすぐ下に太陽光線の線を追加します。
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
<g class="sun-beams" stroke="currentColor">
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</g>
</svg>
今回は、fill の値が currentColor
ではなく、各行のストロークが設定されています。線と円の形が、ビームのあるきれいな太陽を作り出しています。
月
月は、光(太陽)と暗(月)がシームレスに切り替わるような錯覚を起こすため、SVG マスクを使用して太陽のアイコンを拡張したものです。
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
<g class="sun-beams" stroke="currentColor">
…
</g>
<mask class="moon" id="moon-mask">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
<circle cx="24" cy="10" r="6" fill="black" />
</mask>
</svg>
SVG を使用したマスクは強力で、白と黒の色を使用して、別のグラフィックの一部を削除または含めることができます。SVG マスクを使用すると、太陽のアイコンが月の <circle>
シェイプに欠けてしまいます。これは、円形のシェイプをマスク領域の内外に移動するだけです。
CSS が読み込まれない場合はどうなりますか?
CSS が読み込まれなかったかのように SVG をテストし、結果が極端に大きくなったり、レイアウトの問題が生じたりしていないか確認することをおすすめします。SVG 上のインラインの高さと幅の属性と、currentColor
を使用すると、CSS が読み込まれない場合にブラウザが使用する最小限のスタイルルールが設定されます。これにより、ネットワークの乱れに対する優れた防御スタイルが実現します。
Layout
テーマ切り替えコンポーネントは表面積が小さいため、レイアウトにグリッドや Flexbox は必要ありません。代わりに、SVG の位置指定と CSS 変換が使用されます。
スタイル
.theme-toggle
スタイル
<button>
要素は、アイコンの形状とスタイルのコンテナです。この親コンテキストは、SVG に渡すためにアダプティブな色とサイズを保持します。
最初のタスクでは、ボタンを円形にして、デフォルトのボタンスタイルを削除します。
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
}
次に、インタラクションのスタイルを追加します。マウスユーザー向けにカーソル スタイルを追加します。touch-action: manipulation
を追加して、高速に反応するタッチ エクスペリエンスを実現。iOS でボタンに適用される半透明のハイライト表示を削除します。最後に、要素の端からフォーカス状態の輪郭線に注目します。
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
outline-offset: 5px;
}
ボタン内の SVG にもスタイルが必要です。SVG はボタンのサイズに合わせ、線の端を丸くして見た目を滑らかにします。
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
outline-offset: 5px;
& > svg {
inline-size: 100%;
block-size: 100%;
stroke-linecap: round;
}
}
hover
メディアクエリを使用した適応サイズ調整
アイコンボタンのサイズは 2rem
ではやや小さいため、マウスユーザーには適していますが、指などの粗いポインタには使いにくい場合があります。マウスオーバー メディア クエリを使用してサイズの増大を指定することにより、ボタンが多くのタッチサイズ ガイドラインを満たすようにします。
.theme-toggle {
--size: 2rem;
…
@media (hover: none) {
--size: 48px;
}
}
太陽と月の SVG スタイル
ボタンはテーマ切り替えコンポーネントのインタラクティブな要素を保持し、内部 SVG は視覚的要素とアニメーション要素を保持します。そこで、アイコンを美しく作成し、生き生きとしたものにすることができます。
ライトモード
SVG シェイプの中心からスケーリングと回転のアニメーションが発生するようにするには、transform-origin: center center
を設定します。ボタンが提供するアダプティブ カラーが、ここではシェイプで使用されます。月と太陽は var(--icon-fill)
と var(--icon-fill-hover)
のボタンを使用して塗りつぶし、日光はストロークに変数を使用します。
.sun-and-moon {
& > :is(.moon, .sun, .sun-beams) {
transform-origin: center center;
}
& > :is(.moon, .sun) {
fill: var(--icon-fill);
@nest .theme-toggle:is(:hover, :focus-visible) > & {
fill: var(--icon-fill-hover);
}
}
& > .sun-beams {
stroke: var(--icon-fill);
stroke-width: 2px;
@nest .theme-toggle:is(:hover, :focus-visible) & {
stroke: var(--icon-fill-hover);
}
}
}
ダークモード
月のスタイルでは、太陽光を削除して太陽の円を拡大し、円マスクを移動する必要があります。
.sun-and-moon {
@nest [data-theme="dark"] & {
& > .sun {
transform: scale(1.75);
}
& > .sun-beams {
opacity: 0;
}
& > .moon > circle {
transform: translateX(-7px);
@supports (cx: 1) {
transform: translateX(0);
cx: 17;
}
}
}
}
ダークモードには色の変更や遷移はありません。親ボタン コンポーネントは色を所有しており、暗い色と明るい色のコンテキストではすでにアダプティブになっています。遷移情報は、ユーザーのモーション設定メディアクエリの背後に配置する必要があります。
アニメーション
ボタンは機能的かつステートフルである必要がありますが、この時点では遷移はありません。以下のセクションでは、遷移をどのように定義するかについて説明します。
メディアクエリの共有とイージングのインポート
遷移やアニメーションをユーザーのオペレーティング システムのモーション設定の背後に簡単に配置できるように、PostCSS プラグインのカスタム メディアでは、メディア クエリ変数のドラフト CSS 仕様の構文を使用できます。
@custom-media --motionOK (prefers-reduced-motion: no-preference);
/* usage example */
@media (--motionOK) {
.sun {
transition: transform .5s var(--ease-elastic-3);
}
}
ユニークで使いやすい CSS イージングを作成するには、Open Props のイージング部分をインポートします。
@import "https://unpkg.com/open-props/easings.min.css";
/* usage example */
.sun {
transition: transform .5s var(--ease-elastic-3);
}
太陽
太陽の移行は月よりも遊び心があり、弾力性のあるイージングでこの効果を実現できます。太陽光線は回転に合わせて少しだけ跳ね、太陽の中心はスケーリングに合わせて少しだけ跳ねます。
デフォルト(ライトモード)のスタイルで遷移が定義され、ダークモード スタイルでライトへの遷移のカスタマイズが定義されます。
.sun-and-moon {
@media (--motionOK) {
& > .sun {
transition: transform .5s var(--ease-elastic-3);
}
& > .sun-beams {
transition:
transform .5s var(--ease-elastic-4),
opacity .5s var(--ease-3)
;
}
@nest [data-theme="dark"] & {
& > .sun {
transform: scale(1.75);
transition-timing-function: var(--ease-3);
transition-duration: .25s;
}
& > .sun-beams {
transform: rotateZ(-25deg);
transition-duration: .15s;
}
}
}
}
Chrome DevTools の [Animation] パネルに、アニメーション遷移のタイムラインが表示されます。アニメーション全体の時間、要素、イージングのタイミングを検査できます。
月
月のライトとダークの位置はすでに設定済みです。--motionOK
メディアクエリ内に遷移スタイルを追加して、ユーザーのモーション設定を尊重しながら生き生きとしたものにします。
この遷移をクリーンにするには、遅延と時間のタイミングが重要です。たとえば、太陽の日食が早すぎると、遷移がオーケストレートや遊び心を感じず、混沌とした感じがします。
.sun-and-moon {
@media (--motionOK) {
& .moon > circle {
transform: translateX(-7px);
transition: transform .25s var(--ease-out-5);
@supports (cx: 1) {
transform: translateX(0);
cx: 17;
transition: cx .25s var(--ease-out-5);
}
}
@nest [data-theme="dark"] & {
& > .moon > circle {
transition-delay: .25s;
transition-duration: .5s;
}
}
}
}
モーションが低減されたほうを好む
ほとんどの GUI チャレンジでは、モーションの軽減を好むユーザー向けに、不透明度クロス フェードなどのアニメーションを残そうとしています。ただし、このコンポーネントは、状態が即座に変化するとより快適に感じられます。
JavaScript
このコンポーネントでは、スクリーン リーダーの ARIA 情報の管理から、ローカル ストレージからの値の取得や設定まで、JavaScript に関する多くの作業が行われます。
ページ読み込みのエクスペリエンス
ページの読み込み時に色が点滅しないことが重要でした。ダーク カラーパターンのユーザーが、このコンポーネントでライトを好むことを示した後、ページを再読み込みすると、最初はページが暗くなり、ライトに点滅します。これを防ぐには、HTML 属性 data-theme
をできるだけ早い段階で設定することを目的に、少量のブロッキング JavaScript を実行します。
<script src="./theme-toggle.js"></script>
そのためには、ドキュメント <head>
内のプレーンな <script>
タグが、CSS または <body>
マークアップよりも先に読み込まれます。このようにマークされていないスクリプトが見つかると、ブラウザはコードを実行してから、HTML の他の部分より先に実行します。メイン CSS がページをペイントする前に HTML 属性を設定することで、フラッシュや色の発生を防ぐことができます。
JavaScript はまず、ローカル ストレージでユーザー設定を確認し、ストレージに何もない場合はフォールバックしてシステム設定を確認します。
const storageKey = 'theme-preference'
const getColorPreference = () => {
if (localStorage.getItem(storageKey))
return localStorage.getItem(storageKey)
else
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
}
次に、ローカル ストレージのユーザー設定を設定する関数が解析されます。
const setPreference = () => {
localStorage.setItem(storageKey, theme.value)
reflectPreference()
}
その後に、設定を使用してドキュメントを変更する関数が続きます。
const reflectPreference = () => {
document.firstElementChild
.setAttribute('data-theme', theme.value)
document
.querySelector('#theme-toggle')
?.setAttribute('aria-label', theme.value)
}
ここで重要なのは、HTML ドキュメントの解析状態です。<head>
タグが完全に解析されていないため、ブラウザはまだ「#theme-toggle」ボタンを認識しません。ただし、ブラウザには document.firstElementChild
(<html>
タグ)があります。関数は同期を維持するために両方を設定しようとしますが、初回実行時に設定できるのは HTML タグのみです。querySelector
は、最初は何も検出せず、オプションの連鎖演算子が見つからずに setAttribute 関数の呼び出しが試行されても、構文エラーを保証しません。
次に、その関数 reflectPreference()
がすぐに呼び出され、HTML ドキュメントの data-theme
属性が設定されます。
reflectPreference()
ボタンには属性が必要なため、ページの読み込みイベントを待機してください。その後、クエリ、リスナーの追加、属性の設定を安全に行うことができます。
window.onload = () => {
// set on load so screen readers can get the latest value on the button
reflectPreference()
// now this script can find and listen for clicks on the control
document
.querySelector('#theme-toggle')
.addEventListener('click', onClick)
}
切り替え操作
ボタンがクリックされたら、JavaScript メモリとドキュメント内でテーマを入れ替える必要があります。現在のテーマ値を検査し、新しい状態について判断する必要があります。新しい状態が設定されたら、保存してドキュメントを更新します。
const onClick = () => {
theme.value = theme.value === 'light'
? 'dark'
: 'light'
setPreference()
}
システムと同期しています
このテーマスイッチに固有のスイッチは、変更が行われるとシステム設定と同期します。ページとこのコンポーネントが表示されている間にユーザーがシステム設定を変更すると、ユーザーがシステムの切り替えと同時にテーマスイッチを操作した場合と同様に、テーマの切り替えが新しいユーザーの設定に合わせて変化します。
これを行うには、JavaScript と、メディアクエリの変更をリッスンする matchMedia
イベントを使用します。
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({matches:isDark}) => {
theme.value = isDark ? 'dark' : 'light'
setPreference()
})
おわりに
私のやり方がわかったところで、どうしたらいいですか? 🙂?
多様なアプローチと、ウェブでの構築方法を学んでいきましょう。 デモを作成してツイートのリンクをお願いします。下のコミュニティ リミックス セクションに追加します。