Sistema de diseño
El lenguaje de diseño con el que está hecho ranui, y el catálogo completo de los tokens que lo expresan: todas las propiedades personalizadas --ran-* globales que declara la biblioteca, con su valor en ambos temas. Los componentes leen estos tokens en vez de escribir valores a mano, así que sobrescribir uno reestiliza todo lo que lo consume.
Tres páginas responden a tres preguntas distintas, y están separadas a propósito:
| Página | Responde |
|---|---|
| Sistema de diseño (esta página) | Qué son los tokens: el vocabulario |
| Pautas de diseño | Cómo elegir entre ellos al construir una pantalla |
| Tematización | Cómo cambiarlos y sobrescribirlos en tiempo de ejecución |
Úsala cuando necesites el nombre o el valor de un token (un rol de color, un paso de espacio, un tamaño de icono, un nivel de sombra, una curva de aceleración) o quieras entender por qué las escalas tienen la forma que tienen.
El lenguaje: Geist
Los tokens de ranui se basan en Geist, el sistema de diseño de código abierto de Vercel. Cada escala de color es una escalera de trabajos fijos, uno por peldaño, no un conjunto de tonos entre los que elegir: el peldaño 200 no es «un gris un poco más oscuro», es «el fondo del hover». Una vez fijado el trabajo de un peldaño, elegir un color para un estado de interacción es una consulta, no un juicio.
ranui adopta esa escalera como sus escalas --ran-*, superpone tokens semánticos encima y trae Geist Sans / Geist Mono como tipografías por defecto.
Dos capas
Capa 1: la paleta base. Las escalas crudas de abajo. Rara vez se consumen directamente.
Capa 2: los tokens semánticos. --ran-color-* y compañía, mapeados sobre la capa 1. Consume esta capa. El modo oscuro redefine solo la capa 1, así que cada token semántico cambia a través de var() sin una sola sobreescritura oscura por componente en toda la biblioteca.
--ran-gray-1000 → #171717 (claro) / #ededed (oscuro) ← capa 1, cambia
--ran-color-text → var(--ran-gray-1000) ← capa 2, sigue
--ran-btn-color → var(--ran-color-text, …) ← token de componenteEsa cadena es toda la arquitectura: cambia un peldaño base y se propaga a todas partes; cambia un token semántico y cambia un rol; cambia un token de componente y cambia un elemento.
Color
La escalera
Cada escala de tono va de 100 a 1000, y cada peldaño tiene un trabajo fijo:
| Peldaño | Rol | Peldaño | Rol |
|---|---|---|---|
| 100 | Fondo por defecto | 600 | Borde activo |
| 200 | Fondo del hover | 700 | Relleno sólido (botón/insignia) |
| 300 | Fondo activo (pulsado) | 800 | Relleno sólido (hover) |
| 400 | Borde por defecto | 900 | Texto e iconos secundarios |
| 500 | Borde del hover | 1000 | Texto e iconos principales |
Fondos
| Token | Claro | Oscuro | Sirve para |
|---|---|---|---|
--ran-background-100 | #ffffff | #000000 | Fondo de la página |
--ran-background-200 | #fafafa | #000000 | Zonas sutiles de la página |
Gris — --ran-gray-100..1000
La escala que hay detrás del texto, los bordes y las superficies.
| Peldaño | Claro | Oscuro |
|---|---|---|
| 100 | #f2f2f2 | #1a1a1a |
| 200 | #ebebeb | #1f1f1f |
| 300 | #e6e6e6 | #292929 |
| 400 | #eaeaea | #2e2e2e |
| 500 | #c9c9c9 | #454545 |
| 600 | #a8a8a8 | #878787 |
| 700 | #8f8f8f | #8f8f8f |
| 800 | #7d7d7d | #7d7d7d |
| 900 | #4d4d4d | #a0a0a0 |
| 1000 | #171717 | #ededed |
Gris alfa — --ran-gray-alpha-100..1000
Translúcida, así que se superpone a cualquier superficie: la elección correcta para un velo, un lavado de hover o un separador que debe posarse sobre contenido desconocido.
| Peldaño | Claro | Oscuro |
|---|---|---|
| 100 | #0000000d | #ffffff12 |
| 200 | #00000015 | #ffffff17 |
| 300 | #0000001a | #ffffff21 |
| 400 | #00000014 | #ffffff24 |
| 500 | #00000036 | #ffffff3d |
| 600 | #0000003d | #ffffff82 |
| 700 | #00000070 | #ffffff8a |
| 800 | #00000082 | #ffffff78 |
| 900 | #000000b3 | #ffffff9c |
| 1000 | #000000e8 | #ffffffeb |
Azul — --ran-blue-100..1000
Reservado para los enlaces y el anillo de foco.
| Peldaño | Claro | Oscuro |
|---|---|---|
| 100 | #f0f7ff | #06193a |
| 200 | #e9f4ff | #022248 |
| 300 | #dfefff | #002f62 |
| 400 | #cae7ff | #003674 |
| 500 | #94ccff | #00418b |
| 600 | #48aeff | #0090ff |
| 700 | #006bff | #006efe |
| 800 | #0059ec | #005be7 |
| 900 | #005ff2 | #47a8ff |
| 1000 | #002359 | #eaf6ff |
Rojo — --ran-red-100..1000
Peligro y errores.
| Peldaño | Claro | Oscuro |
|---|---|---|
| 100 | #ffeeef | #330a11 |
| 200 | #ffe8ea | #440d13 |
| 300 | #ffe3e4 | #5d0e17 |
| 400 | #ffd7d6 | #6f101b |
| 500 | #ffb1b3 | #88151f |
| 600 | #ff676d | #f32e40 |
| 700 | #fc0035 | #f13242 |
| 800 | #ea001d | #e2162a |
| 900 | #d8001b | #ff565f |
| 1000 | #47000c | #ffe9ed |
Ámbar — --ran-amber-100..1000
Advertencias.
| Peldaño | Claro | Oscuro |
|---|---|---|
| 100 | #fff6de | #2a1700 |
| 200 | #fff4cf | #361900 |
| 300 | #fff1c1 | #502800 |
| 400 | #ffdc73 | #5b3000 |
| 500 | #ffc543 | #703e00 |
| 600 | #ffa600 | #ed9a00 |
| 700 | #ffae00 | #ffae00 |
| 800 | #ff9300 | #ff9300 |
| 900 | #aa4d00 | #ff9300 |
| 1000 | #561900 | #fff3d5 |
Verde — --ran-green-100..1000
Éxito.
| Peldaño | Claro | Oscuro |
|---|---|---|
| 100 | #ecfdec | #002608 |
| 200 | #e5fce7 | #00320b |
| 300 | #d3fad1 | #003a0e |
| 400 | #b9f5bc | #004615 |
| 500 | #82eb8d | #006717 |
| 600 | #4ce15e | #00952d |
| 700 | #28a948 | #00ac3a |
| 800 | #279141 | #009432 |
| 900 | #107d32 | #00ca50 |
| 1000 | #003a00 | #d8ffe4 |
Tokens semánticos de color
La capa que los componentes leen de verdad. Todo lo de aquí se resuelve a través de las escalas de arriba, así que cambia con el tema por sí solo.
| Token | Se resuelve en | Rol |
|---|---|---|
--ran-color-bg | --ran-background-100 | Fondo de la página |
--ran-color-bg-subtle | --ran-background-200 | Zonas sutiles de la página |
--ran-color-bg-elevated | --ran-background-100 · gray-100 (oscuro) | Tarjetas, superficies |
--ran-color-bg-muted | --ran-gray-100 | Rellenos hundidos o apagados |
--ran-color-bg-hover | --ran-gray-200 | Superficie del hover |
--ran-color-bg-active | --ran-gray-300 | Superficie activa (pulsada) |
--ran-color-text | --ran-gray-1000 | Texto principal |
--ran-color-text-secondary | --ran-gray-900 | Texto secundario |
--ran-color-text-disabled | --ran-gray-700 | Texto deshabilitado |
--ran-color-border | --ran-gray-400 | Borde por defecto |
--ran-color-border-secondary | --ran-gray-300 | Borde más sutil |
--ran-color-border-hover | --ran-gray-500 | Borde del hover |
--ran-color-border-active | --ran-gray-600 | Borde activo |
--ran-color-primary | --ran-gray-1000 | La acción principal (monocroma) |
--ran-color-primary-hover | #383838 · #cccccc (oscuro) | Hover del primario |
--ran-color-primary-active | #4d4d4d · #b3b3b3 (oscuro) | Primario pulsado |
--ran-color-primary-text | --ran-background-100 | La tinta sobre una superficie primaria |
--ran-color-success | --ran-green-700 | Éxito |
--ran-color-warning | --ran-amber-700 | Advertencia |
--ran-color-danger | --ran-red-700 | Peligro / error |
--ran-color-link | --ran-blue-700 | Enlaces |
--ran-color-primary-hover / -active son los dos literales de la capa semántica: avanzan hacia el fondo de la página en lugar de a lo largo de una escala, así que el modo oscuro los redefine directamente.
Qué significa cada acento
- El primario es monocromo: negro sobre blanco en claro, blanco sobre negro en oscuro (el tono de marca de Geist,
<r-button type="primary">). El texto y los iconos encima usan--ran-color-primary-text, que cambia con él. No hay un token de «contraste» aparte: el primario es la acción de mayor contraste. - El azul está reservado para los enlaces (
--ran-color-link) y el anillo de foco. No es un primario alternativo. - Verde = éxito · ámbar = advertencia · rojo = peligro. Un significado cada uno.
No existe --ran-color-error; el token es --ran-color-danger. Un var() que nombra una propiedad nunca declarada se resuelve en nada y la declaración entera se descarta en silencio, y por eso el nombre equivocado merece comprobarse contra esta tabla en vez de adivinarse.
Espacio
Los huecos entre las cosas: padding, margin, gap. Una unidad base de 4px con nueve valores, ni uno más:
| Token | Valor | Token | Valor |
|---|---|---|---|
--ran-space-1 | 4px | --ran-space-8 | 32px |
--ran-space-2 | 8px | --ran-space-10 | 40px |
--ran-space-3 | 12px | --ran-space-16 | 64px |
--ran-space-4 | 16px | --ran-space-24 | 96px |
--ran-space-6 | 24px |
El número es el múltiplo de 4px, así que la escala salta: no existe --ran-space-5. Ese es el punto: un conjunto limitado es lo que produce el ritmo de una página.
Tamaños
Las dimensiones propias de un elemento: tamaños de icono, alturas de control, controles pequeños cuadrados o rectangulares.
| Token | Valor | Normalmente |
|---|---|---|
--ran-size-1 | 16px | La caja de un checkbox, un icono pequeño en línea |
--ran-size-2 | 18px | — |
--ran-size-3 | 20px | Icono dentro de un control |
--ran-size-4 | 24px | Botón de icono en una barra de herramientas |
--ran-size-5 | 28px | Altura de un control compacto |
--ran-size-6 | 30px | — |
--ran-size-7 | 32px | Altura de control por defecto |
Es una escala aparte de la de espacio a propósito, y mezclarlas es un error que comprueba una máquina (sizing-scale). Las dos tienen rangos y progresiones distintos (una escala de espacio que dobla desde 4px produce valores incómodos para iconos y controles), y quien la use debe poder reajustar una sin perturbar la otra: que un icono crezca no debería ensanchar además cada hueco que casualmente comparta su valor en píxeles. Cuando un peldaño coincide numéricamente con uno de espacio (--ran-size-4 y --ran-space-6 son ambos 24px) es coincidencia, no un alias.
Una dimensión genuinamente irrepetible que ningún otro componente comparte (el min-width de un menú, por ejemplo) se queda como token de componente con su propio valor literal de respaldo, en vez de forzarse a un peldaño.
Tipografía
| Token | Valor |
|---|---|
--ran-font-family | Geist / Geist Sans, y luego la pila de interfaz del sistema |
--ran-font-mono | Geist Mono, y luego ui-monospace, SF Mono, Menlo, Consolas, … |
--ran-font-size | 14px (el tamaño base) |
--ran-line-height | 1.5715 |
El texto se organiza por rol, y el rol fija a la vez la fuente, el tamaño, el grosor y la altura de línea:
| Rol | Uso | Token de grosor | Tokens de tamaño |
|---|---|---|---|
| heading | Títulos | --ran-text-heading-weight (600) | --ran-text-heading-1..4 (32/24/20/16px) |
| label | Una línea, para recorrer con la vista | --ran-text-label-weight (500) | --ran-text-label-1..3 (14/13/12px) |
| copy | Cuerpo de varias líneas | --ran-text-copy-weight (400) | --ran-text-copy-1..2 (16/14px) |
| button | Texto de botón | --ran-text-button-weight (500) | --ran-text-button-size (14px) |
| mono | Código, datos, antetítulos | --ran-text-mono-weight-regular (400) / --ran-text-mono-weight-medium (500) | toma prestados los tamaños de label / copy |
Dos tokens existen solo para que un rol aterrice bien:
| Token | Valor | Por qué |
|---|---|---|
--ran-text-heading-tracking | -0.03em | Los títulos necesitan un interletraje más apretado a tamaños grandes. |
--ran-text-button-line-height | 1 | Centrado vertical nítido dentro de un control de altura fija. |
Geist limita el grosor a 600 (semibold). El énfasis viene del tamaño y del espacio, no de una tipografía más gruesa. No existe --ran-text-copy-3: el peldaño de 12px es --ran-text-label-3.
Tipografías
ranui aloja por su cuenta ambas familias (peso variable 100–900, SIL OFL 1.1), así que una sola importación las carga sin depender de ningún CDN:
import 'ranui/fonts'; // empaquetadores<link rel="stylesheet" href="…/ranui/dist/fonts/fonts.css" />Sin ella, los tokens recurren a las pilas de fuentes del sistema; todo sigue funcionando, solo que sin las familias de Geist.
Radio
| Token | Valor | Sirve para |
|---|---|---|
--ran-radius-sm | 6px | Controles: botón, campo, desplegable |
--ran-radius-md | 12px | Tarjetas, diálogos |
--ran-radius-lg | 16px | Superficies grandes |
--ran-radius-full | 9999px | Píldoras, avatares |
Elevación
La sombra es un rol, no un adorno. Elige el nivel por lo que es el elemento. El modo oscuro reemplaza los tres, porque una sombra afinada para una página blanca desaparece sobre una negra.
| Token | Sirve para | Claro | Oscuro |
|---|---|---|---|
--ran-shadow-elevated | Superficies en el flujo que además tienen borde: r-card, r-section | 0 1px 2px rgba(0,0,0,.04), 0 2px 4px -2px rgba(0,0,0,.05) | 0 1px 2px rgba(0,0,0,.16) |
--ran-shadow-menu | Capas pasajeras sobre el contenido: desplegable, menú de selección, popover, aviso | 0 2px 4px rgba(0,0,0,.05), 0 8px 24px -6px rgba(0,0,0,.14) | 0 1px 1px rgba(0,0,0,.2), 0 4px 8px -4px rgba(0,0,0,.4), 0 16px 24px -8px rgba(0,0,0,.5) |
--ran-shadow-modal | Diálogos que bloquean: r-modal | 0 4px 12px rgba(0,0,0,.08), 0 20px 48px -12px rgba(0,0,0,.22) | 0 1px 1px rgba(0,0,0,.2), 0 8px 16px -4px rgba(0,0,0,.4), 0 24px 32px -8px rgba(0,0,0,.5) |
Las capas sin borde confían solo en la sombra para separarse, así que los niveles de capa flotante llevan peso de verdad; una capa que cae al nivel elevado se ve plana y clavada a la página.
Apilamiento
Las capas flotantes se portalizan a <body>, así que necesitan un nivel explícito:
| Token | Por defecto | Sirve para |
|---|---|---|
--ran-z-modal | 1000 | Diálogos que bloquean y su máscara |
--ran-z-dropdown | 1100 | Desplegable / menú de selección / popover: por encima del modal, para que un select dentro de un diálogo siga viéndose |
--ran-z-message | 1200 | Avisos y notificaciones: siempre encima |
La escalera empieza en 1000 para superar el marco habitual de una página (las barras de navegación y los fondos suelen vivir en las decenas). Sobrescribe un nivel en :root, o por componente (--ran-dropdown-host-z-index, --ran-modal-root-z-index, --ran-message-z-index), nunca con !important.
Movimiento
| Token | Valor | Uso |
|---|---|---|
--ran-motion-duration-fast | 0.15s | Transiciones de hover y estado activo |
--ran-motion-duration-base | 0.2s | Popovers, menús |
--ran-motion-duration-slow | 0.35s | Apariciones mayores |
| Token de aceleración | Curva | Carácter |
|---|---|---|
--ran-motion-ease-standard | cubic-bezier(0.645,0.045,0.355,1) | De entrada y salida, de uso general |
--ran-motion-ease-snappy | cubic-bezier(0.33,0,0.15,1) | Rápida, sin rebote: interruptores |
--ran-motion-ease-spring | cubic-bezier(0.34,1.26,0.5,1) | Rebote leve: botones, tarjetas |
--ran-motion-ease-bouncy | cubic-bezier(0.34,1.56,0.64,1) | Rebote juguetón: me gusta, añadir al carrito |
--ran-motion-ease-smooth | cubic-bezier(0.4,0,0.2,1) | Serena, sin rebote: apariciones, maquetación |
La familia spring está destilada de muelles afinados de SwiftUI (response/damping reducidos a una bézier de un solo rebote).
Combínalas solo con propiedades de movimiento: transform, opacity, la geometría de la caja. Las propiedades de la paleta (background-color, color, border-color, box-shadow, fill, stroke) no llevan transición por defecto a propósito, porque el CSS no puede distinguir una interacción de un cambio de tema: cualquier fundido que añadas a un color se dispara también al pasar de claro a oscuro. Aun así, cada componente expone un gancho --ran-*-transition por si quieres volver a activarlo.
Foco
| Token | Valor | Para |
|---|---|---|
--ran-focus-ring | 0 0 0 2px var(--ran-background-100), 0 0 0 4px var(--ran-blue-700) | El anillo estándar, como box-shadow |
--ran-focus-ring-inverse-color | #fff | El color del anillo para una superficie oscura en ambos temas |
El anillo tiene dos capas: una interior del color del fondo y otra exterior azul, así que sigue viéndose sobre cualquier superficie, y se queda azul en vez de seguir al primario, ahora monocromo.
--ran-focus-ring-inverse-color no se redefine en modo oscuro a propósito: existe para un componente cuya propia superficie es oscura fija, sea cual sea el tema de la página (la barra de control de r-player, sobre un vídeo cualquiera), y esa superficie no cambia cuando cambia la página.
Primitivas de piel
Los pocos valores estructurales que comparten los componentes y que no son color, tamaño ni tipografía. Se mantienen al mínimo a propósito: esta capa era mucho mayor y casi toda se retiró junto con los paquetes de tema.
| Token | Valor | Para |
|---|---|---|
--ran-skin-border-width | 1px | El grosor de borde que dibujan los componentes |
--ran-skin-border-style | solid | El estilo de borde que dibujan los componentes |
--ran-skin-border-image-width | 4px | El margen interior de border-image-slice, compartido por button/checkbox/input/modal/message |
--ran-skin-raised-shadow | var(--ran-shadow-elevated) | La sombra de superficie elevada, indirecta para que una piel pueda cambiarla |
--ran-skin-font-family | var(--ran-font-family) | La familia que usan los componentes, indirecta del mismo modo |
Qué redefine el modo oscuro
data-ran-theme="dark" en <html> (o en cualquier subárbol, véase tematización) redefine la paleta base y nada más, con tres excepciones que no pueden resolverse a través de una escala:
- toda la capa 1: cada peldaño de gris, gris alfa, azul, rojo, ámbar y verde, y los dos fondos;
--ran-color-bg-elevated, que en oscuro apunta a--ran-gray-100para que una tarjeta se levante de una página negra en vez de desaparecer en ella;--ran-color-primary-hover/-active, que son literales y no referencias a una escala;- los tres niveles de sombra, reafinados para un fondo oscuro.
Todo lo demás (cualquier otro token semántico, cada tamaño, cada duración) se define una sola vez.
Tokens de componente
Por debajo de la capa semántica, cada componente expone sus propios ganchos, con este nombre:
--ran-{component}-{element}[-{state}]-{property}por ejemplo --ran-btn-hover-background, --ran-select-search-active-border-width. Por defecto recurren a los tokens semánticos: var(--ran-btn-background, var(--ran-color-primary, #171717)), así que sobrescribir un token semántico llega a todos ellos, y sobrescribir uno de componente reduce el cambio a un solo elemento.
La lista completa, generada, es style-tokens-public.md en el repositorio; la API por elemento está aquí. Para saber cómo aplicarlos, véase Tematización.
Usar los tokens en tu propio CSS
.panel {
background: var(--ran-color-bg-elevated);
color: var(--ran-color-text);
border: var(--ran-skin-border-width) var(--ran-skin-border-style) var(--ran-color-border);
border-radius: var(--ran-radius-md);
padding: var(--ran-space-4);
box-shadow: var(--ran-shadow-elevated);
}Tres reglas mantienen eso a salvo en oscuro:
- Nada de hex crudo para algo que deba seguir al tema.
- Un respaldo debe nombrar un token que cambie:
var(--ran-color-text, var(--ran-gray-1000)), nuncavar(--ran-color-text, #171717). - Un respaldo debe nombrar un token que exista, o la declaración se descarta y el elemento se queda en silencio con lo que heredó.
Todos los tokens globales que declara la biblioteca están listados en esta página, y una prueba unitaria falla si se añade uno sin documentarlo aquí. Los tokens de ámbito de componente se generan aparte, en style-tokens-public.md.