Skip to content

Design system

A linguagem de design com que o ranui é feito, e o catálogo completo dos tokens que a expressam: toda propriedade personalizada --ran-* global que a biblioteca declara, com o valor dela nos dois temas. Os componentes leem esses tokens em vez de escrever valores na mão, então sobrescrever um deles reestiliza tudo que o consome.

Três páginas respondem a três perguntas diferentes, e são separadas de propósito:

PáginaResponde
Design system (esta página)O que os tokens são: o vocabulário
Diretrizes de designComo escolher entre eles ao montar uma tela
TematizaçãoComo trocá-los e sobrescrevê-los em tempo de execução

Use quando precisar do nome ou do valor de um token (um papel de cor, um passo de espaço, um tamanho de ícone, um nível de sombra, uma curva de aceleração) ou quiser entender por que as escalas têm o formato que têm.

A linguagem: Geist

Os tokens do ranui se baseiam no Geist, o design system de código aberto da Vercel. Toda escala de cor é uma escada de funções fixas, uma por degrau, não um conjunto de tons para escolher: o degrau 200 não é "um cinza um pouco mais escuro", é "o fundo do hover". Fixada a função de um degrau, escolher a cor de um estado de interação vira uma consulta, não um julgamento.

O ranui adota essa escada como as escalas --ran-* dele, põe tokens semânticos por cima e traz Geist Sans / Geist Mono como tipografias padrão.

Duas camadas

Camada 1: a paleta base. As escalas cruas abaixo. Raramente consumidas direto.

Camada 2: os tokens semânticos. --ran-color-* e companhia, mapeados sobre a camada 1. Consuma esta camada. O modo escuro redefine só a camada 1, então todo token semântico vira pelo var() sem nenhuma sobrescrita escura por componente em lugar algum da biblioteca.

--ran-gray-1000        →  #171717 (claro)  /  #ededed (escuro)   ← camada 1, vira
--ran-color-text       →  var(--ran-gray-1000)                    ← camada 2, acompanha
--ran-btn-color        →  var(--ran-color-text, …)                ← token de componente

Essa corrente é a arquitetura inteira: mude um degrau base e ele se propaga por toda parte; mude um token semântico e muda um papel; mude um token de componente e muda um elemento.

Cor

A escada

Toda escala de matiz vai de 100 a 1000, e cada degrau tem uma função fixa:

DegrauPapelDegrauPapel
100Fundo padrão600Borda ativa
200Fundo do hover700Preenchimento sólido (botão/selo)
300Fundo ativo (pressionado)800Preenchimento sólido (hover)
400Borda padrão900Texto e ícones secundários
500Borda do hover1000Texto e ícones principais

Fundos

TokenClaroEscuroServe para
--ran-background-100 #ffffff #000000Fundo da página
--ran-background-200 #fafafa #000000Zonas discretas da página

Cinza — --ran-gray-100..1000

A escala que está por trás do texto, das bordas e das superfícies.

DegrauClaroEscuro
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

Cinza alfa — --ran-gray-alpha-100..1000

Translúcida, então se sobrepõe a qualquer superfície: a escolha certa para um véu, uma lavagem de hover ou um separador que precisa pousar sobre conteúdo desconhecido.

DegrauClaroEscuro
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 os links e o anel de foco.

DegrauClaroEscuro
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

Vermelho — --ran-red-100..1000

Perigo e erros.

DegrauClaroEscuro
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

Avisos.

DegrauClaroEscuro
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

Sucesso.

DegrauClaroEscuro
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 cor

A camada que os componentes de fato leem. Tudo aqui se resolve pelas escalas de cima, então vira com o tema sozinho.

