Saltar al contenido principal

Tematización

El sistema de diseño detrás de NachUI — tokens semánticos, el contrato de temas y cómo adaptarlos a tu marca.

NachUI no tiene colores hardcodeados. Cada componente se pinta con tokens semánticos — bg-card, text-muted-foreground, border-rule — y esos tokens son variables CSS que te pertenecen. Cambiás una variable y todo el sistema se mueve con ella, en claro y en oscuro, sin tocar un solo componente.

Cómo funciona

La tematización pasa por tres capas. Entenderlas es casi todo lo que necesitás.
1. Una variable CSS guarda el valor. Se declara en :root y se sobrescribe dentro de .dark.
1:root {
2 --primary: oklch(54.6% 0.245 262.881);
3}
4
5.dark {
6 --primary: oklch(61.6% 0.245 262.9);
7}
2. @theme inline la mapea al espacio de colores de Tailwind. Esto es lo que convierte una variable en utilidades.
1@theme inline {
2 --color-primary: var(--primary);
3}
3. Tailwind genera las utilidades. Una vez que existe --color-primary, tenés bg-primary, text-primary, border-primary, ring-primary y todas las demás, gratis.
1<button className="bg-primary text-primary-foreground">Desplegar</button>
Como el paso 2 usa inline, la utilidad resuelve la variable en el momento de uso en vez de hornear un valor fijo. Por eso alcanza con activar .dark en el <html> para recolorear toda la app, sin re-render.
El modo oscuro se basa en una clase, declarada una sola vez:
1@custom-variant dark (&:where(.dark, .dark *));
Un token no es un color
Los tokens se nombran por rol, no por tono. --destructive significa "esta acción es peligrosa", no "esto es rojo". Mantener los nombres semánticos es lo que permite que un tema cambie todos los valores y siga teniendo sentido.

La convención foreground

Cada token de superficie viene en par con un token -foreground para el contenido que va encima. La superficie se queda con el nombre pelado; su contenido lleva el sufijo.
  • --card es la superficie, --card-foreground es el texto sobre esa superficie
  • --primary es el relleno, --primary-foreground es la etiqueta sobre ese relleno
1<div className="bg-card text-card-foreground">
2 <button className="bg-primary text-primary-foreground">Confirmar</button>
3</div>
Si respetás el par, el contraste se resuelve solo: quien escribe el tema solo tiene que mantener cada par legible, y todos los componentes que usan ese par heredan esa garantía.

Referencia de tokens

Superficies

El orden de apilado de la interfaz, desde el lienzo hasta las capas flotantes.
TokenUtilidadRol
--backgroundbg-backgroundEl lienzo de la página
--cardbg-cardPaneles y tarjetas elevadas sobre el lienzo
--popoverbg-popoverCapas flotantes: dropdowns, popovers, tooltips
--surface-mutedbg-surface-mutedZonas hundidas: cabeceras de tabla, filas en hover
--background, --card y --popover tienen cada uno su -foreground.

Texto

TokenUtilidadRol
--foregroundtext-foregroundTexto principal
--muted-strongtext-muted-strongTexto secundario que igual hay que poder leer
--muted-foregroundtext-muted-foregroundTexto terciario: epígrafes, pistas, etiquetas inactivas
--muted-strong está a propósito entre los otros dos. Los párrafos largos lo usan para que el cuerpo se lea más suave que los títulos sin caer al contraste de un epígrafe.

Interacción

TokenUtilidadRol
--primarybg-primaryLa acción principal de una pantalla
--secondarybg-secondaryAcciones de apoyo
--accentbg-accentEstados de hover y resalte sobre superficies neutras
--mutedbg-mutedZonas rellenas pero inertes
--inversebg-inverseContraste invertido a propósito, por ejemplo tooltips
Los cinco tienen su contraparte -foreground.

Estructura y foco

TokenUtilidadRol
--borderborder-borderBordes por defecto de los componentes
--border-interactiveborder-border-interactiveBordes de controles con los que se puede interactuar
--inputborder-inputBordes de campos de formulario
--ringring-ringAnillos de foco
--ruleborder-ruleFiletes editoriales: divisores de sección, rieles
--grid-colorFondos de puntos y grillas, usado directo en CSS
--border y --rule suelen tener el mismo valor, pero están separados a propósito: uno es cromo de componente, el otro es estructura de página. Retocar los filetes de tu layout no debería tocar todas las tarjetas.

Feedback

Cada uno de los cuatro tonos de feedback — destructive, warning, success, info — trae una escala de cinco roles en vez de un solo color. Eso es lo que permite que un botón sólido y una alerta suave compartan tono sin que uno de los dos falle el contraste.
SufijoEjemplo de utilidadRol
(ninguno)bg-destructiveRelleno sólido, para botones
-foregroundtext-destructive-foregroundContenido sobre ese relleno sólido
-texttext-destructive-textEl tono como texto legible sobre superficie clara
-surfacebg-destructive-surfaceFondo teñido para alertas y badges
-borderborder-destructive-borderBorde a juego con ese fondo teñido
Sólido — el relleno lleva el significado:
1<Button variant="destructive">Eliminar cuenta</Button>
Suave — lo lleva el tinte, y el texto se mantiene legible:
1<div className="bg-destructive-surface text-destructive-text border-destructive-border border">
2 Esta acción no se puede deshacer.
3</div>
Usar text-destructive sobre un fondo claro es el error típico: ese es el relleno del botón, calibrado para texto blanco, no para leerse. Usá -text en su lugar.

Scrim, elevación y código

