Modo escuro sem flash
Modo escuro que segue o sistema, lembra a escolha da pessoa e não pisca branco ao carregar. Receitas para Next.js (App Router) e Vite com Tailwind CSS v4.
Como o modo escuro funciona
O modo escuro é a classe .dark no <html>. Os tokens --aura-* trocam de valor com ela, então todos os componentes mudam juntos, sem re-render. O tema de cores fica em data-theme:
<html data-theme="aura" class="dark">- O
ThemeProviderguarda a escolha nolocalStorage(chavesaura-themeeaura-color-scheme) e, emsystem, segue o claro/escuro do sistema operacional. - Com o preset
aura-ui-tailwind, a variantedark:do Tailwind segue a classe.darke os 8 temas escuros (midnight, cyber, synthwave, matrix, volcano, grape, obsidian, terminal), não só a preferência do sistema.
Por que a tela pisca
O ThemeProvider roda no navegador depois que o HTML já apareceu. Sem ajuda, a primeira pintura sai com o tema padrão (claro) e só depois troca para o escuro: é o "flash" branco. A correção é um script pequeno no <head> que lê a escolha salva e ajusta o <html> antes de qualquer pintura. Este site faz exatamente isso.
Use as mesmas chaves e padrões
ThemeProvider (system e o tema aura). Se você mudar storageKey, colorSchemeKey ou defaultColorScheme no provider, mude também no script.Next.js (App Router)
Crie o script num arquivo próprio. A lista de temas vem do pacote, então ele continua certo quando surgirem temas novos:
import { AURA_DARK_THEMES, AURA_THEMES } from "aura-ui-core";
/** Roda no <head> antes da primeira pintura e aplica o tema salvo pelo ThemeProvider. */
export const themeScript = `(function () {
try {
var themes = ${JSON.stringify(AURA_THEMES)};
var darkThemes = ${JSON.stringify(AURA_DARK_THEMES)};
var mode = localStorage.getItem("aura-theme") || "system";
var scheme = localStorage.getItem("aura-color-scheme");
if (themes.indexOf(scheme) < 0) scheme = "aura";
var named = themes.indexOf(mode) >= 0 ? mode : null;
var dark = mode === "system"
? matchMedia("(prefers-color-scheme: dark)").matches
: mode === "dark" || darkThemes.indexOf(named) >= 0;
var html = document.documentElement;
html.dataset.theme = named || scheme;
html.classList.toggle("dark", dark);
} catch (e) {}
})();`;Coloque-o no <head> do layout raiz. O Providers é o mesmo da instalação (o ThemeProvider num componente "use client"):
import { Providers } from "./providers";
import { themeScript } from "./theme-script";
import "./globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
// O script muda o <html> antes do React: suppressHydrationWarning evita o aviso.
<html lang="pt-BR" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Vite (ou React sem SSR)
Sem servidor, o lugar é o index.html: o script roda antes do bundle do React. O ThemeProvider continua no main.tsx, como na instalação.
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script>
(function () {
try {
var themes = ["aura","midnight","ocean","forest","sunset","cyber","rose","nord","mocha","synthwave","amber","emerald","cherry","lavender","matrix","arctic","desert","volcano","grape","mint","steel","peach","indigo","lime","sakura","obsidian","solar","aqua","candy","mono","terminal","bubblegum","caramel"];
var darkThemes = ["midnight","cyber","synthwave","matrix","volcano","grape","obsidian","terminal"];
var mode = localStorage.getItem("aura-theme") || "system";
var scheme = localStorage.getItem("aura-color-scheme");
if (themes.indexOf(scheme) < 0) scheme = "aura";
var named = themes.indexOf(mode) >= 0 ? mode : null;
var dark = mode === "system"
? matchMedia("(prefers-color-scheme: dark)").matches
: mode === "dark" || darkThemes.indexOf(named) >= 0;
var html = document.documentElement;
html.dataset.theme = named || scheme;
html.classList.toggle("dark", dark);
} catch (e) {}
})();
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>Aqui a lista de temas fica escrita no script. Ao atualizar o aura-ui-core, confira se surgiram temas novos (AURA_THEMES e AURA_DARK_THEMES).
Botão de claro/escuro
O hook useTheme devolve o modo efetivo (resolvedTheme, já considerando o sistema) e toggleMode:
"use client";
import { Moon, Sun } from "lucide-react";
import { Button } from "aura-ui-components/button";
import { useTheme } from "aura-ui-core/client";
export function ModeToggle() {
const { resolvedTheme, toggleMode } = useTheme();
const dark = resolvedTheme === "dark";
return (
<Button
variant="ghost"
size="icon"
onClick={toggleMode}
aria-label={dark ? "Usar tema claro" : "Usar tema escuro"}
>
{dark ? <Sun /> : <Moon />}
</Button>
);
}Para oferecer as três opções (claro, escuro e sistema), use setTheme:
const { setTheme } = useTheme();
setTheme("dark"); // sempre escuro
setTheme("light"); // sempre claro
setTheme("system"); // volta a seguir o sistema operacionalQuer um seletor pronto com os 33 temas e o modo? Use o ThemeSwitcher.
Cores no modo escuro
Use os tokens (bg-aura-surface, text-aura-muted-foreground, border-aura-border…) em vez de cores fixas: eles já têm o valor do escuro. Deixe dark: para exceções:
{/* Tokens já trocam sozinhos no escuro: prefira-os */}
<div className="bg-aura-surface text-aura-surface-foreground border-aura-border" />
{/* dark: para exceções pontuais */}
<img className="dark:brightness-90" src="/foto.jpg" alt="" />Para ajustar a cor da sua marca só no escuro, sobrescreva os tokens em .dark[data-theme="minha-marca"] (veja Temas) ou monte o tema no gerador de temas, que confere o contraste nos dois modos.
Barras de rolagem e campos nativos
Barras de rolagem, o calendário do <input type="date"> e o fundo do autofill são do navegador. A propriedade color-scheme faz eles seguirem o modo:
/* globals.css — barras de rolagem, inputs de data e o autofill seguem o modo */
:root {
color-scheme: light;
}
:root.dark {
color-scheme: dark;
}