TokenResolve paraPapel
--ran-color-bg--ran-background-100Fundo da página
--ran-color-bg-subtle--ran-background-200Zonas discretas da página
--ran-color-bg-elevated--ran-background-100 · gray-100 (escuro)Cartões, superfícies
--ran-color-bg-muted--ran-gray-100Preenchimentos recuados ou apagados
--ran-color-bg-hover--ran-gray-200Superfície do hover
--ran-color-bg-active--ran-gray-300Superfície ativa (pressionada)
--ran-color-text--ran-gray-1000Texto principal
--ran-color-text-secondary--ran-gray-900Texto secundário
--ran-color-text-disabled--ran-gray-700Texto desabilitado
--ran-color-border--ran-gray-400Borda padrão
--ran-color-border-secondary--ran-gray-300Borda mais discreta
--ran-color-border-hover--ran-gray-500Borda do hover
--ran-color-border-active--ran-gray-600Borda ativa
--ran-color-primary--ran-gray-1000A ação principal (monocromática)
--ran-color-primary-hover #383838 · #cccccc (escuro)Hover do primário
--ran-color-primary-active #4d4d4d · #b3b3b3 (escuro)Primário pressionado
--ran-color-primary-text--ran-background-100A tinta sobre uma superfície primária
--ran-color-success--ran-green-700Sucesso
--ran-color-warning--ran-amber-700Aviso
--ran-color-danger--ran-red-700Perigo / erro
--ran-color-link--ran-blue-700Links

--ran-color-primary-hover / -active são os dois literais da camada semântica: eles caminham em direção ao fundo da página em vez de ao longo de uma escala, então o modo escuro os redefine direto.

O que cada acento significa

  • O primário é monocromático: preto no branco no claro, branco no preto no escuro (o tom de marca do Geist, <r-button type="primary">). O texto e os ícones em cima usam --ran-color-primary-text, que vira junto. Não há um token de "contraste" separado: o primário é a ação de maior contraste.
  • O azul é reservado para os links (--ran-color-link) e o anel de foco. Não é um primário alternativo.
  • Verde = sucesso · âmbar = aviso · vermelho = perigo. Um significado para cada.

Não existe --ran-color-error; o token é --ran-color-danger. Um var() que nomeia uma propriedade nunca declarada não resolve em nada e a declaração inteira é descartada em silêncio — por isso o nome errado merece ser conferido nesta tabela em vez de adivinhado.

Espaço

Os vãos entre as coisas: padding, margin, gap. Uma unidade base de 4px com nove valores, nem um a mais:

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

O número é o múltiplo de 4px, então a escala pula: não existe --ran-space-5. É esse o ponto: um conjunto limitado é o que produz o ritmo de uma página.

Tamanhos

As dimensões do próprio elemento: tamanhos de ícone, alturas de controle, controles pequenos quadrados ou retangulares.

TokenValorNormalmente
--ran-size-116pxA caixa de um checkbox, um ícone pequeno em linha
--ran-size-218px
--ran-size-320pxÍcone dentro de um controle
--ran-size-424pxBotão de ícone numa barra de ferramentas
--ran-size-528pxAltura de um controle compacto
--ran-size-630px
--ran-size-732pxAltura de controle padrão

Esta é uma escala separada da de espaço de propósito, e misturá-las é um erro conferido por máquina (sizing-scale). As duas têm faixas e progressões diferentes (uma escala de espaço que dobra a partir de 4px produz valores esquisitos para ícones e controles), e quem usa precisa poder reajustar uma sem perturbar a outra: um ícone ficar maior não deveria também alargar todo vão que por acaso divida o mesmo valor em pixels. Quando um degrau coincide numericamente com um de espaço (--ran-size-4 e --ran-space-6 são ambos 24px), é coincidência, não apelido.

Uma dimensão genuinamente única que nenhum outro componente compartilha (o min-width de um menu, digamos) continua sendo um token de componente comum, com o próprio valor reserva literal, em vez de ser forçada num degrau.

Tipografia

TokenValor
--ran-font-familyGeist / Geist Sans, e depois a pilha de interface do sistema
--ran-font-monoGeist Mono, e depois ui-monospace, SF Mono, Menlo, Consolas, …
--ran-font-size14px (o tamanho base)
--ran-line-height1.5715

O texto é organizado por papel, e o papel fixa de uma vez a fonte, o tamanho, o peso e a altura de linha:

