Skip to content

サイコロを作りたい(3)ダークモード対応

Updated: at 13:02
ロゴを立方体にしたもの

モードによってシーン背景とマテリアルの色を変更する機能を実装していきます。

目次

目次を開く

技術スタック

パッケージ名バージョン
astro2.3.4
@astrojs/react2.1.2
react18.2.0
tailwindcss3.3.2

3D関連のパッケージは前回と同じです。

モード切り替え機能の仕様

よくあるヘッダー横にボタンがあり、クリックするとモードが切り替わります。スタイルの切り替えはtailwindcssを使用しており、<html>にdata属性を付与することにより、全体のスタイル切り替えを行います。またこのモード切り替え用のjsはAstroのテーマ「AstroPaper」を参照しています。あまりスタイルをいじっていないのでお気づきだとは思いますが、当ブログもAstroPaperがベースとなっています。非常に優れたブログテンプレートなのでおすすめです。

// toggle-theme.js
const primaryColorScheme = ""; // "light" | "dark"

// ローカルストレージからテーマデータを取得
const currentTheme = localStorage.getItem("theme");

function getPreferTheme() {
  // ローカルストレージにテーマが設定されていればその値を返す
  if (currentTheme) return currentTheme;

  // primaryColorScheme が設定されていればその値を返す
  if (primaryColorScheme) return primaryColorScheme;

  // ユーザーのデバイスのカラースキームを返す
  return window.matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark"
    : "light";
}

let themeValue = getPreferTheme();

function setPreference() {
  localStorage.setItem("theme", themeValue);
  reflectPreference();
}

function reflectPreference() {
  document.firstElementChild.setAttribute("data-theme", themeValue);

  document.querySelector("#theme-btn")?.setAttribute("aria-label", themeValue);
}

// 画面の点滅やCSSの更新を防ぐために初期化
reflectPreference();

window.onload = () => {
  // screen readerがボタンの最新の値を取得できるようにロード時に初期化
  reflectPreference();

  // テーマボタンのクリックを監視
  document.querySelector("#theme-btn")?.addEventListener("click", () => {
    themeValue = themeValue === "light" ? "dark" : "light";
    setPreference();
  });
};

// システムのカラースキーム変更を同期
window
  .matchMedia("(prefers-color-scheme: dark)")
  .addEventListener("change", ({ matches: isDark }) => {
    themeValue = isDark ? "dark" : "light";
    setPreference();
  });

すぐにモードを適用させるために<head>に直接展開するようにAstrojsのis:inlineを指定しています(ページを開いた際に白いちらつきを防げる)。

Reactからモードの状態管理

前項の通り、モードの管理はVannilaで実装されています。しかし、今回はReact-Three-Fiberで作成した3Dシーンでモード情報を取得したいです。

モードを管理するuseModeというHooksを作成します。

// useMode.ts
const useMode = () => {
  const [mode, setMode] = useState<"dark" | "light">("dark");

  const themeBtnClick = useCallback(() => {
    setMode(mode === "dark" ? "light" : "dark");
  }, [mode]);

  useEffect(() => {
    document
      .querySelector<HTMLButtonElement>("#theme-btn")
      ?.addEventListener("click", themeBtnClick);

    return () => {
      document
        .querySelector<HTMLButtonElement>("#theme-btn")
        ?.removeEventListener("click", themeBtnClick);
    };
  }, [themeBtnClick]);

  useEffect(() => {
    const m = document.firstElementChild!.getAttribute("data-theme");
    if (m === "dark" || m === "light") {
      setMode(m);
    }
  }, []);

  return mode;
};

初回だけtoggle-theme.jsによって<html>に設定されるデータ属性のdata-themeを参照して現在のモードを取得しています。
あとはモード変更ボタンにuseEffect()からイベントを付与し、クリックされたらStateが変更される仕組みです。
以前、別件でVanillaで書かれたシステムにReactで実装されたアプリケーションの組み込みを担当したことがあったのですが、その際にアプリケーションの起動トリガーをどうするか悩んだ末に今回のようなuseEffect()を使用した実装に落ち着きました。これが正解なのかはわからないので、もっといい案がある方はこっそり教えていただきたいです。

モードの取得ができたので、あとはこれをモードを参照したい場所でインポートするだけです。

export default function MyGL() {
    const mode = useMode();
    return (
        ...省略
        <color
            attach="background"
            args={[
                mode === "dark"
                    ? darkParams.bgColor
                    : lightParams.bgColor,
            ]}
        />
        ...省略
    )
}

(ノ≧∀≦)ノ

終わりに

今回はReactからVanillaを参照して3Dシーンのダークモード対応する機能を実装しました。ダークモードって個人的には必須なんですけど、中にはデフォルトのライトモードしか普段使用していない方もいると思うので、なかなか難しいですね。レイアウトも今回みたいな一工夫が必要になってくるので、ホムペなどのWeb制作界隈などでは、あまり実装しているサイトは見かけません。ないよりかはあったほうが嬉しい機能ではあるので、これからも積極的にダークモード実装していきたいです。


Previous Post
microCMSからFrontMatterCMSに移行しました
Next Post
サイコロを作りたい(2)吹き飛ばす