跳转到内容

视图过渡动画

Astro 仅需几行代码就可以支持可选择的,每个页面的视图过渡动画。视图过渡动画可以在不刷新浏览器的情况下更新页面内容,并在页面之间提供无缝的动画效果。

Astro 提供了一个 <ViewTransitions /> 路由组件,可以添加到单个页面的 <head> 中,以控制页面在导航到另一个页面时的过渡效果。它提供了一个轻量级的客户端路由器,拦截导航 并允许你自定义页面之间的过渡效果。

将此组件添加到一个可复用的 .astro 组件中,例如公共头部或布局组件,以便 在整个站点上实现动画页面过渡(SPA 模式)

Astro 的视图过渡动画支持由新的 View Transitions 浏览器 API 提供,还包括:

向页面添加视图过渡动画

段落标题 向页面添加视图过渡动画

通过在每个页面的 <head> 中导入和添加 <ViewTransitions /> 路由组件,可以选择在单个页面上使用视图过渡动画。

src/pages/index.astro
---
import { ViewTransitions } from 'astro:transitions';
---
<html lang="en">
<head>
<title>我的主页</title>
<ViewTransitions />
</head>
<body>
<h1>欢迎来到我的网站!</h1>
</body>
</html>

整站视图过渡动画(SPA 模式)

段落标题 整站视图过渡动画(SPA 模式)

导入 <ViewTransitions /> 组件并将其添加到公共 <head> 或共享布局组件中。Astro 将根据旧页面和新页面之间的相似之处创建默认的页面动画,并为不支持的浏览器提供回退行为。

下面的示例显示了如何通过导入和添加此组件到 <CommonHead /> Astro 组件中,为不支持的浏览器提供默认的页面导航动画,包括默认的回退控制选项,从而在整个站点上添加 Astro 的默认页面导航动画:

components/CommonHead.astro
---
import { ViewTransitions } from 'astro:transitions';
---
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<!-- 主要 Meta 标签 -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<ViewTransitions />

无需其他配置即可启用 Astro 的默认客户端导航!

对于更精细的控制,可以在单个元素上使用 过渡动画指令覆盖默认的客户端导航

Astro 会自动为在旧页面和新页面中找到的相应元素并分配一个共享的唯一view-transition-name。这对匹配的元素是通过元素的类型和其在 DOM 中的位置推断出来的。

在你的 .astro 组件的页面元素上使用可选的 transition:* 指令,以更精确地控制导航期间的页面过渡行为。

  • transition:name:允许你覆盖 Astro 的默认元素匹配方式,为一对 DOM 元素 指定过渡动画名称
  • transition:animate:在用新元素替换旧元素时,允许你通过指定动画类型来覆盖 Astro 的默认动画效果。可以使用 Astro 的 内置动画指令创建自定义动画
  • transition:persist: 允许你覆盖 Astro 的默认行为。不同于在导航到另一个页面时用将替换旧元素为新元素的方式,该行为则是在导航到另一个页面时 保留组件和 HTML 元素的状态

在某些情况下,你可能希望自己对相应的且包含视图过渡动画的元素进行标识,那么你可以使用 transition:name 指令为一对元素指定一个名称。

old-page.astro
<aside transition:name="hero">
new-page.astro
<aside transition:name="hero">

注意,提供的 transition:name 的值在每个页面上只能使用一次。当 Astro 无法自动推断出合适的名称,或需要更精确地控制匹配元素时,请手动设置这个名称。

添加于: astro@2.10.0

你可以使用 transition:persist 指令在页面导航时保持组件和 HTML 元素的状态(而不是替换它们)。

例如,以下 <video> 元素在你导航到包含相同视频元素的另一个页面时仍然会继续播放。这适用于前进和后退导航。

components/Video.astro
<video controls="" autoplay="" transition:persist>
<source src="https://ia804502.us.archive.org/33/items/GoldenGa1939_3/GoldenGa1939_3_512kb.mp4" type="video/mp4">
</video>

你也可以将指令放在一个 Astro 群岛 上(即带有 client: 指令 的 UI 框架组件)。如果该组件存在于下一个页面上,旧页面上的岛屿与其当前状态将继续显示,而不是用新页上的岛屿替换它。

