UI

Modo escuro

Alterne entre claro e escuro com a classe .dark, em Next.js, Vite ou Laravel + Inertia.

Todo tema Lens traz valores para claro e escuro. O modo escuro é ativado pela classe .dark: com ela no <html>, os tokens passam a usar os valores escuros. Sem ela, valem os claros.

<html lang="pt-BR" data-theme="neutro" class="dark">

A variante dark: do Tailwind segue a mesma classe. Na maioria dos casos você não precisa dela: bg-background e text-foreground já trocam de valor sozinhos. Reserve dark: para exceções, como trocar uma imagem.

Next.js

Use o next-themes. Ele aplica a classe antes da página aparecer, então não há flash de tema claro.

Instale

npm install next-themes

Crie o provider

components/theme-provider.tsx
"use client"

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

export function ThemeProvider(props: React.ComponentProps<typeof NextThemesProvider>) {
  return <NextThemesProvider {...props} />
}

Envolva o layout

attribute="class" faz o next-themes usar .dark. defaultTheme="system" segue a preferência do sistema operacional até a pessoa escolher.

app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR" data-theme="neutro" suppressHydrationWarning>
      <body>
        <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

O suppressHydrationWarning é necessário: o next-themes altera a classe do <html> antes do React hidratar.

Adicione um botão de alternância

npx shadcn@latest add @lenstech/button
components/mode-toggle.tsx
"use client"

import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"

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

  return (
    <Button
      variant="ghost"
      size="icon"
      className="relative"
      aria-label="Alternar modo escuro"
      onPress={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
    >
      <Sun className="scale-100 rotate-0 transition-transform dark:scale-0 dark:-rotate-90" />
      <Moon className="absolute scale-0 rotate-90 transition-transform dark:scale-100 dark:rotate-0" />
    </Button>
  )
}

Os dois ícones ficam no HTML e o CSS escolhe qual mostrar. Assim o botão não depende do tema já ter carregado no cliente.

data-theme não é o tema do next-themes

O next-themes cuida só de claro, escuro e sistema, pela classe. O data-theme escolhe a marca do cliente e fica fixo no <html>. Não passe attribute="data-theme" para o next-themes: ele sobrescreveria o tema do cliente.

Vite e Laravel + Inertia

Sem next-themes, um script pequeno resolve. Ele precisa rodar no <head>, antes do CSS pintar a página, para evitar o flash de tema claro.

index.html
<head>
  <script>
    try {
      const salvo = localStorage.getItem("modo")
      const escuro = salvo ? salvo === "escuro" : matchMedia("(prefers-color-scheme: dark)").matches
      document.documentElement.classList.toggle("dark", escuro)
    } catch {}
  </script>
</head>

No Laravel, o mesmo script vai no <head> de resources/views/app.blade.php. O starter kit de React já traz uma solução equivalente (o hook useAppearance); se o seu projeto usa o starter kit, mantenha a dele.

Para alternar, troque a classe e salve a escolha:

src/components/mode-toggle.tsx
import { Moon, Sun } from "lucide-react"
import { Button } from "@/components/ui/button"

export function ModeToggle() {
  function alternar() {
    const escuro = document.documentElement.classList.toggle("dark")
    try {
      localStorage.setItem("modo", escuro ? "escuro" : "claro")
    } catch {}
  }

  return (
    <Button variant="ghost" size="icon" className="relative" aria-label="Alternar modo escuro" onPress={alternar}>
      <Sun className="scale-100 rotate-0 transition-transform dark:scale-0 dark:-rotate-90" />
      <Moon className="absolute scale-0 rotate-90 transition-transform dark:scale-100 dark:rotate-0" />
    </Button>
  )
}

Escuro só em uma seção

.dark funciona em qualquer elemento, não só no <html>. Tudo dentro dele usa os valores escuros. É útil para um rodapé ou uma faixa de destaque escura numa página clara.

<footer className="dark bg-background text-foreground">
  <p className="text-muted-foreground">Atendimento de segunda a sexta, das 8h às 18h.</p>
</footer>

Aplique bg-background e text-foreground no próprio elemento. A classe troca os valores das variáveis, mas o fundo só muda se algo usar o token.

Tema de cliente e modo juntos

data-theme escolhe a marca. .dark escolhe o modo. Os dois são independentes e podem ficar no mesmo elemento ou em elementos aninhados.

MarcaçãoResultado
<html data-theme="clinica-sorriso">Clínica Sorriso, claro.
<html data-theme="clinica-sorriso" class="dark">Clínica Sorriso, escuro.
<html class="dark"> + <section data-theme="exemplo-verde">Seção com o tema verde, escuro.
<html data-theme="exemplo-verde"> + <section class="dark">Seção com o tema verde, escuro; o resto da página, claro.

Um projeto de cliente normalmente tem um tema só: o theme-<name> grava as cores em :root e .dark. As combinações acima importam quando você carrega vários temas de uma vez, como neste site. Use o seletor de tema e o botão de modo no topo da página para ver os dois funcionando juntos.

On this page