Skip to content

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áginaResponde
Sistema de diseño (esta página)Qué son los tokens: el vocabulario
Pautas de diseñoCómo elegir entre ellos al construir una pantalla
TematizaciónCó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 componente

Esa 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ñoRolPeldañoRol
100Fondo por defecto600Borde activo
200Fondo del hover700Relleno sólido (botón/insignia)
300Fondo activo (pulsado)800Relleno sólido (hover)
400Borde por defecto900Texto e iconos secundarios
500Borde del hover1000Texto e iconos principales

Fondos

TokenClaroOscuroSirve para
--ran-background-100 #ffffff #000000Fondo de la página
--ran-background-200 #fafafa #000000Zonas sutiles de la página

Gris — --ran-gray-100..1000

La escala que hay detrás del texto, los bordes y las superficies.

PeldañoClaroOscuro
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ñoClaroOscuro
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ñoClaroOscuro
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ñoClaroOscuro
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ñoClaroOscuro
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ñoClaroOscuro
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.

TokenSe resuelve enRol
--ran-color-bg--ran-background-100Fondo de la página
--ran-color-bg-subtle--ran-background-200Zonas sutiles de la página
--ran-color-bg-elevated--ran-background-100 · gray-100 (oscuro)Tarjetas, superficies
--ran-color-bg-muted--ran-gray-100Rellenos hundidos o apagados
--ran-color-bg-hover--ran-gray-200Superficie del hover
--ran-color-bg-active--ran-gray-300Superficie activa (pulsada)
--ran-color-text--ran-gray-1000Texto principal
--ran-color-text-secondary--ran-gray-900Texto secundario
--ran-color-text-disabled--ran-gray-700Texto deshabilitado
--ran-color-border--ran-gray-400Borde por defecto
--ran-color-border-secondary--ran-gray-300Borde más sutil
--ran-color-border-hover--ran-gray-500Borde del hover
--ran-color-border-active--ran-gray-600Borde activo
--ran-color-primary--ran-gray-1000La 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-100La tinta sobre una superficie primaria
--ran-color-success--ran-green-700Éxito
--ran-color-warning--ran-amber-700Advertencia
--ran-color-danger--ran-red-700Peligro / error
--ran-color-link--ran-blue-700Enlaces

--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:

TokenValorTokenValor
--ran-space-14px--ran-space-832px
--ran-space-28px--ran-space-1040px
--ran-space-312px--ran-space-1664px
--ran-space-416px--ran-space-2496px
--ran-space-624px

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.

TokenValorNormalmente
--ran-size-116pxLa caja de un checkbox, un icono pequeño en línea
--ran-size-218px
--ran-size-320pxIcono dentro de un control
--ran-size-424pxBotón de icono en una barra de herramientas
--ran-size-528pxAltura de un control compacto
--ran-size-630px
--ran-size-732pxAltura 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

TokenValor
--ran-font-familyGeist / Geist Sans, y luego la pila de interfaz del sistema
--ran-font-monoGeist Mono, y luego ui-monospace, SF Mono, Menlo, Consolas, …
--ran-font-size14px (el tamaño base)
--ran-line-height1.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:

RolUsoToken de grosorTokens de tamaño
headingTítulos--ran-text-heading-weight (600)--ran-text-heading-1..4 (32/24/20/16px)
labelUna línea, para recorrer con la vista--ran-text-label-weight (500)--ran-text-label-1..3 (14/13/12px)
copyCuerpo de varias líneas--ran-text-copy-weight (400)--ran-text-copy-1..2 (16/14px)
buttonTexto de botón--ran-text-button-weight (500)--ran-text-button-size (14px)
monoCó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:

TokenValorPor qué
--ran-text-heading-tracking-0.03emLos títulos necesitan un interletraje más apretado a tamaños grandes.
--ran-text-button-line-height1Centrado 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:

js
import 'ranui/fonts'; // empaquetadores
html
<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

TokenValorSirve para
--ran-radius-sm6pxControles: botón, campo, desplegable
--ran-radius-md12pxTarjetas, diálogos
--ran-radius-lg16pxSuperficies grandes
--ran-radius-full9999pxPí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.

TokenSirve paraClaroOscuro
--ran-shadow-elevatedSuperficies en el flujo que además tienen borde: r-card, r-section0 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-menuCapas pasajeras sobre el contenido: desplegable, menú de selección, popover, aviso0 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-modalDiálogos que bloquean: r-modal0 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:

TokenPor defectoSirve para
--ran-z-modal1000Diálogos que bloquean y su máscara
--ran-z-dropdown1100Desplegable / menú de selección / popover: por encima del modal, para que un select dentro de un diálogo siga viéndose
--ran-z-message1200Avisos 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

TokenValorUso
--ran-motion-duration-fast0.15sTransiciones de hover y estado activo
--ran-motion-duration-base0.2sPopovers, menús
--ran-motion-duration-slow0.35sApariciones mayores
Token de aceleraciónCurvaCarácter
--ran-motion-ease-standardcubic-bezier(0.645,0.045,0.355,1)De entrada y salida, de uso general
--ran-motion-ease-snappycubic-bezier(0.33,0,0.15,1)Rápida, sin rebote: interruptores
--ran-motion-ease-springcubic-bezier(0.34,1.26,0.5,1)Rebote leve: botones, tarjetas
--ran-motion-ease-bouncycubic-bezier(0.34,1.56,0.64,1)Rebote juguetón: me gusta, añadir al carrito
--ran-motion-ease-smoothcubic-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

TokenValorPara
--ran-focus-ring0 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#fffEl 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.

TokenValorPara
--ran-skin-border-width1pxEl grosor de borde que dibujan los componentes
--ran-skin-border-stylesolidEl estilo de borde que dibujan los componentes
--ran-skin-border-image-width4pxEl margen interior de border-image-slice, compartido por button/checkbox/input/modal/message
--ran-skin-raised-shadowvar(--ran-shadow-elevated)La sombra de superficie elevada, indirecta para que una piel pueda cambiarla
--ran-skin-font-familyvar(--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-100 para 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

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:

  1. Nada de hex crudo para algo que deba seguir al tema.
  2. Un respaldo debe nombrar un token que cambie: var(--ran-color-text, var(--ran-gray-1000)), nunca var(--ran-color-text, #171717).
  3. 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.

Publicado bajo la licencia MIT.