Tema claro y oscuro
Dos vistas de primera clase del mismo sistema, no un tema y su inversión: el oscuro tiene valores propios, afinados uno a uno. Los componentes no saben en qué tema están; los tokens sí.
Montar el tema
tokens.css declara el oscuro bajo la clase .dark y fija color-scheme en cada tema, así que las casillas, los selects y las barras de scroll nativas siguen al tema sin hacer nada. No hace falta ningún provider: ThemeToggle pone o quita la clase en <html> y guarda la elección en localStorage con la clave theme y los valores light, dark y system, los mismos que next-themes: si vienes de él, puedes quitarlo y nadie pierde su preferencia.
El selector aplica el tema al montarse; para que la primera pintura no salga en claro y salte, un script en el <head> hace lo mismo antes de pintar. Como cambia la clase antes de que React hidrate, suppressHydrationWarning calla esa diferencia, solo en ese elemento.
import { brandFontVariables } from "@unozero-ai/design-tokens/fonts";
import "./globals.css";
// Lo mismo que hace <uz-theme-toggle> al montarse, pero antes de pintar.
const theme = `try{var t=localStorage.getItem("theme"),d=t==="dark"||(t!=="light"&&matchMedia("(prefers-color-scheme: dark)").matches),r=document.documentElement;r.classList.toggle("dark",d);r.style.colorScheme=d?"dark":"light"}catch(e){}`;
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" className={brandFontVariables} suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: theme }} />
</head>
<body className="font-sans antialiased">{children}</body>
</html>
);
}El selector
ThemeToggle es un control segmentado de tres posiciones y no un interruptor a propósito: un interruptor solo aterriza en claro u oscuro, y el primer clic tiraría el modo automático con el que arranca la app sin forma de recuperarlo. En automático sigue a prefers-color-scheme en vivo. Varios selectores en la misma página se mueven juntos: pulsa la luna y cambia también el de la cabecera.
Dos vistas, no una inversión
Los dos valores de cada token, leídos del tokens.css instalado. Se ven los dos a la vez sea cual sea tu tema. El oscuro no invierte la rampa: el fondo es un neutro tintado, la tarjeta sube de luminosidad sobre él, los estados invierten texto y tinte y el foco pasa a cian.
--backgroundNeutro tintado a H278, no negro--cardEn oscuro sube sobre el fondo: la superficie se separa por luz--mutedLienzo y zonas en reposo--borderHairline--grid-lineEn claro oscurece; en oscuro aclara--foregroundTexto--muted-foregroundTexto secundario--primary-hoverEn claro oscurece; en oscuro aclara--ringAzul en claro, cian en oscuro--success-textTexto claro sobre tinte oscuro--success-subtleTinte de superficie--destructive-textTexto claro sobre tinte oscuro--destructive-subtleTinte de superficieEn tus componentes
Pinta con tokens y el oscuro llega solo, igual que a los componentes del kit, que no saben en qué tema están. La variante dark: de Tailwind queda para excepciones de estructura, nunca para elegir un color. Revisa cada pantalla nueva en los dos temas antes de darla por terminada.
// Bien: el token ya tiene su valor oscuro
<div className="border border-border bg-card text-card-foreground">…</div>
// Mal: duplica el tema a mano y se separa del paquete en la próxima versión
<div className="border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">…</div>