Astro 3への更新とView Transitionsの落とし穴

ブログをAstro v3へ更新した際に必要になった、View Transitionsとダークモードの調整をまとめます。

Published
2023年10月31日
Read in English

Astro v3へ上げたところ、ダークモードが初回表示でしか効かなくなりました。リロードすれば正しく暗くなるのに、サイト内のリンクをたどるとライトモードへ戻ります。犯人はView Transitionsで、直すのに足りなかったのは次の1行でした。

document.addEventListener("astro:after-swap", setup);

なぜこれで直るのかを、移行時に必要だった変更とあわせて残しておきます。

View Transition APIのおさらい

document.startViewTransition を呼び出すと、ブラウザはその時点の表示をキャプチャし、渡したコールバックでDOMを更新します。その後、新しい表示もキャプチャし、新旧の状態を既定ではクロスフェードさせます。

キャプチャされた状態はCSSの擬似要素として扱えるため、独自のアニメーションも指定できます。1つのAPIと遷移用の擬似要素だけで画面遷移を表現できる、シンプルで強力な仕組みです。

変更した箇所について

Tailwind CSSでダークモードに対応するため、localStorage の値をもとに html 要素へ dark クラスを付与しています。この処理を body 要素内のスクリプトで実行していましたが、v3へ上げると初回しか動かなくなりました。

ページの置き換え後に発火する astro:after-swap イベントで処理を呼び直せば、期待どおりに動作します。ドキュメントにも記載されている方法で、実装は次のとおりです。

<script is:inline>
  function setup() {
    if (
      localStorage.theme === "dark" ||
      (!("theme" in localStorage) &&
        window.matchMedia("(prefers-color-scheme: dark)").matches)
    ) {
      document.documentElement.classList.add("dark");
      localStorage.theme = "dark";
    } else {
      document.documentElement.classList.remove("dark");
      localStorage.theme = "light";
    }
  }
  // 初回ロード時にはイベントが発生しないので実行する
  setup();
  document.addEventListener("astro:after-swap", setup);
</script>

なぜスクリプトが再実行されないのか

Astroのクライアントサイドルーターを有効にすると、サイト内の <a> 要素やブラウザの戻る・進む操作を、Astroのルーターが処理します。document.startViewTransition を呼ぶだけでなく、その前後でDOMの差し替えやスクリプトの扱いなど、さまざまな処理が行われます。

router.ts では、data-astro-exec というカスタムデータ属性を見て、ページ読み込みで新たにスクリプトを実行するかどうかを判断しています。

for (const script of Array.from(document.scripts)) {
  // ここの部分で評価している
  if (script.dataset.astroExec === "") continue;
  // ...
}

この分岐により、自前のスクリプトはページ遷移のたびには実行されません。ライフサイクルイベントを購読して、処理を呼び直す必要がありました。

さいごに

View Transition APIそのものはシンプルですが、実際の画面遷移にはフォールバックもアニメーションも遷移前後の処理もあります。Astroはその多くを引き受け、アプリ側で必要になる隙間をライフサイクルイベントとして公開しています。

初回だけ動いて2回目以降に動かないスクリプトを見つけたら、body 内に直接書いたものが astro:after-swap を購読していないか確認してみてください。

参考