在下面的示例中,当在包含具有 transition:persist 属性的 <Counter /> 组件的页面之间来回导航时,count 不会重置。

components/Header.astro
<Counter client:load transition:persist count={5} />

如果岛屿(或元素)在两个页面之间位于不同的组件中,你也可以手动标识相应的元素

pages/old-page.astro
<Video controls="" autoplay="" transition:name="media-player" transition:persist />
pages/new-page.astro
<MyVideo controls="" autoplay="" transition:name="media-player" transition:persist />

作为一种便捷的方式,transition:persist 还可以将过渡动画名称作为值传入。

pages/index.astro
<video controls="" autoplay="" transition:persist="media-player">

Astro 提供了一些内置的动画选项,用于覆盖默认的 fade 过渡动画。在单个元素上添加 transition:animate 指令,以自定义特定过渡的行为。

  • fade (默认):一个基于 Astro 的淡入淡出动画。旧内容淡出,新内容淡入。
  • initial: 退出 Astro 的默认淡入淡出动画,并使用浏览器的默认样式。
  • slide: 旧内容向左滑出,新内容从右侧滑入的动画。在后退导航时,动画是相反的。
  • none: 禁用浏览器的默认动画。在页面的 <html> 元素上使用,以禁用页面上每个元素的默认淡入淡出动画。

组合这些指令以完全控制页面动画。在 <html> 元素上设置页面默认值,并根据需要在任何单个元素上进行覆盖。

下面的示例为页面内容产生了一个滑动动画,同时禁用了页面其余部分的浏览器默认淡入淡出动画:

---
import { CommonHead } from '../components/CommonHead.astro';
---
<html transition:animate="none">
<head>
<CommonHead />
</head>
<body>
<header>
...
</header>
<!-- 覆盖单个元素上的页面默认值 -->
<main transition:animate="slide">
...
</main>
</body>
</html>

你可以使用任何 CSS 动画属性来自定义过渡的各个方面。

要自定义内置动画,请首先从 astro:transitions 导入该动画,然后传入自定义选项。

下面的示例展示了如何自定义了内置 fade 动画的持续时间:

---
import { fade } from 'astro:transitions';
---
<header transition:animate={fade({ duration: '0.4s' })}>

你还可以通过定义前进和后退行为以及新旧页面来为 transition:animate 定义自己的动画,具体类型如下所示:

export interface TransitionAnimation {
name: string; // 关键帧的名称
delay?: number | string;
duration?: number | string;
easing?: string;
fillMode?: string;
direction?: string;
}
export interface TransitionAnimationPair {
old: TransitionAnimation | TransitionAnimation[];
new: TransitionAnimation | TransitionAnimation[];
}
export interface TransitionDirectionalAnimations {
forwards: TransitionAnimationPair;
backwards: TransitionAnimationPair;
}

下面的示例展示了定义自定义 fade 动画所需的所有必要属性:

---
const anim = {
old: {
name: 'fadeIn',
duration: '0.2s',
easing: 'linear',
fillMode: 'forwards',
},
new: {
name: 'fadeOut',
duration: '0.3s',
easing: 'linear',
fillMode: 'backwards',
}
};
const myFade = {
forwards: anim,
backwards: anim,
};
---
<header transition:animate={myFade}> ... </header>

<ViewTransitions /> 路由器通过监听以下事件来处理导航:

  • 点击 <a> 元素。
  • 后退和前进导航事件。

如下选项允许你进一步控制在路由器中发生的导航事件:

只有关联的两个页面都使用了 <ViewTransitions /> 路由器才能通过客户端路由进行导航,防止重新加载全页面。你可能也不希望在每次导航更改时都使用客户端路由,而是更希望在特定路由上进行传统的页面导航。

你可以通过将 data-astro-reload 属性添加到任何 <a><form> 标签中,为单个链接选择不使用客户端路由器。此属性将覆盖任何现有的 <ViewTransitions /> 组件,而是在导航期间触发浏览器刷新。

以下示例显示了仅从主页导航到文章时如何阻止客户端路由器。这样,当从文章列表页面导航到同一页面时,仍然可以对共享元素(例如主视觉图)进行动画处理:

src/pages/index.astro
<a href="/articles/emperor-penguins" data-astro-reload>
src/pages/articles.astro
<a href="/articles/emperor-penguins">

