Design system para e-commerce: tokens, OKLCH e o CSS moderno de 2026
Como dar coerência visual a qualquer stack com tokens nomeados, Tailwind v4 e os recursos nativos de CSS que substituem bibliotecas.
Alexandre Caramaschi
CEO da Brasil GEO, ex-CMO da Semantix (Nasdaq), cofundador da AI Brasil
Depois de escolher a stack de cada rota, vem a pergunta que decide se a loja parece uma marca ou uma colcha de retalhos: o que mantém a aparência coerente quando o conteúdo é Astro, o storefront é Svelte e o painel é React? A resposta não está num framework, e sim num design system feito de tokens e do CSS moderno que, em 2026, resolve no próprio navegador o que antes exigia pilhas de JavaScript.
Este guia mostra como montar essa camada: tokens nomeados em OKLCH com contraste garantido, Tailwind v4 no modelo CSS-first, e os recursos nativos de CSS que tornam componentes reutilizáveis sem reescrita. Ele fecha a trilha iniciada em como escolher a stack de frontend.
Por que cor em frontend é sempre um token, nunca um hex solto?
Resposta direta: porque token é um nome para uma decisão, e nome você muda em um lugar só. Em vez de espalhar #6366f1 por trinta arquivos, você define --color-primary e referencia var(--color-primary). Quando a marca troca de tom, edita-se a definição, não trinta ocorrências. E quando você pede a um agente “use o token primário no botão”, ele compõe com var(--color-primary) em vez de adivinhar um hex.
Esse hábito previne o erro de contraste número um: o hex fixo no dark. Um color: #1a1a1a cravado num componente vira preto-sobre-preto assim que o tema escurece, e o texto some. O token adapta por tema; o hex, não.
A regra é inegociável: texto e fundo sempre fortemente contrastantes. Em tema dark ou light, a cor precisa vir de uma variável que se adapta, nunca de um hex escuro fixo que desaparece quando o fundo escurece.
Como o OKLCH torna a escala de cores previsível?
Resposta direta: porque o OKLCH é um espaço de cor perceptualmente uniforme, então ajustar a luminosidade (L) muda o brilho de forma previsível para o olho. Isso torna trivial gerar uma escala de 50 a 950 em que o contraste cresce de maneira consistente, em vez do comportamento irregular do hex e do HSL.
Na prática, você define os tons por luminosidade e o contraste passa a ser uma propriedade do sistema, não uma loteria. O contraste, aliás, é requisito e não enfeite: o piso é WCAG AA, 4,5:1 para texto normal e 3:1 para texto grande.
@theme {
/* Escala primária em OKLCH: só a luminosidade muda de forma previsível. */
--color-primary-50: oklch(0.97 0.02 277);
--color-primary-500: oklch(0.62 0.19 277);
--color-primary-700: oklch(0.50 0.19 277);
--color-primary-950: oklch(0.28 0.10 277);
/* Tokens semânticos: o componente consome estes, não a escala crua. */
--color-surface: var(--color-primary-950);
--color-text: oklch(0.98 0.01 277); /* claro sobre superfície escura: AA folgado */
--color-danger: oklch(0.58 0.20 25);
}
Repare na separação entre escala (os números) e semântica (--color-surface, --color-text, --color-danger). O componente nunca toca a escala crua; ele pede var(--color-text). Trocar a paleta inteira vira uma edição local, e o contraste foi decidido no momento de definir as luminosidades.
Como Tailwind v4 se encaixa nos tokens em vez de competir?
Resposta direta: porque o Tailwind v4 é CSS-first. O motor novo (Oxide, escrito em Rust) abandonou o tailwind.config.js e move a configuração de tema para um bloco @theme dentro do próprio CSS. Cada token declarado ali vira, automaticamente, uma variável CSS e uma classe utilitária. Design tokens e Tailwind deixam de ser dois mundos.
@import "tailwindcss";
@theme {
--color-primary: oklch(0.62 0.19 277);
--color-on-primary: oklch(0.99 0 0);
--radius-lg: 0.625rem;
}
/* Isto gera, de uma vez: a variável var(--color-primary) E as utilitárias
bg-primary, text-primary, border-primary, rounded-lg... */
Isso muda o vocabulário do prompt quando você vibecoda. Em vez de “deixe esse botão azul com canto arredondado”, você pede “use as utilitárias do nosso tema: bg-primary text-on-primary rounded-lg px-4 py-2”. O agente passa a compor a partir do seu sistema, não a inventar valores avulsos, e a coerência visual deixa de depender de disciplina manual.
Que comportamento o CSS moderno entrega sem JavaScript?
Resposta direta: muito do que antes exigia biblioteca. Container queries, :has(), popover, <dialog>, anchor positioning, View Transitions e text-wrap cobrem responsividade de componente, lógica condicional de estilo, camadas e animação, tudo nativo. O cartão de métrica abaixo é um componente de produção que usa quase todos esses recursos ao mesmo tempo.
Receita últimos 30 dias
R$ 1,24 mi
▲ 8,2%Ticket médio
R$ 312
▲ 3,1%Ruptura de estoque
6,4%
▼ 1,9 p.p. Fora da metaConversão mobile
1,8%
▲ 0,4 p.p.O primeiro card está numa coluna larga e se organiza na horizontal; os três de baixo estão numa grade estreita e empilham. É o mesmo componente, sem nenhuma variante: quem decide o layout é a container query, que mede o espaço onde o card foi colocado, não a largura da janela.
Container queries: o componente que se adapta ao contexto
.metric-card { container-type: inline-size; container-name: metric; }
/* Quando o espaço do PRÓPRIO card passa de 360px, vira linha. */
@container metric (min-width: 360px) {
.metric-card__box { flex-direction: row; align-items: center; }
}
A diferença para a media query é enorme em componentes reutilizáveis: o mesmo card vira layout horizontal numa coluna larga e empilha numa sidebar estreita, sem você reescrever nada e sem saber em que página ele está.
:has(): estilo condicional pela presença de um filho
O card de ruptura acima está com a borda vermelha. Ninguém passou uma prop de cor: ele contém um selo [data-state="alert"], e o :has() reage a isso pintando o card inteiro com o token --danger.
.metric-card__box:has([data-state="alert"]) {
border-color: var(--danger);
box-shadow: 0 0 0 1px var(--danger), 0 8px 28px -12px var(--danger-surface);
}
O texto do alerta usa --danger-text (um vermelho de luminosidade mais alta) sobre --danger-surface (uma tinta sutil), justamente para passar no contraste AA em fundo escuro, em vez de vermelho escuro sobre escuro.
popover, dialog e anchor positioning
O botão ”?” de cada card abre um detalhe com o atributo popover nativo: fechamento por Esc, dismiss ao clicar fora e camada superior vêm de graça, sem JavaScript. O anchor positioning gruda o popover no botão que o abriu.
.metric-card__info { anchor-name: var(--anchor-name); }
@supports (anchor-name: --a) {
.metric-card__detalhe {
position-anchor: var(--anchor-name);
position-area: block-end span-inline-start;
position-try-fallbacks: flip-block, flip-inline;
}
}
Para conteúdo modal (um carrinho que toma a tela, uma confirmação de exclusão), o elemento <dialog> cumpre o mesmo papel com foco preso e backdrop nativos. A regra prática: popover para sobreposições leves e não-modais; <dialog> quando o usuário precisa resolver aquilo antes de seguir.
text-wrap, scroll-driven e View Transitions
Três toques finais que custam quase nada. text-wrap: balance equilibra títulos curtos; text-wrap: pretty evita a palavra órfã na última linha de um parágrafo. As animações de entrada por scroll usam animation-timeline: view(), sem observador em JavaScript. E a View Transitions API anima a troca de estado e de página de forma fluida, ativada por um view-transition-name no elemento.
.metric-card__rotulo { text-wrap: pretty; }
.metric-card__valor { text-wrap: balance; }
[data-reveal] { animation: reveal linear both; animation-timeline: view(); }
.metric-card__box { view-transition-name: var(--vt-name, none); }
Cada um desses recursos tem fallback silencioso: navegadores sem suporte ignoram a regra e mostram o conteúdo legível, o que os torna seguros como progressive enhancement.
Como desenhar um KPI que serve à decisão?
Resposta direta: um bom dashboard é um decision cockpit — responde em segundos a “estamos bem ou mal, em relação a quê, e o que mudou?”. O inimigo número um é a vaidade visual: gráfico de pizza, medidor 3D e gauge floreado ocupam espaço e comunicam pouco. Os cards acima seguem o oposto: cada um responde às três perguntas de uma vez.
Repare no que cada MetricCard mostra. O valor responde “como estamos”; o delta com seta e número responde “o que mudou”; e o bullet chart responde “em relação a quê”, comparando o realizado contra a meta numa única linha. O card de receita bateu a meta (barra verde, ✓); o de conversão ficou abaixo (barra vermelha, ●), com o marcador vertical indicando exatamente onde a meta cai.
Prefira o bullet chart ao medidor circular. O bullet ocupa menos espaço, compara realizado contra meta numa única linha e é muito mais legível. Pizza, gauge e 3D são anti-padrão num cockpit.
A regra de quando desenhar à mão e quando chamar uma biblioteca é prática. Use SVG inline puro para os elementos simples e desenháveis à mão: KPI card, bullet chart, sparkline, barra de progresso, waterfall simples e tabela de variância. Use biblioteca (como ECharts) apenas para o que é genuinamente denso ou interativo: séries temporais com muitos pontos, tooltip, zoom, brush e tipos complexos. O bullet do card acima é um punhado de <rect> em SVG — não precisa de dependência alguma.
<svg class="metric-card__bullet" viewBox="0 0 100 12" preserveAspectRatio="none" role="img"
aria-label="Realizado em 124% da meta">
<rect class="bullet-track" x="0" y="4" width="100" height="4" rx="2" />
<rect class="bullet-measure" data-ok="true" x="0" y="4" width="62" height="4" rx="2" />
<rect class="bullet-target" x="49.3" y="1" width="1.4" height="10" />
</svg>
Quando a acessibilidade precisa de uma primitiva headless?
Resposta direta: quando o componente tem comportamento de teclado, foco e ARIA não-triviais que é fácil errar — menu, combobox, tabs, listbox, dropdown com foco gerenciado. Para esses, use uma primitiva headless (Radix ou Base UI) em vez de improvisar com div e onclick. Para UI simples (card, badge, botão), CSS e HTML semântico bastam; para sobreposições, prefira popover e <dialog> nativos antes de qualquer biblioteca.
É aqui que entra o padrão shadcn, o default de fato para React em 2026. Ele não é uma dependência fechada do npm: é um catálogo de componentes que você copia para dentro do seu projeto. O código do botão fica em src/components/ui/button.tsx, seu e editável, estilizado com utilitárias do tema (os mesmos tokens) e com o comportamento acessível delegado a uma primitiva headless por baixo.
A vantagem do shadcn é ter controle total do código sem brigar com os estilos de uma biblioteca, terceirizando apenas a parte difícil: o comportamento acessível que alguém já resolveu corretamente.
O MetricCard deste guia mostra o outro lado da moeda. Como o comportamento dele é simples (um popover nativo, sem menu nem foco preso), ele não precisa de primitiva alguma: HTML semântico, popover nativo e tokens resolvem, e o componente fica leve. Saber distinguir os dois casos é o que separa um design system enxuto de um que importa megabytes para desenhar um cartão.
Próximo passo
Comece pelo inventário: extraia para tokens as cores que hoje estão cravadas nos componentes, defina-as em OKLCH no bloco @theme e rode um grep por hex em propriedades de cor para caçar o que sobrou. Em seguida, escolha um componente reutilizável (um card, um banner) e troque a media query por container query. Para amarrar a decisão de stack a essa camada visual, volte aos guias de Astro, Next 16 e Svelte 5: o mesmo conjunto de tokens vai vestir as três com a mesma cara.
Perguntas frequentes
Por que usar tokens em vez de escrever a cor direto no componente?
Resposta direta: para mudar a decisão em um lugar só e evitar o erro de contraste mais comum. Um hex cravado num componente vira preto-sobre-preto quando o tema escurece. Um token (var(--text)) adapta por tema e mantém o contraste sob controle.
OKLCH vale o esforço sobre hex ou HSL?
Resposta direta: vale, principalmente para escalas. OKLCH é perceptualmente uniforme, então ajustar a luminosidade muda o brilho de forma previsível. Isso torna simples gerar uma escala de 50 a 950 em que cada par texto/fundo passa no contraste AA sem tentativa e erro.
Preciso de uma biblioteca para tooltip, popover e modal?
Resposta direta: cada vez menos. O atributo popover e o elemento dialog trazem foco, fechamento por Esc e camada superior nativos. Para menus, comboboxes e tabs com teclado e ARIA complexos, ainda compensa uma primitiva headless como Radix ou Base UI.
Container query é melhor que media query?
Resposta direta: para componentes reutilizáveis, sim. A media query mede a viewport; a container query mede o espaço onde o componente foi colocado. O mesmo card vira linha numa coluna larga e empilha numa sidebar estreita, sem saber em que página está.
Como esse design system se conecta à escolha de framework?
Resposta direta: ele é a camada que unifica as stacks. Se a cor, a tipografia e o espaçamento vivem como tokens, o botão tem a mesma aparência em Astro, React e Svelte, porque todos consomem a mesma variável. O framework muda; o sistema visual permanece.
Para levar deste guia
-
Token é nome para uma decisão: --color-primary em vez de um hex repetido em trinta arquivos; muda-se a marca em um só lugar.
-
Gerar cores em OKLCH torna trivial uma escala 50-950 com contraste previsível; o piso WCAG AA (4,5:1 e 3:1) é requisito, não enfeite.
-
Tailwind v4 é CSS-first: tokens declarados em @theme viram, de uma vez, variável CSS e classe utilitária; design tokens e Tailwind se encaixam.
-
CSS moderno (container queries, :has, popover, anchor, View Transitions, text-wrap) entrega comportamento que antes exigia JavaScript e bibliotecas.
-
O padrão shadcn copia componentes editáveis para o seu repo; acessibilidade não-trivial vem de primitiva headless ou de elementos nativos, não de improviso.