Neat Design Journal

カラーモード対応にlight-dark関数を使う方法

カラーモード対応にlight-dark関数を使う方法

少し前に、Webサイトのカラーモードをユーザーが自由に切り替えられるスイッチを実装する方法を投稿しました。

カラーモードを切り替えるスイッチを実装する方法

以前にCSSだけでダークモードに対応させる方法を投稿しました。これはこれで簡単にダークモードに対応できて良かったんですが、私のように、OSの設定はダークモードでもWebサイトはライトモードで閲覧したいと思われる方が少なか […]

カラーモードを切り替えるスイッチを実装する方法のファビコン
https://neatdesignjournal.com/color-mode-switch/
カラーモードを切り替えるスイッチを実装する方法のサムネイル

これはこれで完成されていたと思っていたんですが、各所の色を変える際のスタイルが煩雑になるところが欠点でした。具体的に言うと、前回の分は以下のようにhtml要素にdata-color-mode="light"またはdata-color-mode="dark"を付与したり切り替えることでその状態のときに適用されるスタイルをそれぞれ当てる、という仕様でした。

html {
    --color-dark: #000;
    --color-light: #fff;
    --transition-color-mode: 0.25s ease;
    transition:
        color var(--transition-color-mode),
        background-color var(--transition-color-mode);
    &[data-color-mode="light"] {
        color: var(--color-dark);
        background-color: var(--color-light);
    }
    &[data-color-mode="dark"] {
        color: var(--color-light);
        background-color: var(--color-dark);
    }
}

例えばこれにa要素を追加してみます。

html {
    --color-dark: #000;
    --color-light: #fff;
    --color-accent: #0077e6;
    --color-accent-dark: #004ea2;
    --transition-color-mode: 0.25s ease;
    transition:
        color var(--transition-color-mode),
        background-color var(--transition-color-mode);
    &[data-color-mode="light"] {
        color: var(--color-dark);
        background-color: var(--color-light);
        & a {
            &:any-link {
                color: var(--color-accent);
            }
            @media (any-hover: hover) {
                &:hover {
                    color: var(--color-accent-dark);
                }
            }
        }
    }
    &[data-color-mode="dark"] {
        color: var(--color-light);
        background-color: var(--color-dark);
        & a {
            &:any-link {
                color: var(--color-accent-dark);
            }
            @media (any-hover: hover) {
                &:hover {
                    color: var(--color-accent);
                }
            }
        }
    }
}

この時点でもうかなり肥大化してしまいます。他にも例えばここの箇所のリンクの色はこうとか、この部分の色はこうして、などを追加していくととんでもないことになってしまいます。

light-dark関数

ということで、今回はlight-dark関数を使って実装し直すことにしました。

light-dark() - CSS: カスケーディングスタイルシート | MDN

light-dark() は CSS の <color> 関数で、プロパティに 2 つの色を設定することができます。開発者が明色カラースキームまたは暗色カラースキームを設定したか、ユーザーがライト色またはダーク色のテーマをリクエストしたか検出することによって、 2 つの色の選択肢のいずれかを返します。テーマ色を prefers-color-scheme メディア特性クエリーに入れる必要はありません。 ユーザーは O Sの設定(ライトモードやダークモードなど)やユーザーエージェントの設定を通じて、環境設定を推奨することができます。 light-dark() 関数は、任意の <color> 値が受け入れられる場合に、 2 つの色の値を指定することができます。 CSS の light-dark() 色関数は、ユーザーの環境設定が light に設定されている場合、または何も設定されていない場合に最初の値を返し、ユーザーの環境設定が dark に設定されている場合に 2 つ目の値を返します。

light-dark() - CSS: カスケーディングスタイルシート | MDNのファビコン
https://developer.mozilla.org/ja/docs/Web/CSS/color_value/light-dark
light-dark() - CSS: カスケーディングスタイルシート | MDNのサムネイル

light-dark関数は、

light-dark(light-color, dark-color)

という書き方で、light-colorの箇所にライトモード時のカラー、dark-colorの箇所にダークモード時のカラーをそれぞれ指定することができます。例えば、

body {
    --color-dark: #242628;
    --color-light: #f9fbfc;
    color: light-dark(var(--color-dark), var(--color-light));
    background-color: light-dark(var(--color-light), var(--color-dark));
}

このように、light-dark関数はライトモード時とダークモード時のカラーを一括で(一箇所の記述だけで)管理できるようになるのでとても便利な関数です。