TokenUtilidadRol
--overlaybg-overlayVelo detrás de diálogos y drawers
--elevation-sm / -md / -lgshadow-sm / -md / -lgLa escala de sombras
--codebg-codeSuperficie de los bloques de código
--code-plain, --code-keyword, --code-string, --code-function, --code-number, --code-tag, --code-comment, --code-punctuationColores de sintaxis, leídos directo por el bloque de código
Las sombras son valores tematizables, no drop shadows fijas: un tema oscuro les sube la opacidad, porque una sombra calibrada para un lienzo blanco desaparece sobre uno casi negro.

Radio

Un solo token base gobierna todas las esquinas del sistema.
1:root {
2 --radius: 1.5rem;
3}
La escala se deriva de ahí, así que cambiar la base reforma toda la app de manera proporcional:
UtilidadDerivaciónCon el 1.5rem por defecto
rounded-smcalc(var(--radius) - 0.75rem)12px
rounded-mdcalc(var(--radius) - 0.5rem)16px
rounded-lgvar(--radius)24px
rounded-xlcalc(var(--radius) + 0.5rem)32px
rounded-2xlcalc(var(--radius) + 1rem)40px
Para esquinas más filosas en todos lados, bajá la base:
1:root {
2 --radius: 0.5rem;
3}
--radius es el único token declarado solo en :root — una esquina no tiene variante clara y oscura.

Tipografía

Hay cuatro ranuras de fuente conectadas a Tailwind, así que font-sans, font-heading, font-serif y font-mono resuelven a lo que vos cargues.
1@theme inline {
2 --font-sans: var(--font-sans);
3 --font-heading: var(--font-heading);
4 --font-serif: var(--font-serif);
5 --font-mono: var(--font-mono);
6}
En Next.js, next/font define esas variables por vos:
1import { Bricolage_Grotesque } from 'next/font/google';
2
3export const fontHeading = Bricolage_Grotesque({
4 subsets: ['latin'],
5 variable: '--font-heading',
6 display: 'swap',
7});
Después ponés la clase generada en el <body> y los títulos la toman solos.

Por qué OKLCH

Todos los tokens están escritos en OKLCH:
1oklch(54.6% 0.245 262.881);
2/* ↑ ↑ ↑
3 | | tono, 0–360°
4 | croma, 0 = gris
5 luminosidad, 0% = negro, 100% = blanco */
Es perceptualmente uniforme, y eso importa cuando construís una escala. En HSL, hsl(60 100% 50%) y hsl(240 100% 50%) declaran la misma luminosidad mientras el amarillo se ve mucho más brillante que el azul. En OKLCH, luminosidad igual se ve igual de clara — así que mantener el valor L fijo entre tonos te da una paleta de feedback realmente equilibrada, en vez de una donde el badge de warning grita y el de info susurra.
Además llega a colores que sRGB no puede expresar y degrada bien en navegadores viejos. oklch.com es un buen lugar para elegir valores.

Personalizar tu tema

Sobrescribir un token

Editá la variable en tu CSS. Cambiala en los dos bloques — un valor puesto solo en :root se filtra al modo oscuro.
1:root {
2 --primary: oklch(65% 0.25 150);
3 --primary-foreground: oklch(100% 0 0);
4}
5
6.dark {
7 --primary: oklch(72% 0.22 150);
8 --primary-foreground: oklch(14.5% 0 0);
9}
No hay nada más que actualizar. Todo botón, link y anillo de foco que referencie --primary va detrás.

Agregar un token nuevo

Declaralo en ambos modos

1:root {
2 --brand: oklch(62% 0.19 310);
3 --brand-foreground: oklch(99% 0 0);
4}
5
6.dark {
7 --brand: oklch(70% 0.17 310);
8 --brand-foreground: oklch(14.5% 0 0);
9}

Exponelo a Tailwind

1@theme inline {
2 --color-brand: var(--brand);
3 --color-brand-foreground: var(--brand-foreground);
4}

Usá las utilidades

1<Badge className="bg-brand text-brand-foreground">Nuevo</Badge>
Saltear el paso 2 es la falla habitual: la variable existe, pero bg-brand nunca se genera y el elemento queda sin estilo.

El contrato de temas

Un tema no es un puñado de colores: es el conjunto completo de tokens, declarado en ambos bloques, :root y .dark. Esa lista vive en theme-contract.ts y está cubierta por un test.
La regla existe por cómo falla la cascada acá. Un tema que omite --success-surface no da error; hereda en silencio el valor de la paleta base, y terminás con un verde del tema por defecto filtrándose entre los colores de tu marca. Los tokens faltantes son invisibles hasta que alguien abre el único componente que los usa.
Así que cuando escribas un tema, recorré el contrato:
  • Superficiesbackground, card, popover, surface-muted y sus foregrounds
  • Interacciónprimary, secondary, accent, muted, inverse y sus foregrounds, más muted-strong
  • Estructuraborder, border-interactive, input, ring, rule, grid-color
  • Feedback — los cinco roles de destructive, warning, success e info
  • Scrim y elevaciónoverlay, elevation-sm, elevation-md, elevation-lg
  • Códigocode más los ocho tokens de sintaxis
  • Radio--radius, solo en :root

Cambiar de tema

nachui init trae los temas disponibles y escribe el que elijas en tu hoja de estilos global, debajo de un marcador /* NachUI Tokens */.
1npx nachui init
Volver a ejecutarlo reemplaza ese bloque y deja el resto de tu CSS intacto, así que podés probar un tema y arrepentirte. Para conservar una paleta que editaste a mano, mantené tus overrides debajo del marcador: todo lo que va después sobrevive.
Para ver cómo se aplica y se persiste la clase .dark, mirá Modo Oscuro.
¿Encontraste algo que mejorar?

¿Notaste un error, tipografía o detalle faltante en esta página? Ayúdanos a mejorar la documentación abriendo un issue en GitHub.

Crear un Issue
Ctrl+I