Tailwind CSS: do zero utilitário ao nível de mercado
Você já sabe CSS — esta apostila traduz esse conhecimento para o framework mais pedido em vagas de front-end. Teoria enxuta, exercícios com solução, três projetos guiados, perguntas de entrevista e um cheatsheet para o dia a dia.
O que é Tailwind e por que o mercado usa
Tailwind CSS é um framework utility-first: em vez de escrever CSS em arquivos separados, você compõe o visual aplicando pequenas classes utilitárias diretamente no HTML. Cada classe faz uma coisa só — p-4 aplica padding, flex ativa flexbox, text-center centraliza texto.
O mesmo botão, duas abordagens
Como você já domina CSS, a melhor porta de entrada é uma comparação direta:
/* styles.css */
.btn-primary {
background-color: #0ea5e9;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 600;
}
.btn-primary:hover { background-color: #0284c7; }
<!-- index.html -->
<button class="btn-primary">Salvar</button><button class="bg-sky-500 hover:bg-sky-600 text-white px-4 py-2 rounded-lg font-semibold">
Salvar
</button>Por que as empresas adotaram em massa
- Velocidade: você não sai do HTML/JSX, não inventa nomes de classe (
.card__header--activenunca mais) e não navega entre arquivos. - Consistência: a escala de design (espaçamentos, cores, tamanhos) é limitada e padronizada. Dois devs diferentes chegam a resultados visualmente coerentes.
- Manutenção segura: apagar um componente apaga o estilo junto. Não existe "CSS morto" acumulando por medo de quebrar algo em outra página.
- Bundle mínimo: o compilador só gera o CSS das classes que você realmente usou — projetos grandes ficam com ~10 kB de CSS.
- Ecossistema: shadcn/ui, Radix, Headless UI, templates, plugins — o mercado inteiro gravita em torno dele.
Críticas comuns (e como respondê-las em entrevista)
- "O HTML fica poluído" — verdade no começo; em projetos reais a repetição vive dentro de componentes (React/Vue), então cada trecho de classes existe em um único lugar.
- "É inline style com outro nome" — não: utilitários respeitam uma escala de design, suportam estados (
hover:), media queries (md:) e pseudo-elementos, coisas questyle=""não faz. - "Você não aprende CSS" — Tailwind é um mapeamento 1:1 do CSS. Quem sabe CSS (seu caso) aprende Tailwind em dias; quem não sabe, continua precisando aprender os conceitos.
Exercícios — Capítulo 1
- Liste três problemas de manutenção de CSS que você já viveu e explique, em uma frase cada, como o utility-first ataca cada um.
- Sem consultar nada, tente "adivinhar" as classes Tailwind para:
display:flex,margin-top:1rem,text-align:right. A previsibilidade dos nomes é proposital.
Ver gabarito
display:flex → flex margin-top:1rem → mt-4 (escala: 4 = 1rem) text-align:right → text-right
Instalação e setup (v4 e v3)
Em 2026 o padrão é o Tailwind v4, que simplificou radicalmente o setup: zero arquivo de configuração obrigatório, detecção automática de conteúdo e configuração feita no próprio CSS. Muitas empresas ainda mantêm projetos em v3, então você verá os dois.
Opção A — Vite (o setup mais comum em vagas com React)
# 1. criar o projeto
npm create vite@latest meu-app -- --template react
cd meu-app
# 2. instalar tailwind + plugin do vite
npm install tailwindcss @tailwindcss/viteimport tailwindcss from '@tailwindcss/vite'
export default {
plugins: [tailwindcss()],
}/* No v4, UMA linha substitui as três diretivas antigas */
@import "tailwindcss";Pronto. Rode npm run dev e use classes em qualquer componente.
Opção B — Play CDN (só para estudar e prototipar)
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>v3 vs v4: o que mudou (pergunta frequente de entrevista)
| Aspecto | Tailwind v3 | Tailwind v4 |
|---|---|---|
| Configuração | tailwind.config.js em JavaScript | Direto no CSS com @theme (config JS ainda é suportada) |
| Import no CSS | @tailwind base; @tailwind components; @tailwind utilities; | @import "tailwindcss"; |
| Detecção de arquivos | Manual, via array content: [...] | Automática (respeita .gitignore) |
| Motor | PostCSS puro | Novo engine em Rust (Oxide) — builds até 10× mais rápidos |
| Cores | RGB/HSL | OKLCH (cores mais vivas em telas modernas) |
| Recursos novos | — | Container queries nativas, variáveis CSS para todos os tokens, @starting-style |
Ferramentas essenciais do dia a dia
- Tailwind CSS IntelliSense (extensão do VS Code): autocomplete, preview de cores e lint. Instalação obrigatória — sério.
- Prettier + prettier-plugin-tailwindcss: ordena as classes automaticamente na ordem oficial. Padrão em quase toda empresa que usa Tailwind.
- Documentação oficial (tailwindcss.com): a busca (Ctrl+K) é sua melhor amiga; devs profissionais consultam o tempo todo.
Exercícios — Capítulo 2
- Crie um projeto Vite + React com Tailwind v4 seguindo os passos acima e confirme que
<h1 class="text-3xl font-bold text-sky-500">Funcionou</h1>aparece grande, em negrito e azul. - Crie um arquivo
playground.htmlcom o Play CDN — você vai usá-lo nos exercícios dos próximos capítulos. - Instale a extensão IntelliSense e o plugin do Prettier e observe a ordenação automática das classes ao salvar.
A mentalidade utility-first
A maior barreira para quem vem do CSS tradicional não é técnica, é mental. A regra de ouro: pense em propriedades, não em componentes. Você não cria um "card" — você compõe fundo branco + borda arredondada + sombra + padding.
Anatomia de uma classe Tailwind
md:hover:bg-sky-500/75
─┬ ──┬── ─┬ ─┬─ ─┬─ ┬─
│ │ │ │ │ └─ opacidade (75%)
│ │ │ │ └──── tom da cor (100–950)
│ │ │ └──────── cor da paleta
│ │ └─────────── propriedade (background)
│ └─────────────── estado (só no hover)
└──────────────────── breakpoint (só em telas ≥ 768px)Lendo da direita para a esquerda: "fundo sky-500 com 75% de opacidade, quando houver hover, a partir do breakpoint md". Toda a sintaxe do Tailwind é combinação desse padrão variante:variante:utilitário.
Dicionário CSS → Tailwind
Seu conhecimento de CSS mapeia quase 1:1. Memorize os padrões, não as classes:
| CSS que você já sabe | Tailwind | Padrão |
|---|---|---|
padding: 1rem | p-4 | escala ×0.25rem |
margin-inline: auto | mx-auto | eixos: x, y, t, r, b, l |
display: flex | flex | displays têm nome direto |
justify-content: space-between | justify-between | prefixo da propriedade + valor |
font-size: 1.25rem | text-xl | escala em "camisetas" (xs–9xl) |
color: #fff | text-white | text- para cor de texto |
width: 100% | w-full | frações: w-1/2, w-2/3 |
border-radius: 9999px | rounded-full | none, sm, md, lg, xl, 2xl, full |
Fluxo de trabalho profissional
- Estruture o HTML semântico primeiro (header, main, section, button — semântica conta em acessibilidade e em code review).
- Layout de fora para dentro: container → grid/flex → espaçamentos → tipografia → cores → estados → responsivo.
- Extraia componentes quando repetir: a terceira vez que colar as mesmas classes, vire um componente React/Vue (não uma classe CSS — cap. 14).
flex-wrap: wrap? É flex-wrap. De text-transform: uppercase? É uppercase. O naming do Tailwind quase sempre é a versão encurtada e óbvia.Exercícios — Capítulo 3
- Traduza para Tailwind:
display:grid; grid-template-columns:repeat(3,1fr); gap:1.5rem; - Traduza para CSS puro:
class="fixed inset-0 bg-black/50 backdrop-blur-sm"(um overlay de modal clássico). - No seu playground, monte um card com: fundo branco, cantos
rounded-2xl, sombra, padding interno e um título em negrito. Só com utilitários.
Ver gabarito
1) grid grid-cols-3 gap-6
2) position:fixed; inset:0;
background-color: rgb(0 0 0 / 0.5);
backdrop-filter: blur(4px);
3) <div class="bg-white rounded-2xl shadow-lg p-6">
<h2 class="text-lg font-bold">Meu card</h2>
<p class="text-slate-600">Conteúdo…</p>
</div>Espaçamento, cores e tipografia
A escala de espaçamento: a espinha dorsal do Tailwind
Quase tudo (padding, margin, gap, width, height) usa a mesma escala: 1 unidade = 0.25rem = 4px. Isso força consistência — o motivo nº 1 pelo qual designs em Tailwind "parecem profissionais".
| Classe | Valor | Pixels | Uso típico |
|---|---|---|---|
p-1 | 0.25rem | 4px | micro-ajustes |
p-2 | 0.5rem | 8px | padding de badges/inputs compactos |
p-4 | 1rem | 16px | padding padrão de cards e botões |
p-6 | 1.5rem | 24px | cards espaçosos |
p-8 | 2rem | 32px | seções internas |
p-16 / p-24 | 4rem / 6rem | 64 / 96px | respiro entre seções de página |
Direções e eixos
p-4 → todos os lados
px-4 → eixo horizontal (left + right)
py-4 → eixo vertical (top + bottom)
pt-4 pr-4 pb-4 pl-4 → lados individuais
ps-4 pe-4 → início/fim lógicos (RTL-safe, ponto extra em entrevista)
-mt-4 → margens negativas: prefixo "-"gap e space-*
Em flex/grid, prefira gap-4 (o próprio gap do CSS). O utilitário space-y-4 aplica margem entre filhos diretos — útil fora de flex/grid, como empilhar parágrafos.
Cores: paleta e tons
Cada cor tem 11 tons, de 50 (quase branco) a 950 (quase preto). As famílias: slate gray zinc neutral stone red orange amber yellow lime green emerald teal cyan sky blue indigo violet purple fuchsia pink rose.
text-sky-600 → cor do texto
bg-sky-100 → cor de fundo
border-sky-300 → cor da borda
ring-sky-500 → anel de foco
fill-sky-500 → preenchimento de SVG
bg-sky-500/60 → qualquer cor aceita /opacidadesky) e UM cinza (ex.: slate) e construa a interface inteira com eles: texto principal text-slate-900, secundário text-slate-500, bordas border-slate-200, fundos bg-slate-50. Reserve a cor de marca para ações e destaques. Interfaces "poluídas" quase sempre misturam cinzas de famílias diferentes.Tipografia
| Grupo | Classes |
|---|---|
| Tamanho | text-xs text-sm text-base text-lg text-xl … text-9xl (cada uma já define um line-height adequado) |
| Peso | font-light font-normal font-medium font-semibold font-bold |
| Família | font-sans font-serif font-mono (customizável — cap. 13) |
| Altura de linha | leading-none leading-tight leading-relaxed ou text-lg/8 (tamanho/linha juntos) |
| Espaço entre letras | tracking-tight tracking-wide |
| Outros | italic uppercase capitalize truncate line-clamp-3 text-balance |
<h1 class="text-4xl font-bold tracking-tight text-slate-900">Título da página</h1>
<p class="mt-2 text-lg text-slate-600">Subtítulo explicativo.</p>
<span class="text-xs font-medium uppercase tracking-wider text-slate-400">Rótulo</span>Exercícios — Capítulo 4
- Monte um "badge de status" (
Ativo): fundoemerald-100, textoemerald-700,text-xs,font-medium,px-2 py-0.5,rounded-full. - Crie a hierarquia de um artigo: título 3xl bold, data em xs uppercase cinza, parágrafos
text-slate-600 leading-relaxedcomspace-y-4no container. - Refaça o exercício 1 trocando apenas as cores para uma versão de "erro" e uma de "aviso" — perceba como a escala de tons torna isso mecânico.
Ver gabarito (ex. 1)
<span class="inline-block rounded-full bg-emerald-100
px-2 py-0.5 text-xs font-medium text-emerald-700">
Ativo
</span>Tamanhos, bordas, sombras e fundos
Width e height
w-64 → width: 16rem (escala de espaçamento)
w-full → width: 100%
w-screen → width: 100vw
w-1/2 w-2/3 w-3/4 → frações
max-w-md max-w-3xl → larguras máximas de leitura/containers
min-h-screen → min-height: 100vh (base de layouts de página)
size-10 → width + height juntos (avatares, ícones)
h-dvh → 100dvh — correto em mobile (barra do navegador)<div class="mx-auto max-w-6xl px-4"> — centraliza, limita a largura e garante respiro lateral no mobile. Você digitará isso milhares de vezes.Bordas e anéis (rings)
border → 1px sólida
border-2 border-dashed border-slate-300
rounded-lg rounded-full rounded-t-2xl → raio (com variações por lado)
divide-y divide-slate-200 → borda ENTRE filhos (listas!)
ring-2 ring-sky-500 ring-offset-2 → anel via box-shadow, não afeta layout
outline-none → cuidado: sempre substitua por um focus visívelSombras e efeitos
shadow-sm shadow-md shadow-lg shadow-xl shadow-2xl
shadow-sky-500/30 → sombras coloridas (glow de botões)
opacity-50
blur-sm grayscale brightness-110 → filters
backdrop-blur-md bg-white/70 → efeito "vidro" (glassmorphism)Fundos e gradientes
bg-gradient-to-r from-sky-500 to-indigo-500 → gradiente 2 cores
bg-gradient-to-br from-rose-500 via-purple-500 to-indigo-500
bg-cover bg-center bg-no-repeat → imagens de fundo
bg-[url('/hero.jpg')] → URL arbitrária (cap. 15)Exercícios — Capítulo 5
- Crie um avatar:
size-12 rounded-full ring-2 ring-white shadow-mdem uma<img>. Depois empilhe três avatares sobrepostos usando-space-x-3no container (dica:flex). - Crie um botão "glow": fundo
sky-500,shadow-lg shadow-sky-500/50, texto branco,rounded-xl. - Monte um header "vidro":
sticky top-0 backdrop-blur-md bg-white/70 border-b border-slate-200.
Ver gabarito (ex. 1)
<div class="flex -space-x-3"> <img class="size-12 rounded-full ring-2 ring-white shadow-md" src="a.jpg" alt="Ana"> <img class="size-12 rounded-full ring-2 ring-white shadow-md" src="b.jpg" alt="Bia"> <img class="size-12 rounded-full ring-2 ring-white shadow-md" src="c.jpg" alt="Caio"> </div>
Flexbox
Você já sabe flexbox — aqui é só vocabulário. A tradução é literal:
| CSS | Tailwind |
|---|---|
display:flex / inline-flex | flex / inline-flex |
flex-direction: column | flex-col (e flex-row, flex-col-reverse…) |
justify-content: center | space-between | justify-center | justify-between |
align-items: center | start | items-center | items-start |
flex-wrap: wrap | flex-wrap |
flex: 1 1 0% / flex-grow / flex-shrink:0 | flex-1 / grow / shrink-0 |
align-self: center | self-center |
gap: 1rem | gap-4 (e gap-x-4, gap-y-2) |
Os 4 padrões flex que resolvem 90% do trabalho
<!-- 1. Navbar: logo à esquerda, links à direita -->
<nav class="flex items-center justify-between px-6 py-4">…</nav>
<!-- 2. Centralização perfeita (o clássico) -->
<div class="flex min-h-screen items-center justify-center">…</div>
<!-- 3. Item de lista: avatar fixo + texto que cresce + ação -->
<li class="flex items-center gap-3">
<img class="size-10 shrink-0 rounded-full" src="…" alt="">
<div class="min-w-0 flex-1">
<p class="truncate font-medium">Nome muito comprido…</p>
</div>
<button class="shrink-0">⋯</button>
</li>
<!-- 4. Footer "grudado" no fim da página -->
<body class="flex min-h-dvh flex-col">
<main class="flex-1">…</main>
<footer>…</footer>
</body>min-w-0 no filho flex-1 é o que permite o truncate funcionar — sem ele, o conteúdo estoura o container. É um dos bugs de flexbox mais perguntados em pair programming; saber a causa (min-width:auto padrão de flex items) impressiona.Exercícios — Capítulo 6
- Construa a navbar do padrão 1 com um logo textual, três links (
flex gap-6) e um botão de CTA. - Construa o item de lista do padrão 3 e teste com um nome de 60 caracteres — confirme o truncate.
- Crie uma "media card" horizontal: imagem
w-32fixa + coluna de conteúdo com título, descrição e um rodapé alinhado ao fim (flex-col+mt-auto).
Grid
| CSS | Tailwind |
|---|---|
display:grid | grid |
grid-template-columns: repeat(3, 1fr) | grid-cols-3 |
grid-column: span 2 | col-span-2 |
grid-row: span 2 / posição | row-span-2 / col-start-2 |
grid-template-columns: 240px 1fr | grid-cols-[240px_1fr] (valor arbitrário) |
place-items: center | place-items-center |
Os 3 layouts grid do dia a dia
<!-- 1. Galeria de cards responsiva (o mais comum em teste técnico) -->
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">…</div>
<!-- 2. App shell: sidebar fixa + conteúdo fluido -->
<div class="grid min-h-dvh grid-cols-[260px_1fr]">
<aside class="bg-slate-900">…</aside>
<main class="p-8">…</main>
</div>
<!-- 3. Bento grid (tendência forte em landing pages) -->
<div class="grid grid-cols-4 grid-rows-2 gap-4">
<div class="col-span-2 row-span-2 rounded-2xl bg-slate-100">Destaque</div>
<div class="col-span-2 rounded-2xl bg-slate-100">B</div>
<div class="rounded-2xl bg-slate-100">C</div>
<div class="rounded-2xl bg-slate-100">D</div>
</div>Exercícios — Capítulo 7
- Monte a galeria responsiva (padrão 1) com 6 cards e confirme o comportamento redimensionando a janela.
- Reproduza o bento grid acima e depois inverta o destaque para o canto direito usando
col-start-3. - Construa o app shell do padrão 2 e coloque um header
sticky top-0dentro domain.
Position, z-index e overflow
static relative absolute fixed sticky → position
top-0 right-4 bottom-2 left-1/2 → offsets (mesma escala)
inset-0 → top/right/bottom/left: 0
inset-x-0 inset-y-0 → por eixo
z-10 z-50 -z-10 → z-index
overflow-hidden overflow-y-auto overflow-x-scrollPadrões que caem em teste técnico
<!-- Badge de notificação no canto do ícone -->
<button class="relative">
🔔
<span class="absolute -top-1 -right-1 size-4 rounded-full bg-rose-500
text-[10px] font-bold text-white grid place-items-center">3</span>
</button>
<!-- Modal com overlay -->
<div class="fixed inset-0 z-50 grid place-items-center bg-black/50 p-4">
<div class="w-full max-w-md rounded-2xl bg-white p-6 shadow-2xl">…</div>
</div>
<!-- Centralizar absoluto (o truque translate) -->
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">…</div>
<!-- Imagem cobrindo card com conteúdo por cima -->
<div class="relative h-64 overflow-hidden rounded-2xl">
<img class="absolute inset-0 size-full object-cover" src="…" alt="">
<div class="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent"></div>
<h3 class="absolute bottom-4 left-4 text-white font-bold">Título</h3>
</div>object-cover + size-full em uma <img> substitui o antigo hábito de usar background-image para fotos — mantendo semântica, alt e lazy loading. É o padrão moderno.Exercícios — Capítulo 8
- Crie o card com imagem + gradiente + título sobreposto (última receita acima).
- Monte um botão flutuante de "voltar ao topo":
fixed bottom-6 right-6 z-40. - Faça um cabeçalho de tabela
sticky top-0dentro de um containermax-h-80 overflow-y-auto.
Responsividade mobile-first
Regra que você precisa internalizar: classes sem prefixo valem para TODAS as telas; prefixos aplicam a partir daquele breakpoint para cima. Tailwind é mobile-first, como as boas práticas de CSS moderno.
| Prefixo | Min-width | Pense em |
|---|---|---|
| (nenhum) | 0 | celular |
sm: | 640px | celular grande / paisagem |
md: | 768px | tablet |
lg: | 1024px | notebook |
xl: | 1280px | desktop |
2xl: | 1536px | telas grandes |
<!-- ERRADO (mentalidade desktop-first): -->
<div class="grid-cols-3 sm:grid-cols-1"> ✗ não faz o que parece
<!-- CERTO: comece pelo mobile, adicione colunas conforme cresce -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<!-- Padrões clássicos -->
<div class="flex flex-col gap-6 md:flex-row"> empilha → lado a lado
<h1 class="text-3xl md:text-5xl"> tipografia que escala
<nav class="hidden md:flex"> menu desktop
<button class="md:hidden">☰</button> hambúrguer mobile
<section class="px-4 py-12 md:px-8 md:py-20"> respiro que cresceIntervalos e max-width
md:max-lg:flex → só ENTRE md e lg
max-md:hidden → só ABAIXO de md (esconde no mobile)Container queries (recurso moderno — v4 nativo)
Media queries olham para a janela; container queries olham para o elemento pai. Essencial para componentes reutilizáveis que aparecem tanto numa sidebar estreita quanto no conteúdo largo:
<div class="@container">
<article class="flex flex-col @md:flex-row @md:gap-6">
<!-- vira horizontal quando o CONTAINER tem ≥ 28rem, não a tela -->
</article>
</div>@container/@md:) como estratégia para design systems demonstra atualização — muita gente do mercado ainda só conhece breakpoints de viewport.Exercícios — Capítulo 9
- Monte um hero: no mobile, texto em cima e imagem embaixo; no
lg:, lado a lado com o texto ocupando 50%. - Crie uma navbar com links visíveis no desktop e botão hambúrguer no mobile (só o visual, sem JS).
- Desafio: refaça o card do exercício com
@containerem vez demd:e teste-o dentro de um pai estreito e de um pai largo.
Ver gabarito (ex. 1 — estrutura)
<section class="mx-auto grid max-w-6xl grid-cols-1 items-center
gap-10 px-4 py-16 lg:grid-cols-2">
<div>
<h1 class="text-4xl font-bold lg:text-6xl">Título</h1>
<p class="mt-4 text-lg text-slate-600">Descrição…</p>
</div>
<img class="w-full rounded-2xl" src="hero.jpg" alt="…">
</section>Estados: hover, focus, group, peer
Estados são prefixos empilháveis — a mesma lógica dos breakpoints:
hover:bg-sky-600 focus:ring-2
active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed
focus-visible:ring-2 → foco só via teclado (preferível a focus:)
first:rounded-t-lg last:border-0 odd:bg-slate-50 → pseudo-classes estruturais
placeholder:text-slate-400 selection:bg-sky-200
before:content-[''] after:content-['→'] → pseudo-elementos<button class="rounded-lg bg-sky-600 px-4 py-2 font-semibold text-white
transition hover:bg-sky-700 active:scale-[.98]
focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-sky-500 focus-visible:ring-offset-2
disabled:opacity-50 disabled:pointer-events-none">
Salvar alterações
</button>focus-visible:ring e disabled: tratados. Em code review, esquecer estados de foco é apontado como falha de acessibilidade — trate isso como parte do "definition of done" de qualquer elemento interativo.group: estilizar filhos a partir do estado do pai
<a href="#" class="group block rounded-xl border p-5 hover:border-sky-400">
<h3 class="font-semibold group-hover:text-sky-600">Título do card</h3>
<span class="inline-block transition group-hover:translate-x-1">→</span>
</a>Quando o mouse passa no <a> (que tem group), o título muda de cor e a seta desliza. Com grupos aninhados, nomeie: group/card no pai e group-hover/card: no filho.
peer: estilizar com base no estado de um irmão anterior
<input type="email" class="peer rounded-lg border px-3 py-2" placeholder="seu@email.com">
<p class="invisible text-sm text-rose-600 peer-invalid:visible">
Informe um e-mail válido.
</p>O parágrafo de erro aparece quando o input (o peer) está :invalid — zero JavaScript. Também é a base de toggles customizados (peer-checked:).
Exercícios — Capítulo 10
- Crie uma tabela com
odd:bg-slate-50nas linhas e hover destacando a linha inteira. - Faça um card de produto onde a imagem dá
group-hover:scale-105(comoverflow-hiddenno wrapper). - Construa um toggle switch usando checkbox invisível +
peer-checked:translate-x-5 peer-checked:bg-sky-500.
Ver gabarito (ex. 3)
<label class="inline-flex cursor-pointer items-center gap-3">
<input type="checkbox" class="peer sr-only">
<span class="relative h-6 w-11 rounded-full bg-slate-300
transition peer-checked:bg-sky-500
after:absolute after:left-0.5 after:top-0.5
after:size-5 after:rounded-full after:bg-white
after:shadow after:transition
peer-checked:after:translate-x-5"></span>
<span class="text-sm">Receber notificações</span>
</label>Dark mode
O prefixo dark: aplica estilos no modo escuro. Por padrão ele segue a preferência do sistema (prefers-color-scheme); a maioria dos produtos usa a estratégia de classe/seletor para dar ao usuário um botão de troca.
@import "tailwindcss";
/* dark: passa a responder à classe .dark no html */
@custom-variant dark (&:where(.dark, .dark *));<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm
dark:border-slate-700 dark:bg-slate-800">
<h3 class="font-semibold text-slate-900 dark:text-white">Faturamento</h3>
<p class="text-slate-500 dark:text-slate-400">Últimos 30 dias</p>
</div>document.querySelector('#theme-btn').addEventListener('click', () => {
const dark = document.documentElement.classList.toggle('dark');
localStorage.theme = dark ? 'dark' : 'light';
});
// no <head>, antes do CSS, para evitar "flash" do tema errado:
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) && matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}Boas práticas de dark mode que o mercado espera
- Não use preto puro: fundos em
slate-900/950, superfícies emslate-800. - Reduza contraste de texto secundário:
dark:text-slate-400, não branco. - Sombras somem no escuro — diferencie superfícies com bordas (
dark:border-slate-700). - Nível avançado: defina tokens semânticos (
bg-surface,text-muted) via variáveis CSS que mudam no.dark— assim os componentes nem precisam do prefixo (cap. 13).
Exercícios — Capítulo 11
- Adapte o card de dashboard acima criando também uma variação de badge e botão para dark.
- Implemente o toggle com persistência no seu playground.
- Desafio: crie variáveis
--color-surfacee--color-textque mudam dentro de.darke usebg-(--color-surface)no componente.
Transições e animações
Transições
transition → propriedades comuns (colors, opacity, shadow, transform)
transition-colors → só cores (mais performático, prefira quando possível)
duration-300 delay-150 ease-in-out ease-out
transition-all → evite: transiciona tudo, inclusive layoutTransforms
scale-105 -scale-x-100 rotate-45 -rotate-6
translate-x-2 -translate-y-1 skew-x-3
hover:scale-105 hover:-translate-y-1 → o "lift" clássico de cardshover:-translate-y-1 hover:shadow-xlAnimações prontas e customizadas
animate-spin → loaders
animate-pulse → skeletons de carregamento
animate-bounce → indicadores "role para baixo"
animate-ping → pontos de notificação "ao vivo"<div class="animate-pulse space-y-3">
<div class="h-4 w-3/4 rounded bg-slate-200"></div>
<div class="h-4 w-1/2 rounded bg-slate-200"></div>
<div class="h-32 rounded-xl bg-slate-200"></div>
</div>@theme {
--animate-fade-up: fade-up .5s ease-out both;
@keyframes fade-up {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: none; }
}
}
/* uso: class="animate-fade-up" */motion-safe:animate-fade-up só anima se o usuário permitir, e motion-reduce:transition-none desliga transições. Mencionar isso em entrevista sinaliza maturidade.Exercícios — Capítulo 12
- Crie um botão de loading: spinner com
animate-spin(um SVG ou uma div comborder-2 border-white/30 border-t-white rounded-full). - Monte o skeleton de um card de perfil (avatar redondo + duas linhas).
- Crie a animação
fade-upno seu projeto e aplique nos cards de uma galeria commotion-safe:.
Design tokens e @theme (v4)
No v4, o tema é definido no próprio CSS com @theme. Cada variável que você declara vira, automaticamente, classes utilitárias + uma variável CSS nativa. É assim que se implementa a identidade visual de uma empresa.
@import "tailwindcss";
@theme {
/* Cores da marca → gera bg-brand-500, text-brand-500, border-brand-500… */
--color-brand-500: oklch(0.68 0.16 240);
--color-brand-600: oklch(0.60 0.16 240);
/* Tokens SEMÂNTICOS — o nível profissional */
--color-surface: #ffffff;
--color-muted: #64748b;
/* Tipografia → gera font-display */
--font-display: "Sora", sans-serif;
/* Breakpoint extra → gera 3xl: */
--breakpoint-3xl: 120rem;
/* Espaçamento, raios, sombras… tudo é token */
--radius-card: 1.25rem;
}<button class="bg-brand-500 hover:bg-brand-600 font-display rounded-card">
Comprar agora
</button>Sobrescrever vs estender
@theme {
--color-brand-500: #0ea5e9; /* ADICIONA à paleta padrão */
}
@theme {
--color-*: initial; /* APAGA todas as cores padrão… */
--color-brand: #0ea5e9; /* …e recomeça só com as da empresa. */
--color-ink: #0b1220; /* Padrão de design systems fechados. */
}Tokens como variáveis CSS em runtime
Todo token vira variável nativa (var(--color-brand-500)), utilizável em CSS puro, styled-components, canvas, gráficos — a ponte entre Tailwind e o resto do stack:
/* em qualquer CSS ou lib de gráficos: */
.chart-line { stroke: var(--color-brand-500); }
<!-- e no HTML dá para consumir variável direto: -->
<div class="bg-(--color-surface) text-(--color-muted)">tailwind.config.js, em theme.extend (estender) ou theme direto (sobrescrever). Saiba ler os dois formatos: manutenção de projetos v3 é realidade em consultorias e produtos maduros.Exercícios — Capítulo 13
- Crie um tema com
--color-brand-*em 3 tons e aplique em botão, link e badge. - Crie os tokens semânticos
surface/mutede faça-os mudar no.dark(variável redefinida dentro do seletor) — o componente não deve precisar dedark:. - Adicione
--font-displaycom uma fonte do Google Fonts e use no h1 do seu projeto.
@apply, @layer e componentização
@apply: o recurso mais mal usado do Tailwind
@layer components {
.btn-primary {
@apply rounded-lg bg-sky-600 px-4 py-2 font-semibold text-white hover:bg-sky-700;
}
}Funciona — mas a orientação oficial (e a prática das melhores equipes) é usar com muita parcimônia. Se você recria .btn, .card, .input com @apply, você reconstruiu o CSS tradicional por cima do Tailwind e perdeu as vantagens (co-localização, remoção automática, refactor seguro).
A resposta certa: componentes de framework
export function Button({ children, ...props }) {
return (
<button
className="rounded-lg bg-sky-600 px-4 py-2 font-semibold text-white
transition hover:bg-sky-700 focus-visible:ring-2"
{...props}
>
{children}
</button>
);
}As classes existem em um único arquivo; o resto do app usa <Button>. Esta é a resposta esperada quando perguntarem "como você evita repetição de classes?".
Quando @apply é aceitável
- Estilizar HTML que você não controla (CMS, markdown renderizado, libs de terceiros).
- Meia dúzia de padrões globais minúsculos em projetos sem framework de componentes.
@layer e CSS próprio convivendo com Tailwind
@layer base {
/* resets e defaults de tags */
h1 { text-wrap: balance; }
}
@layer components {
/* classes de componente (raras!) */
}
@utility scrollbar-none {
/* v4: cria um utilitário DE VERDADE (funciona com hover:, md:, etc.) */
scrollbar-width: none;
&::-webkit-scrollbar { display: none; }
}@utility — ele ganha suporte a variantes (md:scrollbar-none) e entra na engrenagem do framework, coisa que uma classe solta não faz.Exercícios — Capítulo 14
- Crie um componente
<Badge>em React que aceita a propvariant("success" | "error" | "warning") trocando cores via objeto de mapeamento. - Crie o utilitário
@utility text-shadow-softe use comhover:. - Argumente em 5 linhas (como se fosse um comentário de code review) por que um PR cheio de novas classes
@applydeveria ser repensado.
Valores e variantes arbitrárias, has(), data-*
Valores arbitrários: colchetes
w-[347px] top-[117px] text-[13px]
bg-[#1da1f2] → cor exata da marca de terceiros
grid-cols-[240px_1fr_auto] → underscores viram espaços
w-[calc(100%-4rem)]
mask-[url(#mask)]
bg-(--minha-var) → atalho v4 para var(--minha-var)[13px] e [#3b5df2], o problema é o tema: promova esses valores a tokens no @theme. Em code review, excesso de valores arbitrários é sinal de design system mal configurado.Propriedades e variantes arbitrárias
[mask-type:luminance] → propriedade que o Tailwind não cobre
[&:nth-child(3)]:text-rose-500 → seletor arbitrário no próprio elemento
[&>li]:mt-2 → estilizar filhos diretos
supports-[backdrop-filter]:bg-white/60 → @supportsdata-* e aria-*: o padrão das bibliotecas headless
Radix, Headless UI e afins expõem estado via atributos — e o Tailwind estiliza direto por eles. É o vocabulário do front-end moderno:
<!-- um item de accordion do Radix, por exemplo -->
<div data-state="open"
class="data-[state=open]:bg-sky-50 data-[state=open]:border-sky-300">
<!-- acessibilidade estilizável -->
<a aria-current="page" class="aria-[current=page]:font-bold aria-[current=page]:text-sky-600">
<!-- shorthands prontos: aria-checked:, aria-disabled:, aria-expanded:… -->
<button class="aria-expanded:rotate-180">▾</button>has(): estilizando o pai pelo filho
<!-- card de opção que se destaca quando o radio interno é marcado -->
<label class="rounded-xl border p-4
has-checked:border-sky-500 has-checked:bg-sky-50">
<input type="radio" name="plano" class="sr-only">
Plano Pro — R$ 49/mês
</label>
<!-- formulário que reage a QUALQUER input inválido dentro dele -->
<form class="has-[input:invalid]:border-rose-400">…</form>
<!-- negação: not-* -->
<div class="not-first:mt-6">…</div>Exercícios — Capítulo 15
- Crie um seletor de planos (3 labels com radios ocultos) usando
has-checked:— sem nenhum JavaScript. - Estilize uma navegação onde o link ativo recebe
aria-current="page"e destaca-se viaaria-[current=page]:. - Use
[&>li]:para estilizar todos os itens de uma lista a partir do<ul>, sem tocar nos<li>.
Plugins oficiais e customizados
Os plugins que o mercado usa
| Plugin | O que faz | Quando usar |
|---|---|---|
@tailwindcss/typography | Classe prose que formata HTML "cru" (blog, CMS, markdown) com tipografia bonita | Blogs, docs, conteúdo de editor rich-text |
@tailwindcss/forms | Reset consistente de inputs, selects, checkboxes entre navegadores | Qualquer app com formulários (menos necessário no v4, que melhorou os defaults) |
tailwindcss-animate / tw-animate-css | Animações de entrada/saída prontas | Base do shadcn/ui |
/* index.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";<article class="prose prose-slate lg:prose-lg dark:prose-invert">
<!-- h1, p, ul, blockquote, code… tudo formatado automaticamente -->
{conteudoDoCMS}
</article>Escrevendo seu próprio "plugin" no v4
Para a maioria dos casos, @utility e @custom-variant substituem plugins JS:
/* um utilitário com valor dinâmico */
@utility text-stroke-* {
-webkit-text-stroke-width: --value(integer)px;
}
/* uso: text-stroke-2 */
/* uma variante própria */
@custom-variant hocus (&:hover, &:focus-visible);
/* uso: hocus:bg-sky-600 → aplica em hover E focus de uma vez */Plugins JS (plugin(function({ addUtilities, matchVariant }) {...})) seguem existindo para lógica complexa e distribuição via npm — vale conhecer a assinatura para ler código de terceiros.
Exercícios — Capítulo 16
- Instale o plugin typography e renderize um markdown convertido com
prose dark:prose-invert. - Crie a variante
hocuse refatore os botões dos capítulos anteriores para usá-la. - Crie um utilitário
@utility glass(blur + fundo translúcido + borda) e aplique no header do seu playground.
Tailwind + React/Next.js: clsx, cva, tailwind-merge, shadcn/ui
Este capítulo é o que separa "sei Tailwind" de "sei Tailwind do jeito que as empresas usam". O stack dominante nas vagas: React/Next.js + Tailwind + shadcn/ui, com três micro-bibliotecas que aparecem em praticamente todo codebase profissional.
1. clsx — classes condicionais legíveis
import clsx from 'clsx';
<div className={clsx(
'rounded-lg p-4', // sempre
isActive && 'bg-sky-50 border-sky-300', // condicional
{ 'opacity-50': disabled } // forma de objeto
)}>2. tailwind-merge — resolvendo conflitos de classe
Problema clássico: um componente tem p-4 e alguém passa className="p-6" por prop. No CSS, quem vence é indefinido (depende da ordem no stylesheet, não no atributo!). O twMerge resolve deterministicamente: a última classe conflitante vence.
// lib/utils.ts — este arquivo existe em milhares de codebases
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
// componente flexível e seguro:
function Card({ className, ...props }) {
return <div className={cn('rounded-2xl border bg-white p-4', className)} {...props} />;
}
// <Card className="p-6" /> → p-6 vence o p-4, como esperado ✔3. cva — variantes de componente declarativas
import { cva } from 'class-variance-authority';
const button = cva(
// base — vale para todas as variantes
'inline-flex items-center justify-center rounded-lg font-semibold transition focus-visible:ring-2',
{
variants: {
intent: {
primary: 'bg-sky-600 text-white hover:bg-sky-700',
secondary: 'border border-slate-300 hover:bg-slate-50',
danger: 'bg-rose-600 text-white hover:bg-rose-700',
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4',
lg: 'h-12 px-6 text-lg',
},
},
defaultVariants: { intent: 'primary', size: 'md' },
}
);
function Button({ intent, size, className, ...props }) {
return <button className={cn(button({ intent, size }), className)} {...props} />;
}
// uso: <Button intent="danger" size="sm">Excluir</Button>shadcn/ui: o "design system inicial" do mercado
- Não é uma biblioteca instalada — os componentes são copiados para o seu projeto (
npx shadcn@latest add button), construídos com Radix (acessibilidade) + Tailwind + cva. - Você é dono do código: customiza cores no tema, ajusta o que quiser, sem lutar contra estilos de terceiros.
- É a base de uma fração enorme dos projetos novos em React — em entrevistas para vagas com Tailwind, conte como certo que shadcn/ui vai aparecer na conversa.
const cardStyles = "..."); (3) extraia sub-componentes. O que NÃO fazer: template strings com interpolação parcial de classe (bg-${cor}-500) — o compilador não detecta classes dinâmicas e elas somem do build. Sempre escreva nomes de classe completos.Exercícios — Capítulo 17
- Implemente o utilitário
cn()e um<Card>que aceitaclassName; prove o conflitop-4vsp-6com e semtwMerge. - Construa o
<Button>com cva acima e adicione a varianteghoste o modificador booleanofullWidth. - Inicie um projeto com shadcn/ui, adicione
button,cardedialog, e customize a cor primária do tema.
Padrões profissionais e acessibilidade
Checklist de code review (o que seniors olham)
- Ordem de classes consistente — automatize com prettier-plugin-tailwindcss.
- Escala respeitada — valores arbitrários só com justificativa; repetiu, vira token.
- Estados completos — hover, focus-visible, disabled, loading em tudo que é clicável.
- Sem classes dinâmicas quebradas — nunca
text-${color}-500; use mapas de variantes. - Semântica primeiro —
buttonpara ação,apara navegação; div clicável é reprovação. - Mobile revisado de verdade — não só "não quebrou": foi pensado.
Acessibilidade com utilitários
sr-only → visível só para leitores de tela (labels, contexto)
not-sr-only → reverte (ex.: skip link que aparece no foco)
focus-visible:ring-2 → foco de teclado sempre visível
motion-reduce: / motion-safe: → respeitar prefers-reduced-motion
forced-colors: → modo de alto contraste do Windows<a href="#conteudo"
class="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4
focus:rounded-lg focus:bg-white focus:px-4 focus:py-2 focus:shadow-lg">
Pular para o conteúdo
</a>Contraste de cores
Regra prática com a paleta padrão: sobre fundo branco, texto slate-500 ou mais escuro para textos pequenos (WCAG AA ≈ 4.5:1); *-400 geralmente só serve para textos grandes ou decorativos. Sobre cores saturadas (sky-500+), texto branco. Valide com o DevTools ou WebAIM Contrast Checker.
Organização de projeto que escala
src/
├─ index.css ← @import "tailwindcss" + @theme (tokens da marca)
├─ lib/utils.ts ← cn()
├─ components/
│ ├─ ui/ ← primitivos (Button, Card, Input) — shadcn vive aqui
│ └─ features/ ← componentes de domínio compostos dos primitivos
└─ app/ | pages/ ← telas montam features; quase não têm classe própriaA pirâmide: tokens → primitivos → features → páginas. Quanto mais alto, menos classes Tailwind diretas — no topo, quase tudo é composição de componentes.
Exercícios — Capítulo 18
- Adicione um skip link funcional ao seu projeto e teste navegando por Tab.
- Audite um componente antigo seu com o checklist de code review e liste 3 correções.
- Pegue um formulário e garanta: labels associados,
focus-visibleem tudo, mensagens de erro comaria-describedby.
Performance e produção
Como o Tailwind gera CSS pequeno
- Compilação sob demanda (JIT): o compilador escaneia seus arquivos e gera apenas as classes encontradas. Não existe "CSS do framework" gigante para purgar — ele nunca é gerado.
- Resultado típico: menos de 10 kB de CSS comprimido mesmo em apps grandes, porque utilitários são infinitamente reutilizados (o custo de usar
flexpela milésima vez é zero). - v4 (engine Oxide): builds completos em milissegundos e watch quase instantâneo.
O que pode dar errado (e cai em entrevista)
// ✗ O scanner lê texto, não executa JS. Isto NÃO entra no build:
const cor = 'rose';
<div className={`bg-${cor}-500`}>
// ✔ Nomes completos, escolhidos por lógica:
const cores = { ok: 'bg-emerald-500', erro: 'bg-rose-500' };
<div className={cores[status]}>- Classe some em produção? 99% das vezes é interpolação dinâmica ou arquivo fora do escaneamento (no v4, verifique se não está no
.gitignore; use@sourcepara incluir pastas extras, ex.:@source "../node_modules/minha-lib"). - CSS crítico e caching: um único arquivo CSS pequeno e estável = excelente cache entre deploys.
- Evite
transition-alle anime preferencialmentetransform/opacity(compositor, sem reflow) — conhecimento de CSS performance que continua valendo.
Exercícios — Capítulo 19
- Rode
npm run buildno seu projeto e meça o tamanho do CSS gerado. - Introduza de propósito uma classe interpolada, confira que ela quebra no build de produção, e corrija com mapa de variantes.
- Explique por escrito (3 linhas) por que "purge" é um termo desatualizado desde o JIT.
Projeto 1 — Landing page responsiva
Missão: construir a landing de um SaaS fictício ("Pulso — monitoramento de sites") com navbar, hero, grade de features e footer. É o formato de teste técnico mais comum para vagas júnior. Construa por etapas, sem olhar o gabarito antes de tentar.
Etapa 1 — Navbar com efeito vidro
<header class="sticky top-0 z-40 border-b border-slate-200 bg-white/80 backdrop-blur-md">
<nav class="mx-auto flex h-16 max-w-6xl items-center justify-between px-4">
<a href="#" class="text-xl font-bold tracking-tight">Pulso<span class="text-sky-500">.</span></a>
<div class="hidden items-center gap-8 text-sm font-medium text-slate-600 md:flex">
<a class="hover:text-slate-900" href="#features">Recursos</a>
<a class="hover:text-slate-900" href="#precos">Preços</a>
<a class="rounded-lg bg-slate-900 px-4 py-2 text-white transition hover:bg-slate-700" href="#">Começar grátis</a>
</div>
<button class="md:hidden" aria-label="Abrir menu">☰</button>
</nav>
</header>Etapa 2 — Hero com badge, gradiente no título e CTAs
<section class="mx-auto max-w-6xl px-4 py-20 text-center md:py-32">
<span class="inline-flex items-center gap-2 rounded-full border border-sky-200
bg-sky-50 px-3 py-1 text-xs font-medium text-sky-700">
<span class="relative flex size-2">
<span class="absolute inline-flex size-full animate-ping rounded-full bg-sky-400 opacity-75"></span>
<span class="relative inline-flex size-2 rounded-full bg-sky-500"></span>
</span>
Novo: alertas via WhatsApp
</span>
<h1 class="mx-auto mt-6 max-w-3xl text-balance text-4xl font-bold tracking-tight md:text-6xl">
Saiba que seu site caiu
<span class="bg-gradient-to-r from-sky-500 to-indigo-500 bg-clip-text text-transparent">
antes do seu cliente
</span>
</h1>
<p class="mx-auto mt-6 max-w-xl text-lg text-slate-600">
Monitoramento a cada 30 segundos, alertas instantâneos e relatórios de uptime.
</p>
<div class="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<a class="w-full rounded-xl bg-sky-600 px-6 py-3 font-semibold text-white shadow-lg
shadow-sky-500/30 transition hover:bg-sky-700 sm:w-auto" href="#">
Começar grátis
</a>
<a class="w-full rounded-xl border border-slate-300 px-6 py-3 font-semibold
transition hover:bg-slate-50 sm:w-auto" href="#">
Ver demonstração
</a>
</div>
</section>Etapa 3 — Grade de features
<section id="features" class="bg-slate-50 py-20">
<div class="mx-auto max-w-6xl px-4">
<h2 class="text-center text-3xl font-bold">Tudo o que você precisa</h2>
<div class="mt-12 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<!-- repita este card 6× -->
<article class="group rounded-2xl border border-slate-200 bg-white p-6
transition hover:-translate-y-1 hover:shadow-lg">
<div class="grid size-11 place-items-center rounded-xl bg-sky-100
text-xl transition group-hover:bg-sky-500">⚡</div>
<h3 class="mt-4 font-semibold">Checagens a cada 30s</h3>
<p class="mt-2 text-sm leading-relaxed text-slate-600">
Monitoramento contínuo de 12 regiões do mundo.
</p>
</article>
</div>
</div>
</section>Complete o projeto
- Adicione uma seção de preços com 3 planos, destacando o do meio com
ring-2 ring-sky-500 scale-105e um badge "Mais popular" posicionado comabsolute -top-3. - Adicione um footer em
bg-slate-900com 4 colunas (grid sm:grid-cols-2 lg:grid-cols-4) que empilham no mobile. - Ative dark mode em toda a página.
- Bônus de portfólio: publique no Vercel/Netlify e coloque o link no seu GitHub — projeto simples publicado vale mais que projeto complexo no localhost.
Projeto 2 — Card de dashboard com dark mode
Missão: um card de métrica de dashboard SaaS — o componente que mais aparece em testes para vagas de produto. Requisitos: número grande, comparação com período anterior (positiva/negativa), mini-lista, dark mode completo e estado de loading.
<article class="w-full max-w-sm rounded-2xl border border-slate-200 bg-white p-6
shadow-sm dark:border-slate-700 dark:bg-slate-800">
<!-- cabeçalho -->
<div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400">
Receita mensal
</h3>
<button class="rounded-lg p-1.5 text-slate-400 transition hover:bg-slate-100
hover:text-slate-600 dark:hover:bg-slate-700"
aria-label="Opções">⋯</button>
</div>
<!-- métrica -->
<div class="mt-2 flex items-baseline gap-2">
<span class="text-3xl font-bold tabular-nums tracking-tight
text-slate-900 dark:text-white">R$ 48.290</span>
<span class="inline-flex items-center gap-0.5 rounded-full bg-emerald-100
px-2 py-0.5 text-xs font-semibold text-emerald-700
dark:bg-emerald-500/15 dark:text-emerald-400">
↑ 12,4%
</span>
</div>
<p class="mt-1 text-xs text-slate-400">vs. mês anterior</p>
<!-- mini-lista com divide -->
<ul class="mt-5 divide-y divide-slate-100 text-sm dark:divide-slate-700">
<li class="flex items-center justify-between py-2">
<span class="text-slate-600 dark:text-slate-300">Plano Pro</span>
<span class="font-medium tabular-nums dark:text-white">R$ 31.480</span>
</li>
<li class="flex items-center justify-between py-2">
<span class="text-slate-600 dark:text-slate-300">Plano Starter</span>
<span class="font-medium tabular-nums dark:text-white">R$ 16.810</span>
</li>
</ul>
</article>tabular-nums alinha dígitos em colunas de números (essencial em dashboards); badges no dark usam bg-emerald-500/15 (cor com opacidade) em vez de tons sólidos — fica elegante em qualquer fundo; o botão "⋯" tem aria-label.Complete o projeto
- Crie a variação negativa (↓ 3,2% em rose) e transforme em componente React com props
title,value,delta. - Crie a versão skeleton do card inteiro com
animate-pulse. - Monte um grid de 4 cards responsivo (
grid-cols-1 sm:grid-cols-2 xl:grid-cols-4) com dados diferentes.
Projeto 3 — Formulário acessível com validação visual
Missão: formulário de cadastro com validação nativa estilizada — sem JavaScript de validação. Formulários são onde acessibilidade e Tailwind se encontram, e onde muitos candidatos escorregam.
<form class="mx-auto max-w-md space-y-5 rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
<h2 class="text-2xl font-bold">Criar conta</h2>
<div>
<label for="email" class="mb-1.5 block text-sm font-medium text-slate-700">
E-mail
</label>
<input id="email" type="email" required aria-describedby="email-erro"
class="peer w-full rounded-lg border border-slate-300 px-3 py-2
placeholder:text-slate-400
focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500/30
invalid:[&:not(:placeholder-shown)]:border-rose-400"
placeholder="voce@empresa.com">
<p id="email-erro"
class="mt-1.5 hidden text-sm text-rose-600
peer-[&:invalid:not(:placeholder-shown)]:block">
Informe um e-mail válido.
</p>
</div>
<!-- checkbox de termos -->
<label class="flex items-start gap-3 text-sm text-slate-600">
<input type="checkbox" required
class="mt-0.5 size-4 rounded border-slate-300 accent-sky-600">
Li e aceito os <a class="font-medium text-sky-600 underline-offset-2 hover:underline" href="#">termos de uso</a>.
</label>
<button class="w-full rounded-lg bg-sky-600 py-2.5 font-semibold text-white transition
hover:bg-sky-700 focus-visible:ring-2 focus-visible:ring-sky-500
focus-visible:ring-offset-2 disabled:opacity-50">
Criar conta
</button>
</form>A mágica do erro: invalid:[&:not(:placeholder-shown)] só marca o campo como errado depois que o usuário digitou algo (evita o formulário nascer todo vermelho), e o peer revela a mensagem correspondente.
Complete o projeto
- Adicione campo de senha com dica de requisitos que fica
text-emerald-600quando o padrão (patternouminlength) é satisfeito, viapeer-valid:. - Adicione um
<select>de "como nos conheceu" estilizado de forma consistente com os inputs. - Teste o formulário inteiro só com teclado (Tab/Enter) e com o leitor de tela do sistema; corrija o que travar.
- Versão dark de tudo.
Perguntas de entrevista (com respostas)
As perguntas reais mais frequentes sobre Tailwind em processos de front-end. Tente responder em voz alta antes de abrir cada resposta.
O que é utility-first e quais as vantagens sobre CSS tradicional/BEM?
Tailwind não é a mesma coisa que estilos inline?
hover:, focus:), media queries (md:), pseudo-elementos (before:), dark mode, nem restringem valores a uma escala. Utilitários são classes reais com especificidade normal, geradas a partir de um design system configurável — inline styles são valores mágicos soltos.Como você evita repetição de classes em um projeto grande?
<Button> ou <Card> com variantes gerenciadas por cva ou mapas de objeto. @apply só para HTML que não controlo (markdown de CMS, por exemplo). Complemento: tokens no @theme para valores repetidos.Por que `bg-${cor}-500` não funciona em produção?
{ ok: 'bg-emerald-500', erro: 'bg-rose-500' }).Para que servem tailwind-merge e clsx? Por que quase todo projeto tem um cn()?
clsx monta strings de classe condicionalmente com sintaxe limpa. tailwind-merge resolve conflitos entre utilitários (ex.: p-4 da base vs p-6 vindo por prop), garantindo que a última vença — sem ele, o resultado dependeria da ordem no CSS gerado, que é imprevisível. cn() combina os dois e é o padrão para componentes que aceitam className.Como funciona a responsividade no Tailwind?
sm: md: lg:) aplicam de um min-width para cima. Também há max-md: para "abaixo de", intervalos (md:max-lg:) e, no v4, container queries nativas (@container + @md:) que respondem ao tamanho do elemento pai — ideais para design systems.O que mudou do Tailwind v3 para o v4?
@theme em vez de tailwind.config.js, que segue suportado); @import "tailwindcss" substituiu as três diretivas; detecção automática de conteúdo (sem array content); novo engine em Rust com builds muito mais rápidos; todos os tokens viram variáveis CSS nativas; cores em OKLCH; container queries e @utility/@custom-variant nativos.Como você implementaria dark mode em um produto real?
.dark no html) para dar controle ao usuário, com script no head lendo localStorage/preferência do sistema antes do primeiro paint (evita flash). Nos componentes, dark: nos pontos de cor — ou, em escala, tokens semânticos (surface/muted) via variáveis CSS redefinidas no .dark, deixando os componentes agnósticos ao tema.O HTML cheio de classes não prejudica a performance?
Quando você usaria @apply?
@utility, que ganha suporte a variantes.Como estilizar componentes de bibliotecas headless (Radix/Headless UI)?
data-*/aria-*, e o Tailwind estiliza direto: data-[state=open]:rotate-180, aria-expanded:bg-sky-50. É o padrão do shadcn/ui: Radix cuida de comportamento e acessibilidade; Tailwind + cva cuidam do visual.Pergunta prática comum: "Recebeu um Figma. Qual seu processo até o PR?"
@theme; 2) HTML semântico da estrutura; 3) layout de fora para dentro (containers → grid/flex → espaçamento), mobile-first; 4) tipografia e cores; 5) estados e acessibilidade (focus-visible, aria); 6) dark mode se aplicável; 7) conferir com o design sobreposto, rodar prettier para ordenar classes, revisar responsivo real antes do PR.Cheatsheet de referência
Cole na segunda tela. Padrão: 1 unidade = 0.25rem = 4px.
Espaçamento
m-4 mx-auto -mt-2 margin
gap-4 gap-x-2 gap
space-y-4 entre filhos
ps-4 pe-4 lógicos (RTL)
Tamanho
max-w-6xl min-h-screen
size-10 w+h
h-dvh altura mobile-safe
Tipografia
font-medium/semibold/bold
leading-relaxed tracking-tight
truncate line-clamp-3
uppercase italic text-balance
Cores
slate/sky/emerald/rose…
tons 50→950
bg-sky-500/60 opacidade
bg-(--var) variável CSS
Flex
items-center justify-between
flex-1 grow shrink-0
self-end order-first
truncate em flex: min-w-0
Grid
col-span-2 row-span-2
grid-cols-[240px_1fr]
place-items-center
Posição
inset-0 top-0 -top-1
z-10 z-50
overflow-hidden overflow-y-auto
Borda & sombra
rounded-full divide-y
ring-2 ring-offset-2
shadow-md shadow-sky-500/30
Responsivo
xl:1280 2xl:1536
max-md:hidden md:max-lg:flex
@container @md:flex-row
Estados
disabled: checked: invalid:
first: last: odd: not-first:
group-hover: peer-checked:
has-checked: aria-expanded:
data-[state=open]:
Efeitos & motion
hover:scale-105 -translate-y-1
animate-spin/pulse/ping
backdrop-blur-md bg-white/70
motion-safe: motion-reduce:
v4 no CSS
@theme { --color-brand: … }
@utility nome { … }
@custom-variant hocus (…)
@plugin "@tailwindcss/typography"
@source "../lib"
Receitas de bolso
/* container de página */ mx-auto max-w-6xl px-4
/* centralizar tudo */ flex min-h-dvh items-center justify-center
/* galeria responsiva */ grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3
/* card padrão */ rounded-2xl border border-slate-200 bg-white p-6 shadow-sm
/* overlay de modal */ fixed inset-0 z-50 grid place-items-center bg-black/50 p-4
/* header vidro */ sticky top-0 z-40 bg-white/80 backdrop-blur-md border-b
/* input padrão */ w-full rounded-lg border border-slate-300 px-3 py-2
focus:border-sky-500 focus:ring-2 focus:ring-sky-500/30
/* foco acessível */ focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-sky-500 focus-visible:ring-offset-2Roadmap: próximos passos para o mercado
Plano de 4 semanas (partindo de quem já sabe CSS)
| Semana | Foco | Entregável |
|---|---|---|
| 1 | Módulos 1–2 (fundamentos + layout). Faça TODOS os exercícios no playground. | Landing page do cap. 20 publicada |
| 2 | Módulos 3–4 (estados, dark mode, tokens). Refaça a landing com @theme próprio e dark mode. | Landing v2 com tema e dark |
| 3 | Módulo 5 em um projeto React: cn(), cva, shadcn/ui. | Mini-dashboard (cap. 21) com 4+ componentes |
| 4 | Simulação: escolha um design no Figma Community e reproduza contra o relógio (4h). Revise as perguntas do cap. 23. | Clone pixel-quase-perfect no portfólio |
O que colocar no currículo/portfólio
- “Tailwind CSS (v3/v4)” junto de React/Next.js — a combinação é o que as vagas pedem.
- 2–3 projetos publicados (Vercel/Netlify) com README mostrando: responsivo, dark mode, acessibilidade, componentes com variantes.
- Um repositório com seu "ui kit" pessoal (Button, Input, Card, Modal com cva) — demonstra pensamento de design system.
Para continuar evoluindo
- Documentação oficial — tailwindcss.com (leia as páginas de conceito, não só a referência).
- Refactoring UI — livro dos criadores do Tailwind sobre design para devs; muda seu nível visual.
- shadcn/ui + Radix docs — leia o código dos componentes que instalar: é uma aula de Tailwind avançado.
- Pratique reproduzindo interfaces reais — Linear, Stripe, Vercel: abra o DevTools e estude as escolhas.