Typography
Escala tipográfica. Hierarquia via tamanho, peso e espaçamento.
Display — 36px bold
text-4xl font-bold tracking-tight
Heading 1 — 24px bold
text-2xl font-bold
Heading 2 — 18px semibold
text-lg font-semibold
Body Medium — 16px medium
text-base font-medium
Body — 14px regular. Texto corrido com line-height relaxed para leitura confortável em parágrafos longos.
text-sm text-zinc-700 leading-relaxed
Caption — 12px · Metadados, hints, labels secundários
text-xs text-zinc-500
Mono — 12px · Código, IDs, valores técnicosfont-mono text-xs (JetBrains Mono)
SECTION LABEL — overline
text-xs font-semibold uppercase tracking-widest text-zinc-400
<!-- Display --> <h1 class="text-4xl font-bold tracking-tight"> Título principal </h1> <!-- Headings --> <h2 class="text-2xl font-bold">Seção</h2> <h3 class="text-lg font-semibold">Subseção</h3> <!-- Body --> <p class="text-sm text-zinc-700 leading-relaxed"> Texto corrido. </p> <!-- Caption --> <p class="text-xs text-zinc-500">Metadado</p> <!-- Section label --> <p class="text-xs font-semibold uppercase tracking-widest text-zinc-400"> LABEL </p> <!-- Mono --> <code class="font-mono text-xs text-zinc-600"> valor-tecnico </code>
Color Tokens
Paleta semântica. Use o token de papel, não a cor direta.
Neutros
Foreground
zinc-900
Muted fg
zinc-600
Placeholder
zinc-400
Border
zinc-200
Surface alt
zinc-100
Surface
white
Status
Success
green-600
Success bg
green-50
Warning
yellow-500
Warning bg
yellow-50
Danger
red-600
Info
blue-600
Espaçamento
4px
p-1 / gap-1
8px
p-2 / gap-2
12px
p-3 / gap-3
16px
p-4 / gap-4
24px
p-6 / gap-6
32px
p-8 / gap-8
Border Radius
None
rounded-none
2px
rounded-sm
4px
rounded
6px
rounded-md
Input
Sempre vincule <label for="id">. Nunca use placeholder como substituto de label.
Mensagem de erro explicativa.
<!-- Padrão --> <div class="flex flex-col gap-1"> <label for="campo" class="text-sm font-medium text-zinc-700"> Rótulo </label> <input id="campo" type="text" class="w-full px-3 py-2 text-sm text-zinc-900 bg-white border border-zinc-300 placeholder:text-zinc-400 hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-zinc-900 focus:border-transparent transition-colors" /> </div> <!-- Erro --> <input aria-invalid="true" aria-describedby="msg" class="... border-red-500 focus:ring-red-500" /> <p id="msg" role="alert" class="text-xs text-red-600 flex items-center gap-1"> Mensagem. </p> <!-- Disabled --> <input disabled class="... bg-zinc-100 text-zinc-400 border-zinc-200 cursor-not-allowed" /> <!-- Com ícone prefixo --> <div class="relative"> <!-- ícone absoluto pl-3 --> <input class="... pl-9" /> </div>
Textarea
Use resize-none para consistência visual. Combine com contador de caracteres quando houver maxlength.
Aparece no seu perfil.
0 / 160
Campo obrigatório.
<!-- Padrão --> <div class="flex flex-col gap-1"> <label for="ta" class="text-sm font-medium text-zinc-700"> Rótulo </label> <textarea id="ta" rows="3" class="w-full px-3 py-2 text-sm text-zinc-900 bg-white border border-zinc-300 placeholder:text-zinc-400 hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-zinc-900 focus:border-transparent transition-colors resize-none"> </textarea> </div> <!-- Com contador de caracteres --> <textarea maxlength="160" ...></textarea> <div class="flex justify-between"> <p class="text-xs text-zinc-500">Hint.</p> <p class="mono text-xs text-zinc-400"> 0 / 160 </p> </div> <!-- Erro --> <textarea aria-invalid="true" aria-describedby="msg" class="... border-red-500 focus:ring-red-500"></textarea> <p id="msg" role="alert" class="text-xs text-red-600"> Mensagem. </p>
Select
Use appearance-none + wrapper relativo para controlar o chevron decorativo.
Campo obrigatório.
<div class="flex flex-col gap-1"> <label for="sel" class="text-sm font-medium text-zinc-700"> Opção </label> <div class="relative"> <select id="sel" class="w-full appearance-none px-3 py-2 pr-9 text-sm text-zinc-900 bg-white border border-zinc-300 hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-zinc-900 focus:border-transparent transition-colors cursor-pointer"> <option value="" disabled selected> Selecione </option> <option value="a">Opção A</option> </select> <!-- Chevron decorativo --> <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-zinc-500"> <!-- SVG chevron-down --> </div> </div> </div>
Checkbox
Técnica sr-only + peer para estilização total sem JS. Acessível nativamente.
<label class="flex items-center gap-3 cursor-pointer group w-fit"> <span class="relative flex items-center justify-center flex-shrink-0"> <!-- Input real, invisível --> <input type="checkbox" class="peer sr-only" /> <!-- Caixa visual via peer-* --> <span class="w-[18px] h-[18px] flex items-center justify-center border border-zinc-300 bg-white transition-all group-hover:border-zinc-500 peer-focus-visible:ring-2 peer-focus-visible:ring-zinc-900 peer-focus-visible:ring-offset-2 peer-checked:bg-zinc-900 peer-checked:border-zinc-900"> <svg class="w-2.5 h-2.5 text-white opacity-0 peer-checked:opacity-100 transition-opacity" viewBox="0 0 10 8" fill="none"> <path d="M1 4L3.5 6.5L9 1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/> </svg> </span> </span> <span class="text-sm text-zinc-700 select-none group-hover:text-zinc-900 transition-colors"> Rótulo </span> </label>
Radio
Seleção exclusiva. Agrupe em <fieldset> com <legend> para acessibilidade.
<fieldset> <legend class="text-sm font-medium text-zinc-700"> Grupo </legend> <label class="flex items-center gap-3 cursor-pointer group w-fit"> <span class="relative flex items-center justify-center flex-shrink-0"> <input type="radio" name="grupo" value="a" class="peer sr-only" /> <!-- Anel externo --> <span class="w-[18px] h-[18px] rounded-full border border-zinc-300 bg-white group-hover:border-zinc-500 peer-checked:border-zinc-900 transition-all"></span> <!-- Ponto interno --> <span class="absolute w-2 h-2 rounded-full bg-zinc-900 opacity-0 peer-checked:opacity-100 transition-opacity"></span> </span> Opção A </label> </fieldset>
Toggle / Switch
Use para ativar/desativar preferências com efeito imediato, sem botão Submit. Adicione role="switch" no input para acessibilidade.
<label class="flex items-center gap-3 cursor-pointer group w-fit"> <span class="relative flex-shrink-0"> <input type="checkbox" role="switch" class="peer sr-only" /> <!-- Trilha --> <span class="block w-9 h-5 bg-zinc-200 border border-zinc-300 transition-all peer-checked:bg-zinc-900 peer-checked:border-zinc-900 peer-focus-visible:ring-2 peer-focus-visible:ring-zinc-900 peer-focus-visible:ring-offset-2"></span> <!-- Bolinha deslizante --> <span class="absolute top-[3px] left-[3px] w-3.5 h-3.5 bg-white border border-zinc-300 transition-all peer-checked:translate-x-4 peer-checked:border-zinc-100"></span> </span> Notificações </label> <!-- CORREÇÃO: role="switch" deve estar no <input>, não no <span> da trilha -->
Badge
Rótulo de status ou categoria. Adicione role="status" + aria-live quando atualizar dinamicamente.
Sólido
Tonal (suave)
<!-- Sólido --> <span class="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-zinc-900 text-white"> Label </span> <!-- Tonal com dot de status --> <span class="inline-flex items-center gap-1.5 px-2 py-0.5 text-xs font-medium bg-green-50 text-green-700 border border-green-200"> <span class="w-1.5 h-1.5 rounded-full bg-green-500"aria-hidden="true"></span> Ativo </span> <!-- Dinâmico (live region) --> <span role="status" aria-live="polite" class="..."> 3 novos </span>
Tag / Chip
Rótulo interativo ou removível. Difere do Badge por ter ação de remoção ou seleção. Use aria-label descritivo no botão de fechar.
Estática
Removível
Selecionável (toggle)
<!-- Estática --> <span class="inline-flex items-center px-2.5 py-1 text-xs font-medium bg-zinc-100 text-zinc-700 border border-zinc-200"> Design </span> <!-- Removível --> <span class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium bg-zinc-100 text-zinc-700 border border-zinc-200"> React <button type="button" aria-label="Remover React" class="ml-0.5 text-zinc-400 hover:text-zinc-900 focus:outline-none focus:ring-1 focus:ring-zinc-900 transition-colors"> <!-- SVG X 12×12 --> </button> </span> <!-- Selecionável (peer trick) --> <label class="cursor-pointer"> <input type="checkbox" class="peer sr-only" /> <span class="inline-flex items-center px-2.5 py-1 text-xs font-medium border border-zinc-300 text-zinc-500 bg-white hover:border-zinc-500 peer-checked:bg-zinc-900 peer-checked:text-white peer-checked:border-zinc-900 transition-colors select-none"> UX </span> </label>
Avatar
Iniciais como fallback universal. Sempre inclua aria-label no container para leitores de tela.
Tamanhos
6
8
10
12
16
Cores por usuário
Com status indicator
<!-- Iniciais (fallback) --> <div class="w-10 h-10 bg-zinc-800 flex items-center justify-center flex-shrink-0" aria-label="Avatar de Ana Barros"> <span aria-hidden="true" class="text-sm font-semibold text-white"> AB </span> </div> <!-- Com imagem --> <div class="w-10 h-10 overflow-hidden flex-shrink-0"> <img src="foto.jpg" alt="Ana Barros" class="w-full h-full object-cover" /> </div> <!-- Com status indicator --> <div class="relative w-fit"> <!-- ...avatar acima... --> <span class="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-500 border-2 border-white" aria-label="Online"></span> </div>
Divider
Use <hr> para separação semântica. div para decorativo.
Padrão
Forte
Pontilhado
Com label
ou
Vertical
<!-- Padrão --> <hr class="border-0 border-t border-zinc-200" /> <!-- Forte --> <hr class="border-0 border-t-2 border-zinc-900" /> <!-- Pontilhado --> <hr class="border-0 border-t border-dashed border-zinc-300" /> <!-- Com label central --> <div class="flex items-center gap-3"> <hr class="flex-1 border-0 border-t border-zinc-200" /> <span class="text-xs text-zinc-400 font-medium uppercase tracking-widest">ou</span> <hr class="flex-1 border-0 border-t border-zinc-200" /> </div> <!-- Vertical (decorativo) --> <div class="w-px h-full bg-zinc-200" role="separator" aria-orientation="vertical"></div>
Skeleton
Placeholder de carregamento. Substitui o conteúdo enquanto os dados chegam. Animação animate-pulse do Tailwind.
Texto
Card com avatar
Linha de tabela
<!-- Wrapper com animate-pulse --> <div class="animate-pulse" aria-busy="true" aria-label="Carregando..."> <!-- Linha de texto --> <div class="h-4 bg-zinc-200 w-3/4"></div> <div class="h-4 bg-zinc-200 w-full mt-2"></div> </div> <!-- Card: avatar + linhas --> <div class="flex items-start gap-3 animate-pulse"> <div class="w-10 h-10 bg-zinc-200 flex-shrink-0"></div> <div class="flex-1 space-y-2 pt-1"> <div class="h-3.5 bg-zinc-200 w-1/3"></div> <div class="h-3 bg-zinc-200 w-full"></div> <div class="h-3 bg-zinc-200 w-4/5"></div> </div> </div> <!-- Dica: use space-y-* dentro do wrapper em vez de mt-* em cada elemento --> <div class="space-y-2 animate-pulse"> <div class="h-4 bg-zinc-200 w-full"></div> <div class="h-4 bg-zinc-200 w-3/4"></div> </div>
Progress Bar
Use o elemento nativo <progress> para acessibilidade ou o padrão role="progressbar" com aria-valuenow.
Padrão
Espessuras
<!-- Com label + valor --> <div> <div class="flex justify-between mb-1.5"> <span class="text-xs font-medium text-zinc-700"> Upload </span> <span class="mono text-xs text-zinc-500"> 72% </span> </div> <!-- Trilha --> <div class="w-full h-1.5 bg-zinc-200" role="progressbar" aria-valuenow="72" aria-valuemin="0" aria-valuemax="100" aria-label="Upload: 72%"> <!-- Preenchimento --> <div class="h-full bg-zinc-900 transition-all" style="width: 72%"></div> </div> </div> <!-- Cores de estado: Padrão → bg-zinc-900 Perigo → bg-red-500 (uso alto) Info → bg-blue-500 Sucesso → bg-green-500 --> <!-- Espessuras: h-1 h-1.5 h-2.5 h-4 -->
Tooltip
CSS puro via group + group-hover. Adicione role="tooltip" e aria-describedby no trigger.
Posições (passe o mouse)
<!-- Wrapper: relative + group --> <div class="relative group"> <!-- Trigger --> <button type="button" aria-describedby="tt-id" class="..."> Hover aqui </button> <!-- Tooltip: topo --> <div id="tt-id" role="tooltip" class="pointer-events-none absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2.5 py-1.5 bg-zinc-900 text-white text-xs font-medium whitespace-nowrap opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity z-50"> Texto da dica <span class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></span> </div> </div> <!-- Posições — troque as classes: Topo: bottom-full mb-2 Base: top-full mt-2 Esq.: right-full mr-2 Dir.: left-full ml-2 -->
Quick Reference
Classes recorrentes em todos os átomos. Memorize uma vez, aplique em tudo.
| Token | Classes Tailwind | Onde usar |
|---|---|---|
| focus-ring | focus:outline-none focus:ring-2 focus:ring-zinc-900 focus:ring-offset-2 | Todos os interativos |
| focus-ring-error | focus:ring-red-500 | Substitui em estado de erro |
| input-base | border border-zinc-300 bg-white text-zinc-900 | Input, Textarea, Select |
| input-hover | hover:border-zinc-400 | Feedback sutil no hover |
| input-error | border-red-500 | Estado inválido |
| input-disabled | bg-zinc-100 text-zinc-400 cursor-not-allowed | Campo desabilitado |
| field-wrapper | flex flex-col gap-1 | Label + Input + Hint/Error |
| btn-primary | bg-zinc-900 text-white hover:bg-zinc-700 | Ação principal |
| peer-trick | peer sr-only + peer-checked:* / peer-focus-visible:* | Checkbox, Radio, Toggle, Tag |
| error-msg | text-xs text-red-600 + role="alert" | Mensagem de erro acessível |
| switch-role | role="switch" no <input> | Toggle/Switch |
| progress-bar | role="progressbar" aria-valuenow aria-valuemin aria-valuemax | Progress Bar |
Spinner
Indicador de carregamento standalone. Use aria-label e role="status". Animação via Tailwind animate-spin.
Tamanhos
Variantes de cor
Com texto inline
<!-- Standalone --> <span role="status" aria-label="Carregando" class="inline-block w-6 h-6 border-2 border-zinc-200 border-t-zinc-900 rounded-full animate-spin"> </span> <!-- Tamanhos: sm → w-4 h-4 border-2 md → w-6 h-6 border-2 lg → w-8 h-8 border-[3px] xl → w-10 h-10 border-4 --> <!-- Cor no escuro (fundo dark) --> <span class="... border-white/30 border-t-white"></span> <!-- Dentro de botão loading --> <button disabled aria-busy="true" class="inline-flex items-center gap-2 ... opacity-70 cursor-wait"> <span role="status" aria-label="Enviando" class="inline-block w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin"></span> Enviando... </button>
Link
Hiperlinks inline com variantes semânticas. Use target="_blank" + rel="noopener noreferrer" para links externos.
Variantes
Texto com link padrão embutido.
Texto com link azul embutido.
Texto com link muted embutido.
Texto com link destrutivo embutido.
Standalone com ícone
<!-- Inline padrão --> <a href="#" class="text-zinc-900 underline underline-offset-2 decoration-zinc-400 hover:decoration-zinc-900 transition-colors"> link </a> <!-- Externo (nova aba) --> <a href="https://..." target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1.5 text-sm text-blue-600 hover:text-blue-800 font-medium transition-colors"> Recurso externo <!-- SVG external-link 14×14 --> </a> <!-- Com chevron (nav inline) --> <a class="inline-flex items-center gap-1.5 text-sm text-zinc-700 hover:text-zinc-900 font-medium transition-colors group"> Ver mais <!-- SVG chevron group-hover:text-zinc-700 --> </a>
Range / Slider
Entrada numérica contínua. Combina com <label> e valor exibido. Estilize via accent-color nativo.
<div class="flex flex-col gap-3"> <div class="flex items-center justify-between"> <label for="range" class="text-sm font-medium text-zinc-700"> Label </label> <span class="mono text-xs text-zinc-500">72%</span> </div> <input id="range" type="range" min="0" max="100" value="72" class="w-full h-1.5 bg-zinc-200 appearance-none cursor-pointer accent-zinc-900 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:bg-zinc-900 [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:rounded-full" /> <div class="flex justify-between mono text-[10px] text-zinc-400"> <span>0%</span><span>100%</span> </div> </div>
Code Block
Bloco de código com header de linguagem e botão copiar. Use <pre><code> para semântica correta.
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('Not found');
return res.json();
}
$ npm install && npm run dev
const x = 42;
<!-- Com header --> <div class="border border-zinc-200 overflow-hidden"> <div class="flex items-center justify-between px-4 py-2 bg-zinc-100 border-b border-zinc-200"> <span class="mono text-[10px] font-semibold text-zinc-500 uppercase tracking-widest"> JavaScript </span> <button type="button" class="mono text-[10px] text-zinc-400 hover:text-zinc-700 transition-colors"> copiar </button> </div> <pre class="bg-zinc-950 p-4 overflow-x-auto m-0"> <code class="mono text-xs text-zinc-300 leading-relaxed"> ... </code> </pre> </div> <!-- Sem header (inline simples) --> <pre class="bg-zinc-950 p-4 overflow-x-auto border border-zinc-800"> <code class="mono text-xs text-zinc-300"> código aqui </code> </pre>
Stepped Progress
Barra de progresso por etapas discretas. Variante do A15 para fluxos multi-step.
Segmentos
Etapa 2 de 4
Com ícones e labels
Conta
Plano
Pagamento
Confirmar
<!-- Segmentos simples --> <div class="flex gap-1" role="progressbar" aria-valuenow="2" aria-valuemin="0" aria-valuemax="4" aria-label="Etapa 2 de 4"> <!-- Concluído → bg-zinc-900 --> <div class="flex-1 h-1.5 bg-zinc-900"></div> <div class="flex-1 h-1.5 bg-zinc-900"></div> <!-- Pendente → bg-zinc-200 --> <div class="flex-1 h-1.5 bg-zinc-200"></div> <div class="flex-1 h-1.5 bg-zinc-200"></div> </div> <!-- Com ícones: Concluído: bg-zinc-900 + SVG check Atual: bg-zinc-900 + ring-2 ring-offset-2 Pendente: border-2 border-zinc-200 + número Conector concluído: bg-zinc-900 h-px mt-3.5 Conector pendente: bg-zinc-200 h-px mt-3.5 -->