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.

Quatro páginas respondem a quatro perguntas diferentes, e são separadas de propósito:

Página Responde
Design system (esta página) O que os tokens são: o vocabulário
Diretrizes de design Como escolher entre eles ao montar uma tela
Arquitetura da informação Que forma a própria página deve ter
Tematização Como 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:

Degrau Papel Degrau Papel
100 Fundo padrão 600 Borda ativa
200 Fundo do hover 700 Preenchimento sólido (botão/selo)
300 Fundo ativo (pressionado) 800 Preenchimento sólido (hover)
400 Borda padrão 900 Texto e ícones secundários
500 Borda do hover 1000 Texto e ícones principais

Fundos

Token Claro Escuro Serve para
--ran-background-100 #ffffff #000000 Fundo da página
--ran-background-200 #fafafa #000000 Zonas discretas da página

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

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

Degrau Claro Escuro
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.

Degrau Claro Escuro
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.

Degrau Claro Escuro
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.

Degrau Claro Escuro
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.

Degrau Claro Escuro
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.

Degrau Claro Escuro
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.

Token Resolve para Papel
--ran-color-bg --ran-background-100 Fundo da página
--ran-color-bg-subtle --ran-background-200 Zonas discretas da página
--ran-color-bg-elevated --ran-background-100 · gray-100 (escuro) Cartões, superfícies
--ran-color-bg-muted --ran-gray-100 Preenchimentos recuados ou apagados
--ran-color-bg-hover --ran-gray-200 Superfície do hover
--ran-color-bg-active --ran-gray-300 Superfície ativa (pressionada)
--ran-color-text --ran-gray-1000 Texto principal
--ran-color-text-secondary --ran-gray-900 Texto secundário
--ran-color-text-disabled --ran-gray-700 Texto desabilitado
--ran-color-border --ran-gray-400 Borda padrão
--ran-color-border-secondary --ran-gray-300 Borda mais discreta
--ran-color-border-hover --ran-gray-500 Borda do hover
--ran-color-border-active --ran-gray-600 Borda ativa
--ran-color-primary --ran-gray-1000 A 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-100 A tinta sobre uma superfície primária
--ran-color-success --ran-green-700 Sucesso
--ran-color-warning --ran-amber-700 Aviso
--ran-color-danger --ran-red-700 Perigo / erro
--ran-color-link --ran-blue-700 Links

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

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

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.

Token Valor Normalmente
--ran-size-1 16px A caixa de um checkbox, um ícone pequeno em linha
--ran-size-2 18px
--ran-size-3 20px Ícone dentro de um controle
--ran-size-4 24px Botão de ícone numa barra de ferramentas
--ran-size-5 28px Altura de um controle compacto
--ran-size-6 30px
--ran-size-7 32px Altura 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

Token Valor
--ran-font-family Geist / Geist Sans, e depois a pilha de interface do sistema
--ran-font-mono Geist Mono, e depois ui-monospace, SF Mono, Menlo, Consolas, …
--ran-font-size 14px (o tamanho base)
--ran-line-height 1.5715

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

Papel Uso Token de peso Tokens de tamanho
heading Títulos --ran-text-heading-weight (600) --ran-text-heading-1..4 (32/24/20/16px)
label Uma linha, para percorrer de relance --ran-text-label-weight (500) --ran-text-label-1..3 (14/13/12px)
copy Corpo de várias linhas --ran-text-copy-weight (400) --ran-text-copy-1..2 (16/14px)
button Texto de botão --ran-text-button-weight (500) --ran-text-button-size (14px)
mono Có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:

Token Valor Por quê
--ran-text-heading-tracking -0.03em Títulos precisam de espacejamento mais fechado em tamanhos grandes.
--ran-text-button-line-height 1 Centralizaçã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:

import 'ranui/fonts'; // empacotadores
<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

Token Valor Serve para
--ran-radius-sm 6px Controles: botão, campo, seletor
--ran-radius-md 12px Cartões, diálogos
--ran-radius-lg 16px Superfícies grandes
--ran-radius-full 9999px Pí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.

Token Serve para Claro Escuro
--ran-shadow-elevated Superfícies no fluxo que também têm borda: 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 Camadas passageiras sobre o conteúdo: menu suspenso, seletor, 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 bloqueiam: 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)

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:

Token Padrão Serve para
--ran-z-modal 1000 Diálogos que bloqueiam e a máscara deles
--ran-z-dropdown 1100 Menu suspenso / seletor / popover: acima do modal, para que um select dentro de um diálogo continue visível
--ran-z-message 1200 Avisos 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

Token Valor Uso
--ran-motion-duration-fast 0.15s Transições de hover e estado ativo
--ran-motion-duration-base 0.2s Popovers, menus
--ran-motion-duration-slow 0.35s Aparições maiores
Token de aceleração Curva Caráter
--ran-motion-ease-standard cubic-bezier(0.645,0.045,0.355,1) De entrada e saída, de uso geral
--ran-motion-ease-snappy cubic-bezier(0.33,0,0.15,1) Rápida, sem passar do ponto: interruptores
--ran-motion-ease-spring cubic-bezier(0.34,1.26,0.5,1) Passa levemente do ponto: botões, cartões
--ran-motion-ease-bouncy cubic-bezier(0.34,1.56,0.64,1) Passa do ponto de forma brincalhona: curtir, adicionar ao carrinho
--ran-motion-ease-smooth cubic-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

Token Valor Para
--ran-focus-ring 0 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 #fff A 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.

Token Valor Para
--ran-skin-border-width 1px A espessura de borda que os componentes desenham
--ran-skin-border-style solid O estilo de borda que os componentes desenham
--ran-skin-border-image-width 4px O recuo do border-image-slice, compartilhado por button/checkbox/input/modal/message
--ran-skin-raised-shadow var(--ran-shadow-elevated) A sombra de superfície elevada, indireta para que uma pele possa mudá-la
--ran-skin-font-family var(--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

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