Este sitio funciona sobre un sistema de diseño llamado SIGNAL: una rampa de grafito, un único acento verde lima eléctrico y dos actos, oscuro y claro. El oscuro se diseñó primero y el claro después, deliberadamente y en ese orden. A continuación está el razonamiento detrás de los tokens, las trampas de contraste que encontramos y por qué hay exactamente un color de acento. Todo lo que se cita aquí vive en un solo archivo, src/app/globals.css, de modo que no hay que fiarse de nada sin comprobarlo.
Pensar primero en oscuro es una restricción, no una mano de pintura
La mayoría de los sistemas de diseño se construyen en claro y luego se invierten. Defines una paleta sobre blanco, la publicas y tres sprints después alguien añade un tema oscuro cambiando unas cuantas variables. Se ve bien en el archivo de diseño y se desmorona en producción: paneles de negro puro, texto gris que desaparece, sombras que no hacen nada porque no hay luz que las proyecte.
Construir al revés obliga a tomar las mejores decisiones pronto.
- Te encuentras primero con los modos de fallo del bajo contraste, cuando aún son baratos de corregir.
- La elevación tiene que expresarse con el color de superficie en lugar de con sombra, que de todos modos es un modelo más honesto.
- El acento se elige sobre un campo oscuro, donde un color saturado se comporta de forma muy distinta a como lo hace sobre blanco.
El marco de los actos no es decoración. Un acto tiene su propia iluminación y su propio ambiente, pero es la misma historia con los mismos personajes. Los dos actos comparten estructura e intención. Lo que cambia es la puesta en escena.
Los tokens llevan significado, no valores
El error que mata la gestión de temas es nombrar los tokens por su aspecto. --gray-900. --blue-500. En cuanto lo haces, los dos temas no pueden compartir una sola línea de código de componente, porque el componente ahora conoce un color literal.
Cada token semántico de SIGNAL se nombra por su función. Un componente pide --surface, --muted o --border. Nunca pide un hexadecimal y nunca sabe en qué acto está. El acto decide a qué se resuelven esos roles.
Un componente debe describir lo que necesita, no el aspecto que quiere tener. «Dame el color de la superficie elevada» es un contrato. «Dame #1a1a1a» es un pasivo.
Hay dos capas por debajo, y mantenerlas separadas es lo que hace manejable el sistema. Una rampa en bruto guarda los valores (--color-n-850, --color-lime-500) y nada fuera del archivo de tokens puede referenciarla. Una capa semántica asigna roles a esa rampa, una vez por acto, y es la única capa que los componentes ven.
El negro puro es una trampa
El primer instinto en un tema oscuro es recurrir a #000000. Te cuesta tres cosas distintas.
En los paneles OLED, el negro puro bajo un texto brillante produce una estela visible cuando el ojo se mueve, de modo que los bordes del texto parecen vibrar. No deja adónde ir por debajo: si la base es negra, has gastado la parte baja de tu rango antes de empezar, y cada nivel de elevación tiene que subir. Y aplana la sombra, porque una sombra es solo una región más oscura y nada es más oscuro que el negro.
El lienzo de SIGNAL es #0a0b0d, unos pocos puntos por encima del negro, y la escala de superficies se aparta de él hacia arriba en lugar de por debajo: #15171c es la superficie elevada sobre la que se dibuja una tarjeta, y #1c1f25 es el escalón hundido que se usa dentro de una, para un bloque de código o una cita destacada. La profundidad se lee solo por el color de superficie, lo que deja a la sombra ser un acento y no una muleta.
La otra trampa es el contraste en la dirección equivocada. El mínimo de WCAG de 4,5:1 para el texto del cuerpo es un suelo, no un objetivo, y sobre un fondo muy oscuro el contraste máximo es activamente desagradable. El blanco puro sobre #0a0b0d da 19,69:1 y cansa en una sesión larga, así que el texto del cuerpo de SIGNAL es #e9ecef, que llega a 16,60:1 sobre la misma base y se lleva mejor. Las dos cifras son la relación de contraste estándar entre los dos valores hexadecimales citados en este párrafo. Cualquier comprobador de contraste las reproducirá.
Los tokens, y cómo se invierten
Los componentes referencian variables semánticas. Un acto es un ámbito que redefine a qué se resuelven esas variables. Ningún código de componente cambia entre uno y otro.
/* Acto uno: el oscuro es el valor por defecto, declarado en :root */
:root {
--bg: #0a0b0d; /* unos puntos por encima del negro, nunca #000 */
--surface: #15171c; /* elevada: la tarjeta, el panel, el control */
--surface-2: #1c1f25; /* hundida: el nivel que se usa DENTRO de una superficie elevada */
--text: #e9ecef; /* deliberadamente no es blanco puro */
--muted: #969da8;
--border: #2c313a;
--accent: #c8ff3d; /* lima eléctrico, ajustado para un fondo oscuro */
}
/* Acto dos: el claro se diseña, no se obtiene invirtiendo el acto uno */
[data-act="light"] {
--bg: #f6f7f9;
--surface: #ffffff;
--surface-2: #e9ecef; /* mismo papel hundido, que aquí se consigue yendo a más OSCURO */
--text: #0f1115;
--muted: #565c66;
--border: #d8dce2;
--accent: #97d60f; /* misma familia, más densa: el lima se desvanece sobre blanco */
}En el archivo real, cada uno de esos valores sale de un escalón con nombre de la rampa en lugar de un literal, de modo que el mismo grafito nunca se escribe dos veces. Aquí se muestran los valores resueltos porque son los que realmente hay que razonar.
Hay dos cosas que merece la pena notar. --surface-2 tiene un solo rol en ambos actos, el escalón hundido dentro de una superficie elevada, pero llega a ese rol desde direcciones opuestas: más claro que --surface en oscuro, más oscuro que ella en claro. El rol es estable y la física se invierte por debajo. Y el acento no tiene el mismo valor en los dos. #c8ff3d se ve limpio sobre casi negro y se desvanece sobre blanco, así que el acto claro baja a un #97d60f más denso, con el mismo matiz. Los roles se mantienen constantes, los valores se ajustan por acto.
Un solo acento, y la excepción que confirma la regla
La tentación en cualquier sistema de diseño es publicar un arcoíris: un primario, un secundario, un azul informativo, un degradado para la página de marketing. SIGNAL publica un solo acento y lo trata como un recurso escaso.
- Hace que la jerarquía no tenga ambigüedad. Cuando una sola cosa tiene color, la cosa con color es la cosa. Un segundo acento abre una competición por la atención que el lector tiene que arbitrar.
- Sigue siendo manejable entre actos. Ajustar un matiz sobre dos fondos es un trabajo que se termina. Ajustar cinco es una fuente permanente de incoherencia.
- Deja a los colores semánticos, éxito, advertencia y peligro, espacio para significar algo de verdad, porque no compiten con la decoración por la mirada del lector.
La excepción es la parte instructiva. El anillo de foco es su propio token en lugar de un alias del acento, porque un indicador de foco es un objetivo de contraste no textual: WCAG 2.2 pide 3:1 contra aquello sobre lo que se asienta, y el acento del acto claro alcanza ahí 1,64:1. Por eso el anillo de foco del acto claro usa un verde chartreuse mucho más oscuro, #3f5c00, que supera el requisito con 7,14:1.
Así es en la práctica la disciplina del acento único. No un hexadecimal en todas partes, sino un solo significado, expresado con el valor que supere el listón en el contexto donde cae. Cuando un token tiene que divergir, la razón está escrita junto a él en el archivo, porque una divergencia que nadie documentó es una divergencia que alguien copiará más tarde.
Lo que compra la disciplina
Si aciertas con la capa semántica, los temas dejan de ser una refactorización y pasan a ser un problema de datos. Un tercer acto, una puesta en escena genuina de alto contraste, es un ámbito nuevo con valores nuevos en lugar de una segunda biblioteca de componentes. La contención se lee como seguridad por la misma razón estructural: un sistema que usa el color con moderación parece saber qué importa, porque tuvo que decidirlo.
Ese es todo el beneficio de escribir los tokens como roles. Los componentes nunca fueron la parte que tenía que cambiar.