按鈕與切換

Theme Toggle

明暗主題切換按鈕:View Transitions 從點擊位置圓形擴散,Sun/Moon 圖示旋轉過渡。

把這個效果裝進你的專案

在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。

npx webberui@latest add theme-toggle

還沒有專案,或安裝遇到問題?看快速開始 →

Theme Toggle 是用 React、Motion(motion/react)與 Tailwind CSS 做的深色模式切換按鈕(dark mode toggle):支援 View Transitions API 的瀏覽器會從點擊位置以圓形擴散換成新主題,Sun/Moon 圖示同時旋轉過渡。可以單獨使用,自行切換 html 上的 dark class,也能接到 next-themes 等既有主題方案。

Playground

即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。

220
編輯中的呼叫預設屬性
<ThemeToggle/>

完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。

適用情境

  • 網站或文件站導覽列右上角的明暗主題切換
  • SaaS 後台的使用者選單或設定頁
  • 部落格、作品集這類想讓切換過程有點變化的個人網站
  • 不適合:需要「淺色/深色/跟隨系統」三選一的設定,這顆按鈕只在兩種主題之間切換,請改用選單或 Segmented Morph Toggle 這類分段控制

安裝

npx webberui@latest add https://webberui.com/r/theme-toggle.json

安裝時會一併寫入 --wb-duration-fast、--wb-ease-out CSS 變數到你的全域樣式。

使用

import { ThemeToggle } from "@/components/ui/theme-toggle";

// 未受控:自動 toggle html 的 dark class 並寫入 localStorage("theme")
<ThemeToggle />

// 受控:搭配 next-themes 等主題方案
<ThemeToggle
  isDark={resolvedTheme === "dark"}
  onToggle={(next) => setTheme(next ? "dark" : "light")}
/>

範例

Next.js App Router:避免首屏閃爍

未受控模式會把選擇寫進 localStorage 的 theme 鍵,但不會在載入時讀回來。在 root layout 放一段 inline script,於繪製前套上 dark class,重新整理頁面時就不會先閃一下淺色。

// app/layout.tsx
const themeScript = `
try {
  if (localStorage.getItem("theme") === "dark") {
    document.documentElement.classList.add("dark");
  }
} catch {}
`;

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-Hant" suppressHydrationWarning>
      <head>
        <script dangerouslySetInnerHTML={{ __html: themeScript }} />
      </head>
      <body>{children}</body>
    </html>
  );
}

suppressHydrationWarning 是因為 script 在 hydration 前就改了 html 的 class。按鈕本身會用 MutationObserver 跟上這個 class,不需要額外設定。

搭配 next-themes

受控模式下由 next-themes 管理主題與儲存,ThemeProvider 要設 attribute="class"。

"use client";

import { useTheme } from "next-themes";
import { ThemeToggle } from "@/components/ui/theme-toggle";

export function HeaderThemeToggle() {
  const { resolvedTheme, setTheme } = useTheme();

  return (
    <ThemeToggle
      isDark={resolvedTheme === "dark"}
      onToggle={(next) => setTheme(next ? "dark" : "light")}
    />
  );
}

調整擴散時長與按鈕樣式

duration 預設 220 毫秒;想要更明顯的擴散可以拉長,但過場期間文字會比較糊。className 會合併到按鈕上,可以改尺寸或拿掉框線。

<ThemeToggle duration={400} className="size-10 border-transparent shadow-none" />

Props

Prop型別預設值說明
isDarkboolean—受控模式:目前是否為深色主題。不傳則元件自行管理 html.dark 與 localStorage
onToggle(next: boolean) => void—切換時觸發,回傳切換後是否為深色。受控模式下請在此更新主題狀態
durationnumber220圓形擴散動畫時長(毫秒)。過場期間整頁是點陣快照,文字會發虛,所以刻意偏短
classNamestring—額外樣式

細節

  • View Transitions 支援度:Chrome / Edge 111+、Safari 18+ 支援 document.startViewTransition;Firefox 尚未支援。不支援的瀏覽器會直接切換主題(無擴散動畫),功能完全不受影響
  • 圓形擴散:以點擊座標為圓心,半徑取到視口最遠角落的距離,對 ::view-transition-new(root) 執行 clipPath 動畫;鍵盤觸發時以按鈕中心為圓心
  • 受控模式:onToggle 內的狀態更新會在 startViewTransition callback 中以 flushSync 同步套用,確保快照時機正確
  • SSR 安全:未受控時掛載後才讀取 html.dark,掛載前渲染中性圖示避免 hydration mismatch
  • 使用者系統開啟「減少動態效果」時,退化為直接切換,圖示過渡改為淡入淡出

常見問題

可以在 Next.js App Router 使用嗎?

可以。檔案開頭已有 "use client",在 layout 或 Server Component 頁面裡照常 import。未受控時掛載前會先顯示中性圖示,避免 hydration 不一致。

切換後 Tailwind 的 dark: 樣式沒有生效?

元件切換的是 html 上的 dark class,Tailwind 要設定成以 class 判斷深色模式。Tailwind v4 在 CSS 加上 @custom-variant dark (&:where(.dark, .dark *));,v3 則是在設定檔寫 darkMode: "class"。

Firefox 沒有圓形擴散?

圓形擴散依賴 document.startViewTransition,不支援的瀏覽器會直接切換主題,功能不受影響。系統開啟「減少動態效果」時也會直接切換,圖示改為淡入淡出。

需要另外加全域 CSS 嗎?

不需要。關閉瀏覽器預設 cross-fade 的 ::view-transition-old(root)/::view-transition-new(root) 樣式由元件內的 <style> 注入,CLI 或手動複製都一樣。

需要哪些依賴?

motion、lucide-react、clsx 與 tailwind-merge。