Dark Mode
Modo escuro, esquemas de cores e temas dinâmicos com Tailwind CSS v4.
Busque em todas as páginas da documentação
Modo escuro, esquemas de cores e temas dinâmicos com Tailwind CSS v4.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
// Utilitários de modo escuro - basta prefixar com dark:
<div className="bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100">
<p className="text-gray-600 dark:text-gray-400">Adapta-se à preferência do sistema</p>
<button className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400">
Clique em mim
</button>
</div>/* globals.css - Tailwind v4 usa prefers-color-scheme por padrão */
@import "tailwindcss";
/* Para modo escuro baseado em classe (alternado via JS): */
@variant dark (&:where(.dark, .dark *));
/* Tokens semânticos que alternam automaticamente */
@theme {
--color-surface: #ffffff;
--color-on-surface: #111827;
}
@layer base {
.dark {
--color-surface: #0f172a;
--color-on-surface: #f1f5f9;
}
}Quando usar isso: Toda aplicação voltada para o usuário deve suportar o modo escuro - reduz a fadiga ocular e respeita as preferências do usuário.
"use client";
import { useEffect, useState } from "react";
type Theme = "light" | "dark" | "system";
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("system");
useEffect(() => {
const stored = localStorage.getItem("theme") as Theme | null;
if (stored) setTheme(stored);
}, []);
useEffect(() => {
const root = document.documentElement;
if (theme === "system") {
root.classList.remove("dark");
const mq = window.matchMedia("(prefers-color-scheme: dark)");
if (mq.matches) root.classList.add("dark");
const handler = (e: MediaQueryListEvent) => {
root.classList.toggle("dark", e.matches);
};
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}
root.classList.toggle("dark", theme === "dark");
localStorage.setItem("theme", theme);
}, [theme]);
return (
<div className="min-h-screen bg-surface text-on-surface transition-colors duration-300">
<header className="flex items-center justify-between border-b border-gray-200 p-4 dark:border-gray-800">
<h1 className="text-lg font-bold">Meu App</h1>
<ThemeToggle theme={theme} setTheme={setTheme} />
</header>
<main className="mx-auto max-w-4xl p-6">{children}</main>
</div>
);
}
function ThemeToggle({
theme,
setTheme,
}: {
theme: Theme;
setTheme: (t: Theme) => void;
}) {
const options: { value: Theme; label: string }[] = [
{ value: "light", label: "Claro" },
{ value: "dark", label: "Escuro" },
{ value: "system", label: "Sistema" },
];
return (
<div className="flex gap-1 rounded-lg bg-gray-100 p-1 dark:bg-gray-800">
{options.map((opt) => (
<button
key={opt.value}
onClick={() => setTheme(opt.value)}
className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${
theme === opt.value
? "bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white"
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
}`}
>
{opt.label}
</button>
))}
</div>
);
}/* globals.css para o exemplo acima */
@import "tailwindcss";
@variant dark (&:where(.dark, .dark *));
@theme {
--color-surface: #ffffff;
--color-on-surface: #111827;
--color-surface-muted: #f9fafb;
--color-primary: #2563eb;
--color-primary-foreground: #ffffff;
}
@layer base {
.dark {
--color-surface: #0f172a;
--color-on-surface: #f1f5f9;
--color-surface-muted: #1e293b;
--color-primary: #3b82f6;
--color-primary-foreground: #ffffff;
}
}O que isso demonstra:
localStoragematchMediatransition-colorsdark: do Tailwind v4 usa @media (prefers-color-scheme: dark) - nenhuma configuração é necessária@variant dark (&:where(.dark, .dark *)) em seu CSS:where() mantém a especificidade em zero, então dark:bg-gray-900 não supera bg-white com base na especificidadenext-themes é a biblioteca padrão para modo escuro em Next.js - ela lida com SSR, prevenção de flash e sincronização do sistemaUsando next-themes:
// app/layout.tsx
import { ThemeProvider } from "next-themes";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</body>
</html>
);
}
// Uso do componente
"use client";
import { useTheme } from "next-themes";
function Toggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
Alternar
</button>
);
}Múltiplos temas (além de claro/escuro):
@layer base {
[data-theme="ocean"] {
--color-primary: #0891b2;
--color-surface: #f0fdfa;
}
[data-theme="sunset"] {
--color-primary: #ea580c;
--color-surface: #fff7ed;
}
}Imagens cientes do modo escuro:
<picture>
<source srcSet="/logo-dark.svg" media="(prefers-color-scheme: dark)" />
<img src="/logo-light.svg" alt="Logo" />
</picture>
{/* Ou com Tailwind: */}
<img src="/logo-light.svg" className="dark:hidden" alt="Logo" />
<img src="/logo-dark.svg" className="hidden dark:block" alt="Logo" />// Tipo de Tema
type Theme = "light" | "dark" | "system";
// Tipos do next-themes
import type { ThemeProviderProps } from "next-themes";
// Cores de tema type-safe
const themeColors = {
light: { bg: "bg-white", text: "text-gray-900" },
dark: { bg: "bg-gray-950", text: "text-gray-100" },
} as const satisfies Record<string, { bg: string; text: string }>;Flash de tema incorreto (FOIT) - Sem tratamento SSR, a página exibe brevemente o tema incorreto. Correção: Use next-themes com suppressHydrationWarning em <html>, ou adicione um <script> bloqueador em <head> que define a classe.
prefers-color-scheme vs alternância de classe - O comportamento padrão da v4 usa a media query. Se você adicionar a alternância de classe .dark, deve substituir o variante. Correção: Adicione @variant dark (&:where(.dark, .dark *)).
Modificadores de opacidade no modo escuro - bg-blue-500/50 funciona, mas dark:bg-blue-500/50 pode não compor com tokens semânticos. Correção: Defina valores de opacidade separados em seus tokens de tema.
Contraste de cor - O modo escuro não é apenas "inverter tudo". Texto claro sobre fundos escuros precisa de diferentes relações de contraste. Correção: Teste com verificadores de contraste WCAG.
Imagens e sombras - Imagens brilhantes e sombras escuras parecem erradas no modo escuro. Correção: Reduza a intensidade da sombra (dark:shadow-none), adicione sobreposições em imagens ou use o filtro brightness.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
next-themes | App Next.js com SSR (previne flash) | Projetos não-Next.js |
CSS color-scheme: dark | Você quer que a UI do navegador (barras de rolagem, inputs) se adapte | Você precisa de temas de componentes personalizados |
Apenas prefers-color-scheme | Você quer modo escuro automático baseado no sistema | Usuários precisam de controle manual de alternância |
Tailwind darkMode: "selector" (v3) | Você está usando Tailwind v3 | Você está usando v4 |
Por padrão, dark: usa @media (prefers-color-scheme: dark) - ele segue a preferência do sistema operacional do usuário sem necessidade de configuração.
Adicione isso ao seu CSS:
@variant dark (&:where(.dark, .dark *));Em seguida, alterne a classe .dark no elemento <html> via JavaScript.
--color-surface e --color-on-surface:root, substituídas em .darkbg-surface em vez de bg-white dark:bg-gray-950next-themes. Ela lida com SSR, previne o flash de tema incorreto, suporta a preferência do sistema e funciona com a estratégia de atributo class.
next-themes adiciona o atributo class no cliente antes da hidratação. Sem suppressHydrationWarning, o React avisa sobre a incompatibilidade servidor/cliente.
@layer base {
[data-theme="ocean"] {
--color-primary: #0891b2;
}
[data-theme="sunset"] {
--color-primary: #ea580c;
}
}Use um atributo data-theme em vez da classe .dark.
<img src="/logo-light.svg" className="dark:hidden" alt="Logo" />
<img src="/logo-dark.svg" className="hidden dark:block" alt="Logo" />O servidor renderiza sem saber a preferência de tema do usuário. O cliente então aplica o tema correto, causando um flash visível. Corrija usando next-themes ou adicionando um <script> bloqueador em <head> que define a classe antes da renderização.
A luminosidade oklch não é simétrica. Simplesmente mudar o número da tonalidade não garante um bom contraste no modo escuro. Teste as cores visualmente em vez de inverter os números.
type Theme = "light" | "dark" | "system";Para next-themes, importe ThemeProviderProps de next-themes para suporte completo de tipo.
const themeColors = {
light: { bg: "bg-white", text: "text-gray-900" },
dark: { bg: "bg-gray-950", text: "text-gray-100" },
} as const satisfies Record<string, { bg: string; text: string }>;Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