具有 data-astro-reload 属性的链接将会被路由器忽略,并且进行全页面导航。

你还可以使用 navigate 方法在通常不被 <ViewTransitions /> 路由器监听的事件里触发客户端导航。该函数来自于 astro:transitions/client 模块,可以在任何客户端脚本和通过客户端指令激活了的客户端组件中使用。

下面的示例展示了一个当访客选择菜单中的选项时导航到另一个页面的 Astro 组件:

src/components/form.astro
<script>
import { navigate } from 'astro:transitions/client';
// 自动导航到所选选项
document.querySelector('select').onchange = (ev) => {
let href = ev.target.value;
navigate(href);
};
</script>
<select>
<option value="/play">Play</option>
<option value="/blog">Blog</option>
<option value="/about">About</option>
<option value="/contact">Contact</option>
</select>
src/pages/index.astro
---
import Form from "../components/form.astro"
import { ViewTransitions } from "astro:transitions";
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<Form />
</body>
</html>

下面的示例在一个 React 的 <Form /> 组件中使用 navigate() 实现了相同的功能:

src/components/form.jsx
import React from 'react';
import { navigate } from 'astro:transitions/client';
export default function ClickToNavigate({ to }) {
return <select onChange={(ev) => navigate(e.target.value)}>
<option value="/play">Play</option>
<option value="/blog">Blog</option>
<option value="/about">About</option>
<option value="/contact">Contact</option>
</select>;
}

这个 <Form /> 组件可以在使用 <ViewTransitions /> 路由器的 Astro 页面上带着客户端指令渲染:

src/pages/index.astro
---
import Form from "../components/form.jsx"
import { ViewTransitions } from "astro:transitions";
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<Form client:load />
</body>
</html>

navigate 方法接受以下参数:

  • href(必需项)- 要导航到的新页面。
  • options - 一个可选的对象,具有以下属性:
    • history: 'push' | 'replace' | 'auto'
      • 'push': 路由器将使用 history.pushState 在浏览器历史中创建一个新条目。
      • 'replace': 路由器将使用 history.replaceState 更新 URL,而不向导航添加新条目。
      • 'auto'(默认): 路由器将尝试使用 history.pushState,但如果无法转换到指定的 URL,则当前 URL 将保持不变,浏览器历史不会发生变化。
  • formData: 一个 POST 请求的 FormData 对象。

要想通过浏览器的历史记录进行后退和前进导航,你可以将 navigate() 与浏览器内置的 history.back()history.forward()history.go() 等函数结合使用。如果 navigate() 在服务器渲染时被调用,它不会有任何效果。

替换浏览器历史条目

段落标题 替换浏览器历史条目

通常情况下,你每次导航时都会在浏览器的历史记录中写入一个新条目。这样可以使用浏览器的 backforward 按钮在页面之间导航。

<ViewTransitions /> 路由器器允许你通过向任何单独的 <a> 标签添加 data-astro-history 属性来覆盖历史记录条目。

data-astro-history 属性可以设置为与 navigate() 函数的 history 选项 相同的三个值:

data-astro-history: 'push' | 'replace' | 'auto'

  • 'push':路由器将使用 history.pushState 在浏览器历史记录中创建一个新条目。
  • 'replace':路由器将使用 history.replaceState 更新 URL 而不添加新的导航条目。
  • 'auto'(默认值):路由器将尝试使用 history.pushState,但是如果 URL 不能进行过渡,则当前 URL 将保持不变,并且浏览器历史记录也不会发生任何变更。

以下示例导航到 /main 页面,但不向浏览历史记录中添加新条目,而是重用历史记录中的当前条目(/confirmation)并进行覆盖。

/confirmation.astro
<a href="/main" data-astro-history="replace">

这样做的效果是,如果你从 /main 页面返回,则浏览器不会显示 /confirmation 页面,而是显示该页面之前的页面。

表单的视图过渡动画

段落标题 表单的视图过渡动画

添加于: astro@4.0.0

<ViewTransitions /> 路由器将从 <form> 元素触发页面内的转换,支持 GETPOST 请求。

默认情况下,即使你使用 event.preventDefault() 来阻止此行为,你的页面也会在提交表单后进行转换。

