Tokens semánticos para el cambio de tema
El cambio de tema es un problema de nomenclatura antes de ser un problema de
CSS. Los design tokens de modo oscuro son roles semánticos — background,
surface, text-primary — donde cada rol contiene dos valores, uno por tema,
y los componentes solo hacen referencia al nombre del rol. Cambia el tema y
cada referencia se vuelve a resolver de golpe; ningún componente tiene una
opinión sobre en qué modo está. El riesgo de ingeniería no es el cambio sino
el mapeo: la tabla de rol-a-valor de un tema oscuro no es un espejo de la del
claro, así que hay que derivarla y volver a comprobarla, nunca invertirla.
La configuración por capas que hay debajo cabe en un párrafo:
los tokens primitivos nombran la materia prima
(los pasos de la rampa — brand-600, neutral-100), los tokens semánticos
nombran trabajos de la UI y apuntan a los primitivos, y los componentes
consumen solo semántica. Los argumentos de nomenclatura y la mecánica del
archivo de diseño tienen sus propias guías; este artículo — parte de nuestra
guía de modo oscuro — trata sobre lo que hace la capa
semántica cuando llega un segundo tema.
¿Por qué cada rol necesita dos valores?
Porque el significado de un rol es estable y su valor no lo es. background
significa «el color en reposo de la página» en cualquier tema; el valor que
hace ese trabajo es casi blanco en modo claro y casi negro en oscuro. La capa
semántica es la frontera del tema: todo lo que hay por encima — componentes,
layouts, pantallas — permanece ciego al tema, y todo lo que hay por debajo
cambia por completo.
Esta es también la razón intuitiva por la que el patrón funciona para quienes lo usan. Un diseñador o desarrollador que piensa en roles se pregunta para qué sirve esta superficie, lo que tiene una sola respuesta; pensar en valores pregunta qué gris es este, lo que tiene dos respuestas que hay que mantener sincronizadas a mano. Los nombres que prometen un trabajo en lugar de un valor son los que hacen que el segundo tema sea un cambio de datos y no un segundo diseño.
¿Por qué el mapeo oscuro no es un espejo del claro?
El modelo mental tentador es invertir la escala — si el modo claro usa
neutral-100 para la página, el modo oscuro usa su número opuesto. Derivar el
mapeo y comprobarlo produce algo más interesante. Seis roles de una paleta
sembrada con #2563eb — oklch(0.546 0.215 262.9):
| Rol | Claro | Oscuro (derivado) | Lo que diría un espejo directo |
|---|---|---|---|
background | neutral-100 (L ≈0,94) | neutral-900 (L ≈0,22) | neutral-800 — pero la página se ancla en la parte inferior de la escala, así que la escalera de elevación tiene espacio por encima |
surface (tarjeta) | neutral-50 (L ≈0,97) | neutral-800 (L ≈0,28) | neutral-900 — que hundiría la tarjeta por debajo de la página e invertiría la elevación |
text-primary | neutral-800 (L ≈0,28) | neutral-100 (L ≈0,94) | neutral-100 — se mantiene, aunque los temas ajustados a mano a menudo lo suavizan de más hasta neutral-200, que se lee endeble |
text-secondary | neutral-600 (L ≈0,47) | neutral-300 (L ≈0,78) | neutral-300 — el espejo se mantiene |
border-subtle | neutral-200, más oscuro que la tarjeta | neutral-700, más claro que la tarjeta | los mismos pasos — pero la relación invierte su dirección |
accent | brand-600 (≈#3E65B5) | ≈oklch(0.70 0.14 262.9) de la rampa oscura | brand-300 de la rampa clara — la rampa equivocada por completo |
Vale la pena nombrar tres asimetrías. Primera, background y surface cruzan
sus espejos: las superficies elevadas deben ser más claras que aquello sobre
lo que se apoyan en modo oscuro, así que la tarjeta acaba por encima de la
página en la escala de luminosidad aunque el espejo la habría colocado debajo.
Segunda, el texto: neutral-200 sobre la página oscura todavía supera el suelo
aritmético (≈12:1 frente al requisito de 4,5:1), pero el texto claro sobre
oscuro tiende a renderizarse más endeble de lo que sugieren los números —
APCA, que modela la polaridad, reporta el par más bajo que su gemelo del tema
claro — así que la derivación acaba un paso más clara de lo que la cautela
elegiría. Tercera, el valor oscuro del acento no proviene en absoluto de la
rampa clara:
una paleta oscura derivada recorre su propia curva de luminosidad
con el croma reforzado en aproximadamente un 20 %,
porque los entornos oscuros atenúan la coloridad percibida.
La consecuencia práctica: los dos mapeos deberían producirse a partir de una
sola derivación, no mantenerse como dos listas. La exportación de tokens de
Scale Composer lleva una sección semantic y una semantic-dark generadas a
partir de las mismas semillas — cada rol vuelve a derivarse por tema, los
suelos WCAG vuelven a comprobarse, APCA se reporta junto a ellos, y los roles
onFill vuelven a verificarse, ya que un relleno que llevaba texto blanco en
claro puede querer texto oscuro en oscuro.
Abre la exportación con ambas secciones semánticas en Scale Composer — los mismos nombres de rol en ambos lados, los valores oscuros que visiblemente no son un espejo de los claros.

¿Qué patrones de cambio de tema fallan a escala?
Se repiten tres patrones, y los tres fallan de la misma manera — el mapeo se dispersa en lugar de declararse una sola vez.
Variantes dark: por componente. El prefijo dark: de Tailwind repite el
valor oscuro en cada sitio de uso: bg-white dark:bg-gray-900 en cada tarjeta,
en cada archivo. Para un sitio pequeño esto está bien — todo el mapeo cabe en
una pantalla. Para un sistema es un impuesto de mantenimiento: cambiar lo que
significa «surface» en modo oscuro es un buscar-y-reemplazar por toda la base
de código, y cualquier componente al que le faltó su gemelo dark: sale roto
en uno de los temas.
if (isDark) en el código del componente. Los condicionales de tema
mueven el mapeo hacia la lógica, donde puede ramificarse, desviarse y escapar a
la revisión. Un componente que pregunta qué tema está activo ha asumido una
responsabilidad que la capa de tokens ya lleva.
Dos hojas de estilo. Un dark.css mantenido junto a un light.css empieza
como una copia y se desvía desde la primera edición que toca solo uno de los
archivos. Nada obliga a que ambos archivos respondan a las mismas preguntas.
¿Cómo llevan el cambio las propiedades personalizadas de CSS?
Volviendo a declarar los mismos nombres bajo un ámbito de tema. Los componentes hacen referencia a la propiedad personalizada; el valor de la propiedad depende de qué declaración se aplica en cada momento — la mecánica son las propiedades personalizadas de CSS más la cascada, nada más.
:root {
--color-background: oklch(0.94 0.008 262.9); /* neutral-100 */
--color-surface: oklch(0.97 0.005 262.9); /* neutral-50 */
--color-text-primary: oklch(0.28 0.02 262.9); /* neutral-800 */
--color-accent: oklch(0.546 0.215 262.9); /* núcleo de marca */
}
[data-theme="dark"] {
--color-background: oklch(0.22 0.015 262.9); /* neutral-900 */
--color-surface: oklch(0.28 0.02 262.9); /* neutral-800 */
--color-text-primary: oklch(0.94 0.008 262.9); /* neutral-100 */
--color-accent: oklch(0.70 0.14 262.9); /* rampa oscura */
}
.card {
background: var(--color-surface);
color: var(--color-text-primary);
}
La regla .card es el quid: aparece una vez, no menciona ningún tema y es
correcta en ambos. El bloque oscuro puede igualmente colgar de la media query
prefers-color-scheme, o — el patrón robusto — la query fija el valor por
defecto y el atributo lo sobrescribe; los detalles del cableado tienen
su propio artículo. Sea cual sea el disparador que elijas, la exportación de CSS desde una fuente de tokens
genera ambos bloques a partir de la misma derivación, que es lo que impide que
los dos mapeos se desvíen entre sí.
Observa cómo se resuelve un cambio
Es más fácil confiar en el mapeo después de verlo en movimiento. Cambia la vista previa del tema sobre el conjunto completo de roles — cada rol semántico volviéndose a resolver de golpe, con las asimetrías de la tabla anterior visibles en su sitio: la tarjeta que se mantiene más clara que la página, el acento que salta a un paso más claro de una rampa re-derivada, sin decisiones a nivel de componente en ninguna parte.