PapelUsoToken de pesoTokens de tamanho
headingTítulos--ran-text-heading-weight (600)--ran-text-heading-1..4 (32/24/20/16px)
labelUma linha, para percorrer de relance--ran-text-label-weight (500)--ran-text-label-1..3 (14/13/12px)
copyCorpo de várias linhas--ran-text-copy-weight (400)--ran-text-copy-1..2 (16/14px)
buttonTexto de botão--ran-text-button-weight (500)--ran-text-button-size (14px)
monoCódigo, dados, antetítulos--ran-text-mono-weight-regular (400) / --ran-text-mono-weight-medium (500)toma emprestados os tamanhos de label / copy

Dois tokens existem só para um papel pousar direito:

TokenValorPor quê
--ran-text-heading-tracking-0.03emTítulos precisam de espacejamento mais fechado em tamanhos grandes.
--ran-text-button-line-height1Centralização vertical nítida dentro de um controle de altura fixa.

O Geist limita o peso em 600 (semibold). A ênfase vem do tamanho e do espaço, não de uma tipografia mais pesada. Não existe --ran-text-copy-3: o degrau de 12px é --ran-text-label-3.

Fontes

O ranui hospeda as duas famílias por conta própria (peso variável 100–900, SIL OFL 1.1), então uma única importação as carrega sem depender de CDN:

js
import 'ranui/fonts'; // empacotadores
html
<link rel="stylesheet" href="…/ranui/dist/fonts/fonts.css" />

Sem ela, os tokens recaem nas pilhas de fontes do sistema; tudo continua funcionando, só que sem as famílias do Geist.

Raio

TokenValorServe para
--ran-radius-sm6pxControles: botão, campo, seletor
--ran-radius-md12pxCartões, diálogos
--ran-radius-lg16pxSuperfícies grandes
--ran-radius-full9999pxPílulas, avatares

Elevação

A sombra é um papel, não enfeite. Escolha o nível pelo que o elemento é. O modo escuro substitui os três, porque uma sombra afinada para uma página branca some numa preta.

TokenServe paraClaroEscuro
--ran-shadow-elevatedSuperfícies no fluxo que também têm borda: 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-menuCamadas passageiras sobre o conteúdo: menu suspenso, seletor, 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 bloqueiam: 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)

Camadas sem borda contam só com a sombra para se separar, então os níveis de camada flutuante carregam peso de verdade; uma camada que cai no nível elevado parece plana e pregada à página.

Empilhamento

Camadas flutuantes são portalizadas para o <body>, então precisam de um nível explícito:

TokenPadrãoServe para
--ran-z-modal1000Diálogos que bloqueiam e a máscara deles
--ran-z-dropdown1100Menu suspenso / seletor / popover: acima do modal, para que um select dentro de um diálogo continue visível
--ran-z-message1200Avisos e notificações: sempre por cima

A escada começa em 1000 para passar por cima da moldura comum de uma página (barras de navegação e fundos costumam viver nas dezenas). Sobrescreva um nível no :root, ou por componente (--ran-dropdown-host-z-index, --ran-modal-root-z-index, --ran-message-z-index), nunca com !important.

Movimento

TokenValorUso
--ran-motion-duration-fast0.15sTransições de hover e estado ativo
--ran-motion-duration-base0.2sPopovers, menus
--ran-motion-duration-slow0.35sAparições maiores
Token de aceleraçãoCurvaCaráter
--ran-motion-ease-standardcubic-bezier(0.645,0.045,0.355,1)De entrada e saída, de uso geral
--ran-motion-ease-snappycubic-bezier(0.33,0,0.15,1)Rápida, sem passar do ponto: interruptores
--ran-motion-ease-springcubic-bezier(0.34,1.26,0.5,1)Passa levemente do ponto: botões, cartões
--ran-motion-ease-bouncycubic-bezier(0.34,1.56,0.64,1)Passa do ponto de forma brincalhona: curtir, adicionar ao carrinho
--ran-motion-ease-smoothcubic-bezier(0.4,0,0.2,1)Calma, sem passar do ponto: aparições, layout

A família spring é destilada de molas afinadas do SwiftUI (response/damping reduzidos a uma bézier de uma só ultrapassagem).