你可以使用 data-astro-reload 属性在任何单独的表单上选择退出路由器视图过渡动画:

src/components/Form.astro
<form action="/contact" data-astro-reload>
<!-- -->
</form>

<ViewTransitions /> 路由器在支持视图过渡动画的浏览器(如 Chromium 浏览器)中表现最佳,但也包含了对其他浏览器的默认回退支持。即当浏览器不支持视图过渡动画 API 时,Astro 仍将提供基于浏览器的导航,使用其中一种回退选项来实现类似的效果。

你可以通过在 <ViewTransitions /> 组件上添加一个名为 fallback 的属性,并将其设置为 swapnone,来覆盖 Astro 的默认回退支持:

  • animate(默认,推荐使用)- Astro 将在更新页面内容之前使用自定义属性模拟视图过渡动画。
  • swap - Astro 不会尝试进行页面动画效果。相反,旧页面将立即被新页面替换。
  • none - Astro 将不进行任何页面过渡动画。在不支持的浏览器中,你将获得全量的页面导航。
---
import { ViewTransitions } from 'astro:transitions';
---
<title>My site</title>
<ViewTransitions fallback="swap">

<ViewTransitions /> 路由器被使用时,以下步骤将会产生 Astro 的客户端导航:

  1. 一个访客通过以下任何操作触发导航:

    • 点击链接到站点上另一个页面的 <a> 标签。
    • 点击后退按钮。
    • 点击前进按钮。
  2. 路由器开始获取下一个页面。

  3. 路由器将 data-astro-transition 属性添加到 HTML 元素中,并将其值设置为 'forward''back'

  4. 路由器调用 document.startViewTransition。这将触发浏览器自己的 视图过渡动画流程。重要的是,浏览器会对页面的当前状态进行截图。

  5. startViewTransition 回调中,路由器执行 swap,包括以下事件序列:

    • <head> 的内容被交换,其中一些元素被保留:

      • 如果新页面上存在,则保留样式表 DOM 节点,以防止 FOUC。
      • 如果新页面上存在,则保留脚本。
      • 如果新页面上存在对应的元素,则保留任何其他带有 transition:persist 的 head 元素。
    • <body> 被新页面的 <body> 完全替换。

    • 任何带有 transition:persist 的元素(如果存在)都会被移动到新的 DOM 中。

    • 如果需要,将恢复滚动位置。

    • astro:after-swap 事件在 document 上触发。这是 swap 过程的结束。

  6. 路由器等待任何新样式表加载完成,然后才会解析过渡动画。

  7. 路由器执行添加到页面的任何新脚本。

  8. astro:page-load 事件触发。这是导航流程的结束。

页面导航时的脚本行为

段落标题 页面导航时的脚本行为

在使用 <ViewTransitions /> 组件导航页面时,脚本将按顺序运行以匹配浏览器行为。

如果你有设置全局状态的代码,那么该状态需要考虑到该脚本可能会执行多次的情况。在 <script> 标签中检查全局状态,并在可能的情况下有条件地执行你的代码:

<script is:inline>
if(!window.SomeGlobal) {
window.SomeGlobal = {} // ....
}
</script>

模块脚本,包括那些为 DOMContentLoaded 添加事件监听器的脚本,只会执行一次,因为浏览器会跟踪已经加载的模块。你必须添加一个 生命周期事件 的监听器,以便在客户端页面导航期间重新执行脚本。

有关在项目中更新现有脚本的示例,请参阅 添加视图过渡动画教程

<ViewTransition /> 路由器在导航期间在 document 上触发了许多事件。这些事件提供了导航生命周期的钩子,允许你做一些事情,例如显示新页面正在加载的指示器、覆盖默认行为以及在导航完成时恢复状态。

导航过程包括一个准备阶段,在此阶段加载新内容;一个DOM 交换阶段,在此阶段旧页面的内容被新页面的内容替换;以及一个完成阶段,在此阶段执行脚本,报告加载完成,并进行清理工作。

Astro 的视图过渡动画 API 生命周期事件顺序如下:

虽然某些动作可以在任何事件中触发,但是某些任务只能在特定事件中执行,以获得最佳结果,例如在准备阶段之前显示加载指示器或在交换内容之前覆盖动画对。

添加于: astro@3.6.0

