Pular para o conteúdo
// visualizar em
salvo em todas as páginas

Modo escuro

Claro, escuro ou o que o sistema escolher, por página ou por ilha. O modo escuro é CSS puro, então a primeira pintura já sai certa.

// você vai aprender

  • O que mode="light" | "dark" | "system" faz, e o que herda esse valor
  • Por que não há piscada na primeira pintura, e como manter assim com uma escolha salva
  • Como colorir a interface do navegador com theme-color

Experimente

Esta ilha define mode no próprio StyleProvider, independente do que diz a barra "visualizar em". Escolha Sistema e mude a configuração do seu sistema operacional: ela acompanha sem recarregar.

minimal · light · #3fa34d

Armazenamento

Usado38,2 GB de 50 GB
x

Como funciona

Todo token de cor é escrito com light-dark(): um valor para cada modo numa única declaração. O navegador escolhe o lado pelo color-scheme, e o data-mode define esse valor:

tokens.css (trecho)
:root { color-scheme: light dark; }
[data-mode="light"] { color-scheme: light; }
[data-mode="dark"] { color-scheme: dark; }
[data-mode="system"] { color-scheme: light dark; }

:where(:root, [data-style]) {
  --cml-surface: light-dark(#ffffff, #09090b);
  --cml-fg: light-dark(#09090b, #fafafa);
}

color-scheme: light dark quer dizer "siga o prefers-color-scheme". O color-scheme é herdado, então um provider sem mode simplesmente pega o do pai, e a raiz da página segue o sistema sem script nenhum. As partes nativas (barras de rolagem, seletores de data, preenchimento automático) acompanham também.

modedata-modeResultado
omitidonenhumHerda do pai; na raiz, segue o sistema
"system"systemSegue o sistema, mesmo dentro de um pai claro ou escuro
"light"lightSempre claro
"dark"darkSempre escuro
DICA

Diálogos, menus e toasts renderizam num portal. Eles levam o modo do provider pelo contexto do React, então um popup aberto a partir de uma ilha escura também é escuro.

Escolha um padrão

Na maioria dos apps, omita o mode ou use "system" no provider do topo: a página acompanha o sistema operacional desde o primeiro byte. Use um modo fixo em áreas que precisam ficar sempre iguais, como um painel de código escuro ou uma prévia de impressão clara.

app/providers.tsx
<StyleProvider styleName="minimal" mode="system">
  <App />
  <StyleProvider styleName="minimal" mode="dark">
    <CodePanel />
  </StyleProvider>
</StyleProvider>

Salve a escolha de quem visita sem piscar

Um botão que guarda a escolha no localStorage e a aplica depois da hidratação pinta o modo errado primeiro. Em vez disso, renderize o modo salvo no servidor: guarde-o num cookie e espalhe no <html> com styleScopeProps, que é seguro de chamar num Server Component.

app/layout.tsx
import { cookies } from "next/headers";
import { styleScopeProps } from "@camaleo/react/style-scope";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const saved = (await cookies()).get("mode")?.value;
  const mode = saved === "light" || saved === "dark" ? saved : "system";
  return (
    <html lang="pt-BR" {...styleScopeProps({ styleName: "minimal", mode })}>
      <body>{children}</body>
    </html>
  );
}

O botão grava o cookie e o atributo; nada precisa renderizar de novo:

mode-toggle.tsx
"use client";

export function setMode(mode: "light" | "dark" | "system") {
  document.cookie = `mode=${mode}; path=/; max-age=31536000; samesite=lax`;
  document.documentElement.setAttribute("data-mode", mode);
}
CUIDADO

Não dê mode aos providers internos se a raiz cuida disso: um mode fixo num provider aninhado vence o botão. E ler cookies() faz a rota renderizar a cada requisição.

Num site estático ou numa SPA sem servidor, a única opção sem piscada é um pequeno script inline no <head> que roda antes da primeira pintura:

index.html
<script>
  try {
    document.documentElement.setAttribute("data-mode", localStorage.getItem("mode") || "system");
  } catch {}
</script>

Pinte a interface do navegador

O theme-color tinge a barra de endereço e a tela de apps recentes nos navegadores de celular. Dê a ele um valor por esquema, usando a cor de superfície do seu estilo. No Next, exporte-o do layout raiz:

app/layout.tsx
import type { Viewport } from "next";

export const viewport: Viewport = {
  themeColor: [
    { media: "(prefers-color-scheme: light)", color: "#ffffff" },
    { media: "(prefers-color-scheme: dark)", color: "#09090b" },
  ],
};

As media queries seguem o sistema. Se quem visita pode forçar um modo, o botão também deve trocar o content das duas tags theme-color para a cor do modo forçado.

Antes de publicar

  • O provider do topo usa "system" ou nenhum mode
  • Uma escolha salva é renderizada pelo servidor, ou aplicada por um script inline antes da pintura
  • Só as áreas que precisam ficar sempre iguais têm um mode fixo
  • O theme-color bate com a superfície do estilo nos dois modos
Esta página foi útil? · Editar esta página no GitHub