为你的 Remix 应用添加暗黑模式。

暗黑模式

修改你的 tailwind.css 文件

:root[class~="dark"] 添加到你的 tailwind.css 文件中。这将允许你在 HTML 元素上使用 dark 类来应用暗黑模式样式。

app/tailwind.css

  1. .dark,
  2. :root[class~="dark"] {
  3. ...;
  4. }

安装 remix-themes

首先安装 remix-themes

pnpm npmyarn bun

  1. pnpm add remix-themes

创建会话存储和主题会话解析器

app/sessions.server.tsx

  1. import { createThemeSessionResolver } from "remix-themes"
  2. // 如果未设置 process.env.NODE_ENV,可以默认为 'development'
  3. const isProduction = process.env.NODE_ENV === "production"
  4. const sessionStorage = createCookieSessionStorage({
  5. cookie: {
  6. name: "theme",
  7. path: "/",
  8. httpOnly: true,
  9. sameSite: "lax",
  10. secrets: ["s3cr3t"],
  11. // 只有在生产环境中设置 domain 和 secure
  12. ...(isProduction
  13. ? { domain: "your-production-domain.com", secure: true }
  14. : {}),
  15. },
  16. })
  17. export const themeSessionResolver = createThemeSessionResolver(sessionStorage)

设置 Remix 主题

ThemeProvider 添加到你的根布局中。

app/root.tsx

  1. import clsx from "clsx"
  2. import { PreventFlashOnWrongTheme, ThemeProvider, useTheme } from "remix-themes"
  3. import { themeSessionResolver } from "./sessions.server"
  4. // 使用 loader 从会话存储中返回主题
  5. export async function loader({ request }: LoaderFunctionArgs) {
  6. const { getTheme } = await themeSessionResolver(request)
  7. return {
  8. theme: getTheme(),
  9. }
  10. }
  11. // 用 ThemeProvider 包裹你的应用。
  12. // `specifiedTheme` 是存储在会话存储中的主题。
  13. // `themeAction` 是用于在会话存储中更改主题的操作名称。
  14. export default function AppWithProviders() {
  15. const data = useLoaderData<typeof loader>()
  16. return (
  17. <ThemeProvider specifiedTheme={data.theme} themeAction="/action/set-theme">
  18. <App />
  19. </ThemeProvider>
  20. )
  21. }
  22. export function App() {
  23. const data = useLoaderData<typeof loader>()
  24. const [theme] = useTheme()
  25. return (
  26. <html lang="en" className={clsx(theme)}>
  27. <head>
  28. <meta charSet="utf-8" />
  29. <meta name="viewport" content="width=device-width, initial-scale=1" />
  30. <Meta />
  31. <PreventFlashOnWrongTheme ssrTheme={Boolean(data.theme)} />
  32. <Links />
  33. </head>
  34. <body>
  35. <Outlet />
  36. <ScrollRestoration />
  37. <Scripts />
  38. <LiveReload />
  39. </body>
  40. </html>
  41. )
  42. }

添加操作路由

/routes/action.set-theme.ts 创建一个文件。确保将文件名传递给 ThemeProvider 组件。此路由用于在用户更改主题时将首选主题存储到会话存储中。

app/routes/action.set-theme.ts

  1. import { createThemeAction } from "remix-themes"
  2. import { themeSessionResolver } from "./sessions.server"
  3. export const action = createThemeAction(themeSessionResolver)

添加模式切换

在你的站点上添加一个模式切换按钮,以在光明模式和暗黑模式之间切换。

components/mode-toggle.tsx

  1. import { Moon, Sun } from "lucide-react"
  2. import { Theme, useTheme } from "remix-themes"
  3. import { Button } from "./ui/button"
  4. import {
  5. DropdownMenu,
  6. DropdownMenuContent,
  7. DropdownMenuItem,
  8. DropdownMenuTrigger,
  9. } from "./ui/dropdown-menu"
  10. export function ModeToggle() {
  11. const [, setTheme] = useTheme()
  12. return (
  13. <DropdownMenu>
  14. <DropdownMenuTrigger asChild>
  15. <Button variant="ghost" size="icon">
  16. <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
  17. <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
  18. <span className="sr-only">切换主题</span>
  19. </Button>
  20. </DropdownMenuTrigger>
  21. <DropdownMenuContent align="end">
  22. <DropdownMenuItem onClick={() => setTheme(Theme.LIGHT)}>
  23. 光明模式
  24. </DropdownMenuItem>
  25. <DropdownMenuItem onClick={() => setTheme(Theme.DARK)}>
  26. 暗黑模式
  27. </DropdownMenuItem>
  28. </DropdownMenuContent>
  29. </DropdownMenu>
  30. )
  31. }