在准备阶段开始时触发的事件,导航开始后(例如,用户点击链接后),但在加载内容之前。

此事件用于:

  • 在加载开始之前做一些事情,例如显示加载指示器。
  • 更改加载内容,例如从模板中加载而不是从外部 URL 加载。
  • 在加载时更改 direction(通常为 forwardbackward)以进行自定义动画。

以下示例展示了如何在内容加载前使用 astro:before-preparation 事件加载一个加载指示器,并在加载完成后立即停止它。请注意,以这种方式使用加载器回调允许异步执行代码。

<script is:inline>
document.addEventListener('astro:before-preparation', ev => {
const originalLoader = ev.loader;
ev.loader = async function() {
const { startSpinner } = await import('./spinner.js');
const stop = startSpinner();
await originalLoader();
stop();
};
});
</script>

添加于: astro@3.6.0

在准备阶段结束时触发的事件,新页面的内容在加载并解析为文档后。此事件发生在视图过渡阶段之前。

此示例使用 astro:before-preparation 事件来启动加载指示器,使用 astro:after-preparation 事件来停止它:

<script is:inline>
document.addEventListener('astro:before-preparation', () => {
document.querySelector('#loading').classList.add('show');
});
document.addEventListener('astro:after-preparation', () => {
document.querySelector('#loading').classList.remove('show');
});
</script>

这是一个比上面显示的加载 spinner 更简单的版本:如果所有监听器的代码都可以同步执行,则无需进行加载器的回调。

添加于: astro@3.6.0

在新文档(在准备阶段填充)替换当前文档之前触发的事件。此事件发生在视图过渡中,用户仍然可以看到旧页面的快照。

此事件可用于在交换发生之前进行更改。事件上的 newDocument 属性表示传入的文档。以下示例确保浏览器中的浅色或深色模式偏好设置在新页面中得到保留:

<script is:inline>
function setDarkMode(document) {
let theme = localStorage.darkMode ? 'dark' : 'light';
document.documentElement.dataset.theme = theme;
}
setDarkMode(document);
document.addEventListener('astro:before-swap', ev => {
// Pass the incoming document to set the theme on it
setDarkMode(ev.newDocument);
});
</script>

astro:before-swap 事件还可用于更改交换的实现。默认的交换实现会对 head 内容进行 diff,将 persistent 元素从旧文档移动到 newDocument,然后用新文档的 body 替换整个 body

在生命周期的这一点上,你可以选择定义自己的交换实现,例如对现有文档的整个内容进行 diff(某些其他路由器会这样做):

<script is:inline>
document.addEventListener('astro:before-swap', ev => {
ev.swap = () => {
diff(document, ev.newDocument);
};
});
</script>

在新页面替换旧页面后立即触发的事件。你可以在 document 上监听此事件,并触发在新页面的 DOM 元素渲染和脚本运行之前发生的操作。

此事件在旧页面上监听时,可用于传递和恢复需要转移到新页面的 DOM 上的任何状态。

这是生命周期中的最后一个点,仍然可以安全地在其中添加一个深色模式类名(<html class="dark-mode">),尽管你可能希望在更早的事件中这样做。

astro:after-swap 事件发生在浏览器历史记录已更新并设置了滚动位置之后。因此,可以使用此事件的一个用途是覆盖默认的历史记录导航滚动恢复。以下示例将水平和垂直滚动位置重置为页面的左上角:

document.addEventListener('astro:after-swap',
() => window.scrollTo({ left: 0, top: 0, behavior: 'instant' }))

在页面导航结束时触发的事件,在新页面对用户可见且阻塞样式和脚本加载完成后。你可以在 document 上监听此事件。

<ViewTransitions /> 组件在预渲染页面的初始页面导航以及任何后续导航(向前或向后)时都会触发此事件。

你可以使用此事件在每次页面导航时运行代码,或者只运行一次:

<script>
document.addEventListener('astro:page-load', () => {
// This only runs once.
setupStuff();
}, { once: true });
</script>

启用客户端路由和动画页面过渡都会面临着无障碍挑战,而 Astro 旨在使启用了视图过渡动画的网站尽可能地具备默认的无障碍特性。

添加于: astro@3.2.0

