Sistema de design
O sistema é o artefato. Tudo o mais é construído a partir dele. Um entregável que inventa suas próprias cores e espaçamentos é um item único; um entregável construído sobre tokens pode ser estendido, renomeado e mantido consistente.
Saída
Três arquivos, e a separação é o ponto:
design/tokens.css— os tokens, e nada mais. Propriedades personalizadas CSS simples.design/components.css— os componentes, como classes construídas apenas a partir dos tokens semânticos.design/Design System.html— o espécime. Ele<link>a ambos os arquivos e os documenta. Documentação apenas: não declara valores próprios.
Os dois arquivos CSS são o sistema: são o que outros aplicativos copiam. O espécime permanece no projeto como sua documentação.
Seções no espécime, nesta ordem:
- Intenção — três frases: para quem é, como deve se sentir, o que explicitamente não é.
- Cor — cada amostra com seu nome de token, o valor que resolve e os pares que passam no contraste.
- Tipografia — famílias, pesos, a escala com uma função nomeada por passo e uma amostra de parágrafo no tamanho do corpo.
- Espaço e forma — a escala de espaçamento, raios, sombras, bordas.
- Componentes — cada um renderizado em seus estados reais, junto com a marcação que um aplicativo copia.
- Regras — as cinco ou seis decisões que alguém normalmente erraria.
- Usar em um aplicativo — a versão, os dois arquivos para copiar, a ordem de carregamento, o
<link>da fonte, a classe raiz e o que mudou desde a última versão.
Tokens
Duas camadas. Primitivas mantêm valores brutos; tokens semânticos nomeiam uma função e apontam para uma primitiva. Os componentes referenciam apenas tokens semânticos — é isso que torna uma mudança de marca uma alteração de uma linha.
Escreva isso em design/tokens.css. É o único arquivo no projeto onde um valor bruto pode aparecer. acme representa o prefixo do sistema (veja Compartilhamento):
/* Sistema de design Acme 1.0.0 — tokens. Copie este arquivo como está; altere-o em seu projeto de origem. */
:root {
/* primitivas */
--acme-ink-900: oklch(0.22 0.02 260);
--acme-ink-600: oklch(0.48 0.02 260);
--acme-paper-50: oklch(0.99 0.005 90);
--acme-paper-100: oklch(0.96 0.008 90);
--acme-line-200: oklch(0.90 0.006 90);
--acme-accent-500: oklch(0.55 0.14 35);
--acme-accent-600: oklch(0.45 0.15 35);
--acme-accent-100: oklch(0.94 0.03 35);
/* semânticas */
--acme-text: var(--acme-ink-900);
--acme-text-muted: var(--acme-ink-600);
--acme-bg: var(--acme-paper-50);
--acme-surface: var(--acme-paper-100);
--acme-border: var(--acme-line-200);
--acme-accent: var(--acme-accent-500);
--acme-accent-hover: var(--acme-accent-600);
--acme-accent-wash: var(--acme-accent-100);
/* tipo */
--acme-font-display: "Fraunces", Georgia, serif;
--acme-font-text: "Public Sans", system-ui, sans-serif;
--acme-step--1: 0.875rem;
--acme-step-0: 1rem;
--acme-step-1: 1.25rem;
--acme-step-2: 1.5625rem;
--acme-step-3: 1.953rem;
--acme-step-4: 2.441rem;
--acme-step-5: 3.052rem;
/* espaço, em base de 4px */
--acme-s-1: 4px; --acme-s-2: 8px; --acme-s-3: 12px; --acme-s-4: 16px;
--acme-s-5: 24px; --acme-s-6: 32px; --acme-s-7: 48px; --acme-s-8: 64px; --acme-s-9: 96px;
/* forma e movimento */
--acme-radius-sm: 4px; --acme-radius: 8px; --acme-radius-lg: 16px;
--acme-border-width: 1px; --acme-ring-width: 2px;
--acme-shadow-1: 0 1px 2px oklch(0.2 0.02 260 / 0.06);
--acme-shadow-2: 0 4px 16px oklch(0.2 0.02 260 / 0.08);
--acme-ease: cubic-bezier(0.2, 0.7, 0.3, 1);
--acme-dur: 160ms;
}
Esse bloco é um ponto de partida, não um modelo para copiar cegamente. Altere o matiz do acento, o par tipográfico e a temperatura da tinta para corresponder ao briefing — mas mantenha a estrutura.
Regras para os tokens
- O contraste é medido, não estimado visualmente. Texto do corpo 4,5:1 contra seu fundo real; texto grande e bordas de UI 3:1. Cada página de amostra informa seus pares.
- Oklch, não hex aleatório. Etapas uniformes de luminosidade evitam que paletas fiquem turvas. Mantenha o croma aproximadamente constante em uma família de matizes.
- Nenhum token não utilizado. Se nada referencia um token, delete-o.
- Nenhum valor fora deste arquivo. Um hex, literal
oklch(), tamanho em px ou sombra em qualquer outro lugar é um token que ainda não foi nomeado. - Duas famílias tipográficas. Uma de exibição, uma de texto. As pilhas de fallback devem ser reais, não apenas
sans-serif. - Escala, não improvise. Se um valor não está na escala de espaçamento ou tipo, adicione-o deliberadamente ou use o mais próximo.
- Modo escuro apenas se solicitado — mas mantenha as cores semânticas para que adicioná-lo depois seja uma troca de token, não uma reescrita.
Componentes
Escreva-os em design/components.css, e renderize cada um no espécime em estado padrão, hover, foco, ativo e desabilitado, quando aplicável:
- Botão — primário, secundário, fantasma. Um tamanho geralmente é suficiente.
- Input de texto e select, com rótulo, texto de ajuda e estado de erro.
- Card, com um exemplo de conteúdo real, não "Lorem ipsum".
- Navegação, tanto em desktop quanto em largura estreita.
- Tabela ou linha de lista com cabeçalho.
- Badge e tag.
- Modal ou diálogo.
- Estado vazio.
- Toast ou aviso inline.
Cada componente usa apenas tokens semânticos. Se um componente precisar de um valor que não possui token, isso é uma lacuna no sistema — adicione o token, não codifique o valor diretamente.
/* Sistema de design Acme 1.0.0 — componentes. Precisa de tokens.css da mesma versão, carregado primeiro. */
.acme {
font-family: var(--acme-font-text);
font-size: var(--acme-step-0);
color: var(--acme-text);
background: var(--acme-bg);
}
.acme-button {
font: inherit;
padding: var(--acme-s-2) var(--acme-s-4);
border: var(--acme-border-width) solid transparent;
border-radius: var(--acme-radius);
background: var(--acme-accent);
color: var(--acme-bg);
transition: background var(--acme-dur) var(--acme-ease);
}
.acme-button:hover { background: var(--acme-accent-hover); }
.acme-button:focus-visible { outline: var(--acme-ring-width) solid var(--acme-accent); outline-offset: var(--acme-ring-width); }
.acme-button--ghost { background: transparent; color: var(--acme-accent); border-color: var(--acme-border); }
Compartilhamento
Outros aplicativos usam o sistema copiando tokens.css e components.css. As regras abaixo são o que permite que uma cópia funcione em qualquer lugar.
- Prefixe tudo. Escolha um nome curto para o sistema antes do primeiro token e coloque-o em toda propriedade personalizada e toda classe:
--acme-accent,.acme-button. As cópias então ficam lado a lado com o CSS do próprio aplicativo sem conflitos. O prefixo faz parte da API; nunca o mude. - CSS puro em ambos os arquivos. Sem diretivas Tailwind, sem sintaxe de pré-processador, sem aninhamento, sem exigência de
@layer, sem@importentre eles. Um aplicativo React, uma página estática ou outra ferramenta de design deve obter resultados idênticos de uma cópia direta. - Tudo resolve em tempo de execução. Sem matemática de cores em tempo de compilação, sem saída compilada.
- Nada global em
components.css. Sem resets, sem seletores de elemento, nada emhtmloubody: inserir o arquivo em um aplicativo não deve reestilizá-lo. Os estilos base de texto residem na classe raiz,.acme, que um aplicativo adiciona onde desejar. - Estados vêm da marcação. Estilize
:hover,:focus-visible,:disabled,[aria-invalid="true"],[aria-current]e[aria-expanded]em vez de classes de estado que um aplicativo teria que alternar, para que os componentes funcionem com o HTML de qualquer framework. - Fácil de sobrescrever. Uma classe por regra sempre que possível, sem IDs, sem
!important. - Uma versão para ambos os arquivos. A primeira linha de cada um nomeia o sistema e sua versão; incremente a cada alteração, em ambos. Patch para um valor alterado, minor para um novo token ou componente, major para uma remoção. Renomeie adicionando o novo token e mantendo o antigo como um alias,
--acme-old: var(--acme-new);, até que os aplicativos tenham migrado; remover o alias é o major bump. - Coisas do projeto ficam no espécime. Tags
<link>de fonte, larguras de contêiner e caminhos de ativos não são tokens. O espécime mostra o<link>da fonte para que um aplicativo possa copiá-lo junto com os arquivos.
Se o projeto de destino já tiver um arquivo de token, corresponda à sua nomenclatura antes de inventar a sua própria. Um vocabulário compartilhado vale mais do que um nome melhor.
Página espécime
O espécime é documentação para humanos, não uma galeria de componentes para ser admirada. Mostre o nome do token ao lado da amostra. Mostre cada componente com a marcação que um aplicativo copia. Observe os estados que são difíceis de ver (anéis de foco, desabilitado).
Link para ambos os arquivos, tokens primeiro, e coloque a classe raiz em <body>. Use o próprio tipo e espaçamento do sistema para organizar o espécime — é a primeira prova de que o sistema funciona.
Leia os valores do arquivo em vez de digitá-los, para que a documentação não se desvie:
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="components.css">
…
<body class="acme">
<figure class="swatch" data-token="--acme-accent-500"></figure>
…
<script>
for (const el of document.querySelectorAll('[data-token]')) {
const name = el.dataset.token;
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
el.dataset.value = value; // impresso pelo CSS
el.style.setProperty('--swatch', value); // pintado pelo CSS
}
</script>
A amostra se pinta a partir do token e imprime o valor que o navegador resolveu. Um hex digitado manualmente é uma mentira futura.
Finalizar
Execute design-verify no espécime. Ele lista qualquer literal de cor no espécime ou em components.css; não deve haver nenhum. Verifique se cada rótulo de amostra é legível, cada componente está alinhado à mesma grade de linha de base e nenhum texto de amostra transborda seu contêiner a 1024px de largura.
Quando o sistema for para outro aplicativo, ele copia tokens.css e components.css inalterados, além do <link> da fonte do espécime — não o espécime em si. Exporte uma cópia independente do espécime apenas se ele precisar ser lido em algum lugar sem a pasta.