完成品

それでは、light-dark関数を使ったカラーモード切り替えスイッチの完成品をどうぞ。

See the Pen Color Mode Switch 2 by ryskyshd (@ryskyshd) on CodePen.

HTML

<label for="color_mode_switch" aria-label="カラーモードを切り替える"></label>
<input type="checkbox" id="color_mode_switch" class="color_mode_switch">

前回はbutton要素のみで実装していたのですが、このパターンでもできますのでやってみました。label要素とinput type="checkbox"を連動させてチェックのオンオフによって見た目を切り替える仕様です。

CSS

:root {
    --color-dark: #242628;
    --color-light: #f9fbfc;
    --text-color-base: light-dark(var(--color-dark), var(--color-light));
    --background-color-base: light-dark(var(--color-light), var(--color-dark));
    --transition-color-mode: 0.25s ease;
}

body {
    color: var(--text-color-base);
    background-color: var(--background-color-base);
    transition:
        color var(--transition-color-mode),
        background-color var(--transition-color-mode);
}

input {
    all: unset;
    cursor: pointer;
    &:focus-visible {
        --outline-color: #0077e6;
        outline: 2px solid var(--outline-color);
        outline-offset: 2px;
    }
}

.color_mode_switch {
    --switch-block-size: 2rem;
    --switch-inline-size: calc(var(--switch-block-size) * 2);
    --switch-radius: calc(infinity * 1px);
    --switch-color-light: #d99c00;
    --switch-color-dark: #3f6fd1;
    --switch-color-thumb: #fff;
    position: relative;
    inline-size: var(--switch-inline-size);
    block-size: var(--switch-block-size);
    background-color: var(--switch-color-light);
    border-radius: var(--switch-radius);
    transition: background-color var(--transition-color-mode);
    &::before {
        --thumb-size: var(--switch-block-size);
        --thumb-border-thickness: 2px;
        position: absolute;
        inset: 0;
        display: inline-grid;
        place-content: center;
        inline-size: var(--switch-block-size);
        block-size: var(--switch-block-size);
        content: "\e518";
        color: var(--switch-color-light);
        font-family: "Material Icons Round";
        background-color: var(--switch-color-thumb);
        border: var(--thumb-border-thickness) solid var(--switch-color-light);
        border-radius: var(--switch-radius);
        box-sizing: border-box;
        transition:
            inset-inline-start var(--transition-color-mode),
            border var(--transition-color-mode);
    }
    &:checked {
        background-color: var(--switch-color-dark);
        &::before {
            inset-inline-start: calc(100% - var(--thumb-size));
            content: "\e51c";
            color: var(--switch-color-dark);
            border: var(--thumb-border-thickness) solid var(--switch-color-dark);
        }
    }
}

少し長いですが、後半のスイッチ部分のスタイルについては前回のものとほぼほぼ変わりはありませんので、ここでは説明を割愛します。詳しくは以前の投稿をご参照ください。ポイントは前半部分で、

--text-color-base: light-dark(var(--color-dark), var(--color-light));
--background-color-base: light-dark(var(--color-light), var(--color-dark));

文字の色と背景の色をlight-dark関数で指定したものを変数化しておき、

color: var(--text-color-base);
background-color: var(--background-color-base);

body要素にてそれぞれを指定します。これによって、カラーモードによって対応するカラーでそれぞれが表示されるようになります。

jQuery

const savedScheme = localStorage.getItem("color-scheme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const initialScheme = savedScheme || (prefersDark ? "dark" : "light");

$("html").css("color-scheme", initialScheme);
$("#color_mode_switch").prop("checked", initialScheme === "dark");

$("#color_mode_switch").on("change", function () {
    const newScheme = $(this).prop("checked") ? "dark" : "light";
    $("html").css("color-scheme", newScheme);
    localStorage.setItem("color-scheme", newScheme);
});

以前の仕様と同じく、初期表示はユーザーのOS設定に従い、以降はユーザー指定のカラーモードをローカルに保存しておいてそこから表示、という内容です。

最後に

余談なんですけど、iOSのSafariで、スイッチを切り替えるたびに端末が若干コクっとなるというか、そんな触覚作用を実装したサイトがあったのでこれをやってみたくてChatGPT先生に聞いてみました。すると、スイッチ部分をinput type="checkbox"で実装するといいよ!って言われたので実装してみたんですが、どうも上手くできませんでした。何でなん?笑