---
title: 深色模式
description: 在 HeroUI v3 中添加浅色、深色以及跟随系统的主题切换
icon: moon
links:
  themes: true
  tailwind: theme
---

HeroUI 的深色模式由 CSS 驱动。组件会从根元素读取主题变量，因此你不需要使用 HeroUI Provider。只需在 `<html>` 上添加 `dark` 类或 `data-theme="dark"`，HeroUI 就会应用深色主题。

```html
<html class="dark" data-theme="dark">
  <body class="bg-background text-foreground">
    <!-- Your app -->
  </body>
</html>
```

请保持你的应用根元素上有 `bg-background` 与 `text-foreground` 类，这样页面画布才会随着当前主题变化。

<Callout type="info">
  HeroUI 内置的浅色和深色主题同时响应 `.light` / `.dark` 类以及 `data-theme="light"` / `data-theme="dark"` 属性。如果你手动同时设置两者，请确保它们的值保持一致。
</Callout>

## 在 Next.js 中使用 next-themes

当你在 Next.js 应用中需要主题持久化、系统偏好支持，并希望在水合（hydration）前不出现闪烁时，请使用 [next-themes](https://github.com/pacocoursey/next-themes)。

### 安装 next-themes

<Tabs items={["npm", "pnpm", "yarn", "bun"]}>
  <Tab value="npm">
    ```bash
    npm i next-themes
    ```
  </Tab>
  <Tab value="pnpm">
    ```bash
    pnpm add next-themes
    ```
  </Tab>
  <Tab value="yarn">
    ```bash
    yarn add next-themes
    ```
  </Tab>
  <Tab value="bun">
    ```bash
    bun add next-themes
    ```
  </Tab>
</Tabs>

### App Router

为 `next-themes` 创建一个客户端 Provider。

```tsx
// app/providers.tsx
"use client";

import {ThemeProvider as NextThemesProvider} from "next-themes";

export function Providers({children}: {children: React.ReactNode}) {
  return (
    <NextThemesProvider
      attribute="class"
      defaultTheme="system"
      enableSystem
      disableTransitionOnChange
    >
      {children}
    </NextThemesProvider>
  );
}
```

在根布局中用该 Provider 包裹你的应用。请在 `<html>` 上添加 `suppressHydrationWarning`，因为 `next-themes` 会在水合之前更新该元素。

```tsx
// app/layout.tsx
import "./globals.css";

import {Providers} from "./providers";

export default function RootLayout({children}: {children: React.ReactNode}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className="bg-background text-foreground">
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
```

### 主题切换器

在客户端组件中使用 `next-themes` 提供的 `useTheme`。请等到组件挂载后再渲染，因为在 SSR 期间无法得知当前激活的主题。

```tsx
// app/components/theme-switcher.tsx
"use client";

import {Button} from "@heroui/react";
import {useTheme} from "next-themes";
import {useEffect, useState} from "react";

export function ThemeSwitcher() {
  const [mounted, setMounted] = useState(false);
  const {resolvedTheme, setTheme, theme} = useTheme();

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;

  const activeTheme = theme === "system" ? resolvedTheme : theme;

  return (
    <div className="flex items-center gap-2">
      <Button
        variant={activeTheme === "light" ? "primary" : "secondary"}
        onPress={() => setTheme("light")}
      >
        Light
      </Button>
      <Button
        variant={activeTheme === "dark" ? "primary" : "secondary"}
        onPress={() => setTheme("dark")}
      >
        Dark
      </Button>
      <Button variant={theme === "system" ? "primary" : "secondary"} onPress={() => setTheme("system")}>
        System
      </Button>
    </div>
  );
}
```

### Pages Router

如果使用 `pages/`，请在 `pages/_app.tsx` 中包裹你的应用。

```tsx
// pages/_app.tsx
import "@/styles/globals.css";

import type {AppProps} from "next/app";
import {ThemeProvider as NextThemesProvider} from "next-themes";

export default function App({Component, pageProps}: AppProps) {
  return (
    <NextThemesProvider
      attribute="class"
      defaultTheme="system"
      enableSystem
      disableTransitionOnChange
    >
      <Component {...pageProps} />
    </NextThemesProvider>
  );
}
```

## 使用自定义主题名称

`attribute="class"` 这套配置非常适合内置的 `light` 和 `dark` 主题。如果你的自定义主题 CSS 是基于 `data-theme` 选择器编写的，请改为让 `next-themes` 写入 `data-theme`。

```tsx
<NextThemesProvider
  attribute="data-theme"
  defaultTheme="system"
  enableSystem
  themes={["light", "dark", "ocean", "ocean-dark"]}
>
  {children}
</NextThemesProvider>
```

当你传入自定义的 `themes` 列表时，如果仍然希望保留内置主题，请将 `"light"` 和 `"dark"` 一并包含进去。

## 在 React 中使用 useTheme

当你正在构建一个普通的 React 应用（例如 Vite 或 Create React App），并且不需要 `next-themes` 时，可以使用 HeroUI 提供的 `useTheme` 钩子。

该钩子从 `@heroui/react` 中导出。它会将当前选择的主题保存到 `localStorage`，根据用户的操作系统偏好解析 `"system"`，并同时把对应的类与 `data-theme` 属性应用到 `<html>` 上。

```tsx
// src/components/theme-switcher.tsx
import {Button, useTheme} from "@heroui/react";

export function ThemeSwitcher() {
  const {resolvedTheme, setTheme, theme} = useTheme("system");

  return (
    <div className="flex items-center gap-2">
      <Button
        variant={resolvedTheme === "light" ? "primary" : "secondary"}
        onPress={() => setTheme("light")}
      >
        Light
      </Button>
      <Button
        variant={resolvedTheme === "dark" ? "primary" : "secondary"}
        onPress={() => setTheme("dark")}
      >
        Dark
      </Button>
      <Button variant={theme === "system" ? "primary" : "secondary"} onPress={() => setTheme("system")}>
        System
      </Button>
    </div>
  );
}
```

<Callout type="warning">
  每个应用只应使用一个主题控制器。在 Next.js 中，推荐使用 `next-themes` 及其 `useTheme` 钩子；在普通的 React 应用中，请使用 `@heroui/react` 提供的 `useTheme`。
</Callout>

## 同时为两种主题设置样式

主题相关的工具类会自动生效，因为它们读取的是 CSS 变量：

```tsx
<main className="min-h-screen bg-background text-foreground">
  <section className="bg-surface text-surface-foreground shadow-surface">
    Theme-aware content
  </section>
</main>
```

对于仅在深色模式下生效的一次性样式调整，请使用 `dark:` 变体：

```tsx
<div className="bg-background text-foreground dark:border-default">
  Custom dark-mode adjustment
</div>
```