<ViewTransitions /> 组件包含一个用于在客户端路由间进行页面导航的路由读出器。你无需配置或操作即可启用此功能。

辅助技术通过在导航后读出新页面标题来告知访客页面已被更改。在使用传统的全页面浏览器刷新服务端路由时,则会默认在新页面加载后执行此操作。而在客户端路由中,则由 <ViewTransitions /> 组件执行此操作。

为在客户端路由中添加路由读出,该组件会向新页面添加一个带有 aria-live 属性设置为 assertive 的元素。这告诉 AT(assistive technology,辅助技术)立即读出。该组件还按照优先顺序检查以下内容来确定读出的文本:

  • 如果存在 <title> 标签,则使用该标签。
  • 使用第一个找到的 <h1> 标签。
  • 使用页面的 pathname

我们强烈建议你始终在每个页面中包含 <title> 标签以更好地支持无障碍。

Astro 的 <ViewTransitions /> 组件包含一个 CSS 媒体查询,当检测到 prefer-reduced-motion 设置时,禁用所有视图过渡动画,包括回退动画。相反,浏览器将不使用动画,简单直接的切换 DOM 元素。

在 Astro v3.0 中,视图过渡动画不再是实验性的。

如果你 没有 在 Astro 2.x 中启用此实验性标志,则不会对你的项目造成任何破坏性的更改。新的视图过渡动画 API 不会影响你现有的代码。

如果你之前使用了实验性的视图过渡动画,那么从早期版本升级你的 Astro 项目时可能会有一些破坏性的更改。

请根据以下说明适当地升级 使用 experimental.viewTransitions: true 配置的 Astro v2.x 项目 到 v3.0。

experimental.viewTransitions 升级

段落标题 从 experimental.viewTransitions 升级

如果你之前启用了视图过渡动画的实验性标志,那么你需要更新你的项目到 Astro v3.0,因为现在 Astro 默认支持视图过渡动画。

移除 experimental.viewTransitions 标志

段落标题 移除 experimental.viewTransitions 标志

移除实验性标志:

astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
experimental: {
viewTransitions: true
}
});

<ViewTransitions /> 组件被从 astro:components 移动到了 astro:transitions。请在你的项目中更新导入来源。

src/layouts/BaseLayout.astro
---
import { ViewTransitions } from "astro:components astro:transitions"
---
<html lang="en">
<head>
<title>My Homepage</title>
<ViewTransitions />
</head>
<body>
<h1>Welcome to my website!</h1>
</body>
</html>

更新 transition:animate 指令

段落标题 更新 transition:animate 指令

改变: transition:animate 的值 morph 已重命名为 initial。此外,这不再是默认的动画。如果没有指定 transition:animate 指令,则你的动画现在默认为 fade

  1. 重命名任何 morph 动画为 initial

    src/components/MyComponent.astro
    <div transition:name="name" transition:animate="morph initial" />
  2. 为了保持任何以前默认使用 morph 的动画,显式添加 transition:animate="initial"

    src/components/MyComponent.astro
    <div transition:name="name" transition:animate="initial" />
  3. 你可以安全地删除任何显式设置为 fade 的动画。这是现在的默认行为:

    src/components/MyComponent.astro
    <div transition:name="name" transition:animate="fade" />

添加: Astro 还支持一个新的 transition:animate 值,none。这个值可以在页面的 <html> 元素上使用,以禁用整个页面上的动画过渡。这只会覆盖页面元素上默认的动画行为,而不是动画指令。你仍然可以在单个元素上设置动画,这些特定的动画将会发生。

  1. 你现在可以在单个页面上禁用所有默认的过渡动画,只对显式使用 transition:animate 指令的元素进行动画:

    <html transition:animate="none">
    <head></head>
    <body>
    <h1>Hello world!</h1>
    </body>
    </html>

事件 astro:load 已重命名为 astro:page-load。在你的项目中重命名所有 astro:load 出现的地方。

src/components/MyComponent.astro
<script>
document.addEventListener('astro:load astro:page-load', runSetupLogic);
</script>

事件 astro:beforeload 已重命名为 astro:after-swap。在你的项目中重命名所有 astro:beforeload 出现的地方。

src/components/MyComponent.astro
<script>
document.addEventListener('astro:beforeload astro:after-swap', setDarkMode);
</script>