Combine-as só com propriedades de movimento: transform, opacity, a geometria da caixa. As propriedades da paleta (background-color, color, border-color, box-shadow, fill, stroke) de propósito não trazem transição padrão, porque o CSS não distingue uma interação de uma troca de tema: qualquer desvanecimento que você acrescente a uma cor também dispara quando o claro vira escuro. Mesmo assim, todo componente expõe um gancho --ran-*-transition caso você queira ligar de volta.

Foco

TokenValorPara
--ran-focus-ring0 0 0 2px var(--ran-background-100), 0 0 0 4px var(--ran-blue-700)O anel padrão, como box-shadow
--ran-focus-ring-inverse-color#fffA cor do anel para uma superfície que é escura nos dois temas

O anel tem duas camadas: uma interna da cor do fundo e uma externa azul, então ele continua visível sobre qualquer superfície, e continua azul em vez de acompanhar o primário, agora monocromático.

--ran-focus-ring-inverse-color de propósito não é redefinido no modo escuro: ele existe para um componente cuja própria superfície é escura fixa, seja qual for o tema da página (a barra de controle do r-player, sobre um vídeo qualquer), e essa superfície não muda quando a página muda.

Primitivas de pele

Os poucos valores estruturais que os componentes compartilham e que não são cor, tamanho nem tipografia. Mantidos no mínimo de propósito: esta camada já foi bem maior e quase tudo dela saiu junto com os pacotes de tema.

TokenValorPara
--ran-skin-border-width1pxA espessura de borda que os componentes desenham
--ran-skin-border-stylesolidO estilo de borda que os componentes desenham
--ran-skin-border-image-width4pxO recuo do border-image-slice, compartilhado por button/checkbox/input/modal/message
--ran-skin-raised-shadowvar(--ran-shadow-elevated)A sombra de superfície elevada, indireta para que uma pele possa mudá-la
--ran-skin-font-familyvar(--ran-font-family)A família que os componentes usam, indireta do mesmo jeito

O que o modo escuro redefine

data-ran-theme="dark" no <html> (ou em qualquer subárvore, veja tematização) redefine a paleta base e nada mais, com três exceções que não conseguem se resolver por uma escala:

  • a camada 1 inteira: cada degrau de cinza, cinza alfa, azul, vermelho, âmbar e verde, e os dois fundos;
  • --ran-color-bg-elevated, que no escuro aponta para --ran-gray-100 para um cartão se levantar de uma página preta em vez de sumir nela;
  • --ran-color-primary-hover / -active, que são literais e não referências a uma escala;
  • os três níveis de sombra, reafinados para um fundo escuro.

Todo o resto (qualquer outro token semântico, cada tamanho, cada duração) é definido uma única vez.

Tokens de componente

Abaixo da camada semântica, todo componente expõe os próprios ganchos, nomeados assim:

--ran-{component}-{element}[-{state}]-{property}

por exemplo --ran-btn-hover-background, --ran-select-search-active-border-width. Por padrão eles recaem nos tokens semânticos: var(--ran-btn-background, var(--ran-color-primary, #171717)), então sobrescrever um token semântico alcança todos eles, e sobrescrever um de componente estreita a mudança para um elemento só.

A lista completa, gerada, é o style-tokens-public.md no repositório; a API por elemento está aqui. Para saber como aplicá-los, veja Tematização.

Usar os tokens no seu próprio 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);
}

Três regras mantêm isso seguro no escuro:

  1. Nada de hexadecimal cru para algo que deva acompanhar o tema.
  2. Um valor reserva precisa nomear um token que vire: var(--ran-color-text, var(--ran-gray-1000)), nunca var(--ran-color-text, #171717).
  3. Um valor reserva precisa nomear um token que exista, ou a declaração é descartada e o elemento fica em silêncio com o que herdou.

Todo token global que a biblioteca declara está listado nesta página, e um teste unitário falha se algum for acrescentado sem ser documentado aqui. Os tokens de escopo de componente são gerados à parte, no style-tokens-public.md.

Publicado sob a licença MIT.