Apostila profissional · Design de Experiência

Web design
avançado:
o acabamento que separa
o competente do impecável

Um guia de técnicas modernas — layout intrínseco, tipografia fluida, cor perceptual, composição, movimento nativo e performance percebida — focado nas decisões de craft e nas capacidades que a plataforma web ganhou nos últimos anos e que tornaram obsoleta metade do que se fazia com JavaScript.

12 capítulosníveis intermediário → muito avançado
20+ exercícioscom antes/depois
8 casosde acabamento destrinchado
1 demo ao vivode card ganhando camadas (cap. 9)

↓ role para começar — a barra no topo são as camadas de acabamento

Capítulo 01 Intermediário

O que é "avançado" em web design

Não é mais efeito. É decisão informada e acabamento — e, hoje, é também saber o que a plataforma passou a fazer sozinha, sem a camada de JavaScript que a gente aprendeu a empilhar.

Existe uma faixa larga entre "o site funciona e está bonito" e "o site é impecável". Quem trabalha nessa faixa não usa mais efeitos — usa decisões: por que este espaçamento e não aquele, por que a escala de tipo responde ao contêiner e não à viewport, por que esta cor tem o mesmo peso perceptual da outra, por que este estado de foco existe e é bonito. É esse conjunto que esta apostila cobre.

O que mudou na plataforma (e o que ficou obsoleto)

Boa parte do que exigia bibliotecas hoje é nativo. Se o seu repertório parou em 2021, você está escrevendo JavaScript para coisas que o navegador resolve:

Antes exigia…Hoje é nativo
JS para adaptar componente ao espaço disponível@container (container queries) e unidades cqi/cqw
Classe no pai via JS ("tem filho X?"):has() — o seletor de pai
Biblioteca para animar ao rolarScroll-driven animations (animation-timeline)
Framework para transição entre páginas/estadosView Transitions API
JS para posicionar tooltip/menu junto ao gatilhoAnchor positioning; popover
JS para animar entrada/saída de elemento@starting-style e transition-behavior: allow-discrete
Pré-processador para aninhar e organizar cascataNesting nativo e @layer (cascade layers)
Cálculo manual de escalas de coroklch(), color-mix(), cor relativa

Onde esta apostila se encaixa no catálogo

A de Web Dev & Design (do básico ao expert) dá o panorama; a de CSS Avançado, Animações e SVG cobre a sintaxe e a técnica de animação/SVG; a de Motion de Interface trata movimento como disciplina; a de Design Systems & Tokens, a escala e a governança. Esta aqui é sobre a decisão de design e o acabamento — o que escolher, por quê, e o que separa o resultado competente do impecável.

Definição de trabalho

Definição

Web design avançado é escolher, entre as capacidades atuais da plataforma, as que resolvem o problema de design com menos código e mais robustez — e depois cuidar do acabamento (estados, alinhamento óptico, ritmo, foco, movimento, percepção de velocidade) que o usuário não nota conscientemente mas que decide se o produto parece bem-feito.

Na prática · onde isso é avaliado

Portfólio e teste técnico de design/front-end, revisão de UI, implementação de design system, auditoria de qualidade de interface, trabalho em produto com barra alta de craft. Recrutadores chamam de "atenção ao detalhe", "craft", "senioridade em front-end/UI", "CSS moderno".

Exercício 1.1 — Auditoria do seu repertório

Percorra a tabela desta seção. Para cada linha, marque: eu uso a versão nativa, ou ainda resolvo com JS/biblioteca? As que ficaram marcadas como "JS" são a sua lista de estudo desta apostila.

Exercício 1.2 — Diário de acabamento (hábito da apostila)

Crie um "Banco de Acabamento". A cada semana, escolha uma interface (sua ou alheia) e registre 3 detalhes que a fazem parecer bem-feita — e 3 que a denunciam. Ao fim, você terá treinado o olho, que é a metade não-técnica desta habilidade.

Capítulo 02 Intermediário

Layout intrínseco: o fim do breakpoint

Durante uma década, layout responsivo significou "media query na largura da janela". Hoje o componente responde ao espaço que ele de fato tem — e a maioria dos breakpoints some.

2.1 O problema do breakpoint

Media query mede a viewport. Mas um card não se importa com a janela — se importa com a coluna em que ele foi colocado. O mesmo card numa sidebar de 320px e numa área principal de 900px precisa de layouts diferentes, com a mesma viewport. Por isso, componentes com media query quebram quando reutilizados noutro contexto — e o time acaba criando variantes (card--small, card--wide) que são a gambiarra de um problema já resolvido.

2.2 Container queries

/* o pai declara que é um contexto de contenção */
.card-area { container-type: inline-size; container-name: card; }

/* o componente responde ao CONTÊINER, não à janela */
@container card (min-width: 480px) {
  .card { grid-template-columns: 12rem 1fr; gap: 1.5rem; }
  .card__título { font-size: 1.5rem; }
}

Com isso, o card é autocontido: funciona em qualquer lugar, sem o pai precisar saber. Existem também as unidades de contêinercqi (1% da largura inline do contêiner), úteis para tipo e espaçamento que escalam com o componente.

2.3 Layouts que não precisam de query nenhuma

Antes de escrever uma query, veja se o layout não se resolve sozinho:

/* grade que se adapta sem breakpoint */
.grade {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
  gap: 1.5rem;
}

/* sidebar que vira empilhada quando não cabe */
.com-sidebar { display: flex; flex-wrap: wrap; gap: 1.5rem; }
.com-sidebar > .principal { flex: 1 1 60%; min-inline-size: 20rem; }
.com-sidebar > .lateral   { flex: 1 1 15rem; }

O min(18rem, 100%) evita o overflow clássico do minmax em telas estreitas. Esse tipo de layout — chamado intrínseco — deixa o conteúdo e o espaço disponível decidirem, em vez de você adivinhar tamanhos de tela.

2.4 Subgrid e :has()

/* subgrid: os filhos alinham nas MESMAS linhas do avô —
   fim do desalinhamento entre cards de alturas diferentes */
.cards { display: grid; grid-template-rows: auto 1fr auto; }
.card  { display: grid; grid-row: span 3; grid-template-rows: subgrid; }

/* :has() — o seletor de pai */
.card:has(img)            { grid-template-columns: 8rem 1fr; }
.form-campo:has(:invalid) { border-color: var(--erro); }
.pagina:has(dialog[open]) { overflow: clip; }

:has() é a mudança mais subestimada dos últimos anos: ele permite estilar um elemento em função do que existe dentro dele ou depois dele, eliminando classes de estado que antes só o JavaScript podia adicionar.

2.5 Cascade layers: a cascata sob controle

@layer reset, base, componentes, utilitarios;
/* a ordem acima decide quem vence, INDEPENDENTE de especificidade */
@layer componentes { .botao { padding: .75rem 1.25rem; } }
@layer utilitarios { .p-0 { padding: 0; } }   /* vence, mesmo com menos especificidade */

Isso encerra a guerra de !important e de seletores cada vez mais longos: a precedência passa a ser uma decisão de arquitetura declarada uma vez.

Exercício 2.1 — Mate os breakpoints

Pegue um componente seu com media queries. Reescreva com container query (ou com grade intrínseca, se der). Depois coloque-o em três larguras de contêiner diferentes na mesma página. Ele se comporta bem nas três?

Exercício 2.2 — Um :has() que apaga JavaScript

Ache no seu código uma classe adicionada por JS só para refletir um estado do DOM ("tem-imagem", "tem-erro", "menu-aberto"). Substitua por :has(). Quantas linhas de JS sumiram?

Capítulo 03 Intermediário

Tipografia fina na web

Tipografia é onde o acabamento mais aparece — e onde a web finalmente ganhou as ferramentas que a impressão tinha há décadas.

3.1 Escala fluida com clamp()

:root {
  /* mínimo, ideal (escala com a viewport), máximo */
  --passo-0: clamp(1rem, 0.95rem + 0.25vi, 1.125rem);
  --passo-1: clamp(1.25rem, 1.1rem + 0.75vi, 1.6rem);
  --passo-2: clamp(1.6rem, 1.3rem + 1.5vi, 2.5rem);
  --passo-3: clamp(2rem, 1.4rem + 3vi, 4rem);
}

Duas regras que a maioria erra: (1) use vi (viewport inline) em vez de vw, para respeitar modos de escrita; (2) sempre inclua um termo em rem na parte do meio — clamp(1rem, 2vi, 2rem) sem rem impede o texto de escalar quando o usuário aumenta a fonte do navegador, o que é uma falha de acessibilidade.

3.2 Variable fonts, de verdade

@font-face {
  font-family: "Fonte";
  src: url("fonte.woff2") format("woff2-variations");
  font-weight: 300 800;          /* faixa contínua */
  font-display: swap;
  size-adjust: 104%;             /* casa métricas com o fallback */
}
h1 { font-variation-settings: "opsz" 48; }  /* optical size no display */
p  { font-variation-settings: "opsz" 14; }  /* e no texto corrido */

O eixo optical size (opsz) é o mais negligenciado: fontes desenhadas para 12px têm contraste e espacejamento diferentes das mesmas em 60px. Usar opsz é a diferença entre "a fonte é bonita" e "o texto está bem composto". Com font-optical-sizing: auto, o navegador faz isso sozinho quando a fonte suporta.

3.3 Medida, ritmo e quebras

DecisãoTécnica
Medida (comprimento de linha)max-inline-size: 65ch para texto corrido; 45–75 caracteres é a faixa confortável.
Ritmo verticalline-height sem unidade (ex. 1.6) para herdar proporcionalmente; espaçamento entre blocos por uma escala, não ad hoc.
Títulos sem "viúva"text-wrap: balance — equilibra as linhas de um título curto.
Parágrafos sem última linha órfãtext-wrap: pretty — evita a palavra sozinha no fim.
Hifenização em coluna estreitahyphens: auto com lang correto no HTML.
Números em tabelafont-variant-numeric: tabular-nums — alinha as colunas.

3.4 Fontes sem salto de layout

A troca da fonte de fallback pela fonte real causa reflow (o CLS que a apostila de performance mede — cap. 8). Conserto de duas partes: font-display: swap com preload do arquivo crítico, e size-adjust / ascent-override no @font-face do fallback para que as métricas coincidam. Bem-feito, a troca fica praticamente invisível.

Exercício 3.1 — Escala fluida honesta

Monte uma escala de 5 passos com clamp(), todos com termo em rem no meio. Teste com o zoom do navegador em 200%: o texto cresce? (Se não cresce, a escala está errada.)

Exercício 3.2 — Antes/depois de composição

Pegue um bloco de texto seu. Aplique: medida em ch, text-wrap: pretty, line-height sem unidade e opsz se a fonte suportar. Compare as capturas de tela lado a lado.

Capítulo 04 Avançado

Cor moderna: oklch, gamut e contraste real

Hex e HSL mentem sobre o que o olho vê. Os espaços perceptuais consertam isso — e mudam a forma de construir uma paleta.

4.1 Por que sair de HSL

Em HSL, dois tons com a mesma "luminosidade" declarada têm brilhos percebidos muito diferentes: hsl(60 100% 50%) (amarelo) parece muito mais claro que hsl(240 100% 50%) (azul). Consequência prática: uma escala gerada mudando só o L do HSL fica irregular, e o time compensa "no olho", tom a tom.

OKLCH é perceptualmente uniforme: oklch(L C H) onde L é lightness real (0–1), C é croma e H é matiz. Mesma L = mesmo brilho percebido, qualquer que seja o matiz.

:root {
  --marca:   oklch(0.55 0.18 265);
  /* escala coerente: só o L varia, o brilho percebido acompanha */
  --marca-100: oklch(0.95 0.04 265);
  --marca-500: oklch(0.55 0.18 265);
  --marca-900: oklch(0.25 0.10 265);
}

4.2 Gerar a paleta sem planilha

/* cor relativa: deriva variantes da cor base */
.botao        { background: var(--marca); }
.botao:hover  { background: oklch(from var(--marca) calc(l - 0.06) c h); }
.botao:active { background: oklch(from var(--marca) calc(l - 0.12) c h); }

/* misturar para estados sutis e superfícies */
.aviso { background: color-mix(in oklab, var(--alerta) 12%, canvas); }

Isso elimina dezenas de tokens escritos à mão: as variantes de estado passam a ser derivadas da cor base, e mudar a marca muda tudo coerentemente.

4.3 Gamut amplo (P3) com fallback

.destaque { background: #0a84ff; }                    /* fallback sRGB */
@supports (color: color(display-p3 1 1 1)) {
  .destaque { background: color(display-p3 0.04 0.52 1); }  /* mais saturado */
}

Telas modernas mostram cores que o sRGB não alcança. Usar P3 dá vivacidade real — desde que você mantenha o fallback e não dependa da diferença para transmitir informação.

4.4 Contraste: WCAG 2 e o que vem depois

Cuidado com a razão de contraste do WCAG 2

A fórmula do WCAG 2.x (razão 4.5:1) é conhecida por avaliar mal certos pares — especialmente texto claro sobre fundo escuro, onde ela é permissiva demais, e cores saturadas, onde é imprevisível. O APCA (em desenvolvimento para o WCAG 3) modela melhor a percepção e leva em conta o peso e o tamanho da fonte. Na prática hoje: cumpra o WCAG 2 porque é o que a lei e as auditorias exigem, mas use o APCA como segunda leitura para os casos em que o número passa e o texto continua difícil de ler.

4.5 Temas sem duplicar tokens

:root { color-scheme: light dark; }
:root {
  --superficie: light-dark(oklch(0.99 0 0), oklch(0.19 0.01 265));
  --texto:      light-dark(oklch(0.25 0.02 265), oklch(0.94 0.01 265));
}
/* respeitar o modo de alto contraste do sistema */
@media (forced-colors: active) {
  .botao { border: 1px solid ButtonText; }
}
Exercício 4.1 — Converta a paleta para OKLCH

Pegue a escala de cor de um projeto seu. Converta para oklch() e regenere os passos variando só o L. Compare com a escala antiga: qual tem brilho percebido mais regular?

Exercício 4.2 — Estados derivados

Substitua os tokens de hover/active/disabled de um componente por cores relativas (oklch(from …)) ou color-mix(). Quantos tokens você apagou?

Capítulo 05 Avançado

Composição avançada: profundidade, máscara e mistura

Sombra genérica e cantos arredondados são o mínimo. Profundidade convincente e superfícies com caráter vêm de outras ferramentas — quase todas baratas.

5.1 Profundidade que não parece template

A sombra padrão (0 2px 4px rgba(0,0,0,.1)) é o "Arial das sombras". Profundidade convincente imita luz real: sombras em camadas, com opacidade decrescente e desfoque crescente, e uma leve dessaturação — a sombra real não é cinza neutro, ela puxa a cor do ambiente.

.elevado {
  box-shadow:
    0 1px 1px oklch(0.2 0.03 265 / .04),
    0 2px 4px oklch(0.2 0.03 265 / .04),
    0 8px 16px oklch(0.2 0.03 265 / .05),
    0 16px 32px oklch(0.2 0.03 265 / .04);
}

E lembre: profundidade também se faz sem sombra — por contraste de superfície, por borda de 1px mais escura embaixo, por sobreposição, por desfoque de fundo.

5.2 Máscara e recorte

/* esmaecer a borda de uma lista rolável, sem sobrepor um gradiente falso */
.rolavel {
  mask-image: linear-gradient(to bottom, transparent, black 2rem,
                              black calc(100% - 2rem), transparent);
}
/* recorte de forma */
.selo { clip-path: polygon(50% 0, 100% 38%, 82% 100%, 18% 100%, 0 38%); }

5.3 Modos de mistura e filtros

FerramentaUso de acabamento
mix-blend-mode: multiplyTexto ou selo que "pertence" à foto em vez de flutuar sobre ela.
mix-blend-mode: differenceTexto legível sobre fundo de luminosidade variável (cursor, hero animado).
backdrop-filter: blur()Barra ou modal translúcido com legibilidade preservada. Cuidado com o custo em GPU.
filter: saturate() contrast()Harmonizar fotos de origens diferentes numa mesma grade.

5.4 Textura sem peso

Ruído sutil quebra a chapadura do gradiente e disfarça o banding. Um SVG de feTurbulence em base64, com opacidade de 3–5%, custa poucos KB e faz muita diferença — e gradientes cônicos (conic-gradient) dão superfícies mais ricas que o linear de sempre.

A regra do acabamento

Cada efeito precisa justificar o custo — em desempenho, em acessibilidade e em atenção. backdrop-filter numa lista longa derruba o scroll; mix-blend-mode pode arruinar o contraste; ruído em excesso vira sujeira. O teste: desligue o efeito. O que se perde de fato?

Exercício 5.1 — Sombra em camadas

Substitua a sombra padrão de um card por uma sombra de 4 camadas com cor puxada do fundo. Compare lado a lado — qual parece mais "física"?

Exercício 5.2 — Uma máscara no lugar de um truque

Ache um lugar onde você sobrepôs um gradiente falso para simular esmaecimento. Troque por mask-image. Funciona sobre qualquer fundo agora?

Capítulo 06 Avançado

Estados, densidade e o detalhe que ninguém nota

O usuário não repara no alinhamento óptico nem no estado de foco bem-feito. Mas a soma dessas coisas é exatamente o que ele chama de "parece profissional".

6.1 Os oito estados de um componente

Design incompleto é o que só desenha o estado padrão. Um componente sério tem:

1–4

Padrão · Hover · Foco · Ativo

Hover só existe em ponteiro (use @media (hover: hover)). Foco tem que ser visível e bonito. Ativo dá a sensação de que o clique registrou.

5–8

Desabilitado · Carregando · Erro · Vazio

Desabilitado precisa dizer por quê. Carregando não pode causar salto. Erro precisa de texto, não só de cor. Vazio é uma tela de design, não um espaço em branco.

/* foco moderno: visível, respeitando o navegador */
.botao:focus-visible {
  outline: 2px solid var(--foco);
  outline-offset: 2px;
  border-radius: inherit;
}
/* hover só onde existe ponteiro de verdade */
@media (hover: hover) and (pointer: fine) {
  .botao:hover { background: oklch(from var(--marca) calc(l - 0.06) c h); }
}

6.2 Alinhamento óptico vs. matemático

Centralizar pelo cálculo nem sempre parece centralizado. Casos clássicos: um ícone de "play" (triângulo) centralizado matematicamente parece deslocado à esquerda — o peso visual está no lado achatado; letras redondas (o, e, c) precisam ultrapassar levemente a linha de base e a altura-x para parecerem do mesmo tamanho das retas; um botão com ícone à esquerda pede padding direito um pouco maior. A régua final é o olho, não a régua.

6.3 Densidade, alvo e espaço

6.4 Microcópia é design

"Erro" vs. "Não conseguimos salvar — sua conexão caiu. Tentar de novo?" A segunda diz o que houve, de quem é a culpa (não do usuário) e o que fazer. Botões com verbo específico ("Salvar rascunho") batem "OK". Estado vazio com uma ação batem uma ilustração bonita e um beco sem saída. Nada disso é texto: é design de interação escrito.

Exercício 6.1 — Os oito estados

Pegue um componente seu e desenhe/implemente os oito estados. Quantos não existiam? Teste o foco navegando só pelo teclado.

Exercício 6.2 — Caça ao espaçamento avulso

Numa tela sua, liste todos os valores de espaçamento usados. Quantos estão fora da escala? Normalize e compare o antes/depois.

Capítulo 07 Avançado

Movimento nativo: o que saiu do JavaScript

Transições entre páginas, animações ligadas ao scroll, entrada e saída de elementos, posicionamento de menus — tudo isso virou CSS. Menos código, mais robustez, melhor desempenho.

7.1 View Transitions

/* transições entre estados na mesma página */
document.startViewTransition(() => atualizarDOM());

/* e entre páginas, sem framework */
@view-transition { navigation: auto; }

/* elemento que "voa" da lista para o detalhe */
.card__imagem { view-transition-name: capa-do-item; }

O navegador tira um "antes" e um "depois" e interpola. O efeito de continuidade — o item da lista que cresce e vira o cabeçalho do detalhe — era um projeto de semanas; hoje são poucas linhas.

7.2 Animações ligadas ao scroll

/* barra de progresso da leitura, sem JS e sem custo de scroll handler */
.progresso {
  animation: crescer linear;
  animation-timeline: scroll();
}
/* elemento que anima ao entrar na viewport */
.reveal {
  animation: aparecer linear both;
  animation-timeline: view();
  animation-range: entry 10% cover 40%;
}
@keyframes crescer  { to { scale: 1 1; } }
@keyframes aparecer { from { opacity: 0; translate: 0 1rem; } }

Rodam fora da thread principal — não travam a rolagem como um scroll listener em JS.

7.3 Entrada, saída e o problema do display

.tooltip {
  transition: opacity .2s, display .2s allow-discrete;
  opacity: 0; display: none;
}
.tooltip.aberto { opacity: 1; display: block; }
@starting-style { .tooltip.aberto { opacity: 0; } }  /* estado inicial da entrada */

Animar a entrada de um elemento que passa de display:none exigia truques com JS e requestAnimationFrame. Com @starting-style e allow-discrete, é declarativo.

7.4 Popover e anchor positioning

<button popovertarget="menu">Opções</button>
<div id="menu" popover>…</div>

#menu { position-anchor: --gatilho; position-area: block-end span-inline-start; }

popover traz de graça: camada superior (sem z-index de guerra), fechar com Esc, fechar ao clicar fora, e gerenciamento de foco. Anchor positioning ancora o elemento ao gatilho sem medir posições em JS.

7.5 Quando ainda vale JavaScript

Movimento contínuo dirigido por física (arrastar com inércia), coreografias complexas com muitos elementos interdependentes, animação de dados em tempo real, e casos que precisam de controle fino de timeline. Para o resto — e é a maioria —, o CSS de hoje faz melhor e mais barato.

Respeite quem não quer movimento

Toda animação passa por @media (prefers-reduced-motion: reduce). Reduzir não é remover: troque deslocamento e escala por fade, mantenha a mudança de estado perceptível. Movimento vestibular (parallax, zoom grande) é o que mais causa mal-estar — é o primeiro a desligar.

Exercício 7.1 — Troque JS por CSS

Pegue uma animação sua feita com biblioteca ou scroll listener. Reimplemente com animation-timeline ou view transition. Meça: quantos KB de JS saíram? A rolagem ficou mais suave?

Exercício 7.2 — Um popover nativo

Substitua um dropdown/tooltip caseiro por popover + anchor positioning. Teste Esc, clique fora e navegação por teclado — quanto disso você não precisou implementar?

Capítulo 08 Avançado

Performance percebida é decisão de design

As métricas de experiência não são assunto só de engenharia: LCP, CLS e INP são consequências diretas de decisões de layout, imagem e feedback que o designer toma.

8.1 As três métricas, traduzidas para design

MétricaO que medeA decisão de design por trás
LCPQuando o maior elemento acima da dobra aparece.Que elemento é o "herói"? Uma imagem enorme de hero custa caro; um título tipográfico forte é instantâneo.
CLSQuanto o layout salta depois de carregar.Reservar espaço: aspect-ratio em toda mídia, altura mínima em áreas dinâmicas, métricas de fonte casadas (cap. 3.4), nunca inserir banner acima de conteúdo já visível.
INPQuão rápido a interface responde ao clique/toque.Feedback imediato: estado ativo instantâneo, resposta otimista, e não bloquear a thread com animação pesada.

8.2 Reservar espaço, sempre

img, video { aspect-ratio: attr(width) / attr(height); height: auto; }
/* ou explicitamente */
.thumb { aspect-ratio: 16 / 9; object-fit: cover; }
/* área que carrega depois */
.slot-anuncio { min-block-size: 250px; }

8.3 Imagem moderna

<img src="foto.jpg"
     srcset="foto-400.avif 400w, foto-800.avif 800w, foto-1600.avif 1600w"
     sizes="(min-width: 60rem) 40rem, 100vw"
     width="1600" height="900"
     loading="lazy" decoding="async" alt="…">

Regras: o LCP nunca é loading="lazy" (use fetchpriority="high" nele); sizes correto evita baixar 1600px para exibir 400; AVIF/WebP com fallback; e width/height sempre presentes para reservar o espaço.

8.4 Esqueleto, spinner ou otimista?

< 100ms

Nada

Parece instantâneo. Qualquer indicador aqui só adiciona ruído e faz parecer mais lento.

100ms – 1s

Resposta otimista

Mostre o resultado presumido já (o "curtido" acende no clique) e reconcilie depois. É o que mais reduz a sensação de lentidão.

1s – 3s

Esqueleto

Com a forma real do conteúdo. Esqueleto genérico que não corresponde ao layout final causa CLS e frustração.

> 3s

Progresso com contexto

Barra determinada quando você sabe o progresso, mais texto do que está acontecendo. Spinner infinito é a pior opção.

8.5 O custo de cada efeito

Animar opacity e transform é barato (composição na GPU). Animar width, height, top, margin dispara layout a cada quadro. backdrop-filter e sombras grandes em elementos que rolam são caros. content-visibility: auto em seções longas fora da tela é quase de graça e economiza muito. Antes de aprovar um efeito, pergunte em que ele custa.

Exercício 8.1 — Cace o CLS

Abra uma página sua e recarregue com a rede lenta simulada. Onde o layout salta? Para cada salto, aplique a reserva de espaço adequada e meça de novo.

Exercício 8.2 — Escolha o feedback certo

Liste 5 ações da sua interface e o tempo real de cada uma. Aplique a tabela de 8.4. Algum spinner deveria ser resposta otimista? Algum esqueleto deveria ser nada?

Capítulo 09 Avançado · Demo

Um card ganhando acabamento — demo ao vivo

Role devagar. Um card de produto "que funciona" recebe as camadas desta apostila, uma por vez — e vira outro objeto no fim.

Acabamento é acumulativo: nenhuma camada isolada transforma; as seis juntas, sim. A demo mostra o card sendo refeito no painel da esquerda.

Ponto de partida

O card que "está pronto"

Largura fixa de 320px, padding em px, sombra padrão, tipo em px, uma media query para o mobile. Nada está errado — e é exatamente por isso que ele parece igual a todos os outros. Não há decisão nenhuma visível.

Camada 1

Layout intrínseco (cap. 2)

O contêiner declara container-type; o card responde a ele com @container, não à viewport. A largura fixa some. Agora o mesmo componente funciona na sidebar estreita e na área principal larga — sem variantes, sem o pai saber de nada.

Camada 2

Tipografia fluida e cor perceptual (caps. 3 e 4)

O título escala com o contêiner via clamp() com unidade cqi, com text-wrap: balance para não deixar viúva. O texto ganha medida em ch e pretty. A cor vira oklch, e hover/active passam a ser derivados da base com cor relativa — três tokens a menos.

Camada 3

Estados e alinhamento óptico (cap. 6)

Entram os oito estados: foco visível com :focus-visible, hover só onde há ponteiro, carregando sem salto, erro com texto, vazio com ação. Alvo de toque de 44px, espaçamento normalizado na escala, e o ícone deslocado 1px para parecer centralizado. É a camada que ninguém nota e todo mundo sente.

Camadas 4 e 5

Profundidade, movimento nativo e performance (caps. 5, 7 e 8)

Sombra de quatro camadas com a cor do ambiente em vez do cinza padrão. view-transition-name faz a capa voar da lista para a página de detalhe. Tudo dentro de prefers-reduced-motion. E a imagem ganha aspect-ratio, srcset/sizes e fetchpriority — zero salto de layout, LCP rápido. O card agora é impecável, e nenhuma das camadas foi cara.

Exercício 9.1 — Seu componente pelas 5 camadas

Pegue um componente seu que "está pronto". Aplique as camadas na mesma ordem: layout intrínseco → tipo e cor → estados e óptica → profundidade e movimento → performance. Guarde a captura de tela antes e depois de cada camada.

Capítulo 10 Muito avançado

Acessibilidade como técnica, e os limites do avançado

Acessibilidade não é uma checklist no fim — é uma série de decisões de design que só o designer pode tomar. E "avançado" tem armadilhas: nem tudo que é novo deve entrar em produção hoje.

10.1 O que o design decide antes do dev implementar

Decisão de designConsequência de acessibilidade
Ordem visual dos elementosSe a ordem do DOM não bate com a visual (por order ou grid-area), a navegação por teclado pula de um lado para o outro. A ordem do código é a ordem de leitura.
Como o foco é desenhadoRemover outline "porque é feio" é a falha de acessibilidade mais comum da web. Redesenhe-o; não o apague.
Cor como portadora de informaçãoSe o único sinal de erro é vermelho, quem não distingue a cor perde a informação. Cor sempre acompanhada de ícone, texto ou forma.
Densidade e alvoAlvos pequenos e juntos excluem quem tem tremor ou usa a interface em movimento.
Quantidade de movimentoParallax e zoom grandes causam mal-estar vestibular. prefers-reduced-motion não é opcional.
Comportamento em zoom 400%Requisito WCAG. Layouts intrínsecos (cap. 2) passam quase de graça; layouts com largura fixa quebram.

O detalhe de conformidade — critérios, níveis A/AA/AAA, ARIA — está na apostila de Acessibilidade Digital & WCAG. Aqui o ponto é outro: essas decisões são de design, e são tomadas no Figma, não no código.

10.2 Suporte progressivo: como usar o novo sem quebrar o velho

/* base que funciona em qualquer lugar */
.grade { display: flex; flex-wrap: wrap; gap: 1rem; }

/* melhoria onde houver suporte */
@supports (container-type: inline-size) {
  .area { container-type: inline-size; }
  @container (min-width: 30rem) { .grade { display: grid; } }
}

A regra: a experiência base tem que ser boa sozinha; o recurso novo é melhoria, não requisito. Isso vale para view transitions, scroll-driven, anchor positioning e P3 — todos degradam bem se você não apoiar a funcionalidade neles.

10.3 As armadilhas do "avançado"

Sinais de que a técnica virou vaidade
  • Efeito que só funciona num navegador e que carrega informação essencial — sem fallback, é exclusão.
  • Animação que atrapalha a tarefa: se o usuário espera a transição terminar para agir, ela é custo, não polimento. Interface de trabalho pede movimento curto (150–250ms) ou nenhum.
  • Complexidade que o time não sustenta: uma técnica que só uma pessoa entende vira dívida no primeiro handoff.
  • Craft no lugar errado: polir o hero enquanto o formulário de checkout tem estados quebrados. O acabamento vale mais onde o usuário passa tempo e decide.
  • Novidade sem problema: usar a técnica porque ela existe. Toda escolha desta apostila deve responder "que problema de design isso resolve?".

10.4 Como se manter atualizado sem correr atrás de tudo

A plataforma mudou muito em pouco tempo e vai continuar mudando. Duas práticas bastam: acompanhar o Baseline (o indicador de quando um recurso está disponível de forma ampla nos navegadores) para saber o que já dá para usar sem medo, e manter um pequeno laboratório — um projeto de brincadeira onde você testa o recurso novo antes de precisar dele em produção. O que vem a seguir, e o que isso significa para o ofício, é o tema da apostila de O Futuro do Web Design.

Exercício 10.1 — Teste de teclado e de zoom

Navegue uma tela sua usando só o teclado: a ordem faz sentido? o foco é sempre visível? Depois aplique zoom de 400%: o layout se reorganiza ou quebra?

Exercício 10.2 — Onde o craft rende mais

Liste as 5 telas em que os seus usuários passam mais tempo. O seu esforço de acabamento recente foi para essas — ou para a home?

Capítulo 11 Estudo de casos

Estudos de caso: 8 acabamentos destrinchados

Cada caso mostra o sintoma, a causa técnica e a correção. Ilustrativos.

O card que quebrava quando reutilizado

Layout · Container query

Sintoma: o card ficava ótimo na grade principal e apertado na sidebar; o time criou card--compact, depois card--compact-sm. Causa: media query mede a viewport, não o espaço do componente. Correção: container-type no contêiner + @container no card; as três variantes viraram uma.

Lição transferível — Toda variante criada "porque num lugar não cabe" é sinal de que o componente deveria responder ao contêiner.

A escala de cor que ficava "estranha" no amarelo

Cor · OKLCH

Sintoma: a paleta gerada em HSL tinha passos regulares no azul e irregulares no amarelo e no verde; o time corrigia no olho, tom a tom. Causa: HSL não é perceptualmente uniforme. Correção: paleta em oklch() variando só o L; os passos ficaram regulares em todos os matizes, e as correções manuais sumiram.

Lição transferível — Se a sua paleta precisa de ajustes "no olho" por matiz, o problema é o espaço de cor, não o seu olho.

O título com uma palavra sozinha na segunda linha

Tipografia · text-wrap

Sintoma: títulos de card quebrando com uma palavra órfã; o time inseria &nbsp; manualmente no CMS. Causa: quebra automática sem balanceamento. Correção: text-wrap: balance nos títulos e pretty nos parágrafos. Os &nbsp; manuais foram removidos do conteúdo.

Lição transferível — Quando o time está resolvendo tipografia editando o conteúdo, existe uma propriedade CSS esperando por você.

A página que saltava ao carregar

Performance · CLS

Sintoma: o usuário clicava e o botão "fugia"; CLS alto no relatório. Causas: imagens sem dimensão, fonte com métricas diferentes do fallback, e um banner injetado acima do conteúdo. Correções: aspect-ratio + width/height em toda mídia, size-adjust no @font-face de fallback, e o banner reposicionado com espaço reservado.

Lição transferível — CLS é quase sempre design: reservar espaço para tudo que chega depois é uma decisão de layout, não um ajuste de engenharia.

O menu com z-index 9999

Interação · Popover

Sintoma: guerra de z-index, menu cortado por overflow do pai, Esc não fechava, foco escapava. Causa: dropdown caseiro. Correção: popover nativo + anchor positioning — camada superior real, Esc, clique fora e foco vieram de graça; ~120 linhas de JS removidas.

Lição transferível — Se você está gerenciando z-index, foco e Esc à mão, existe uma primitiva de plataforma que faz isso melhor.

A animação de scroll que travava no celular

Movimento · Scroll-driven

Sintoma: rolagem engasgada em aparelhos médios. Causa: listener de scroll em JS recalculando posições e animando top. Correção: animation-timeline: view() animando só opacity e translate — fora da thread principal. A rolagem ficou fluida e 14KB de JS saíram.

Lição transferível — Animação ligada ao scroll é caso resolvido pela plataforma. E anime transform/opacity, nunca propriedades que disparam layout.

O foco removido "porque era feio"

Acessibilidade · Foco

Sintoma: outline: none global; ninguém conseguia usar a interface pelo teclado. Causa: o anel padrão do navegador destoava do design. Correção: anel próprio com :focus-visible, outline-offset e border-radius: inherit — bonito e visível; e nada de foco visível para cliques de mouse.

Lição transferível — O anel de foco não é para ser apagado, é para ser desenhado. :focus-visible resolve o motivo estético original.

O spinner que fazia parecer mais lento

Percepção · Feedback

Sintoma: ação de ~200ms com spinner; usuários reclamavam de lentidão. Causa: o indicador aparecia e sumia num piscar, chamando atenção para a espera. Correção: abaixo de 100ms, nada; entre 100ms e 1s, resposta otimista (o estado muda na hora e reconcilia depois). A percepção de velocidade melhorou sem nenhuma mudança no back-end.

Lição transferível — Indicador de carregamento em ação rápida cria a sensação de lentidão. Escolha o feedback pelo tempo real da ação.
Exercício 11.1 — Autópsia de caso

Escolha 2 casos. Encontre o equivalente na sua interface e aplique a mesma correção. Meça o antes/depois (linhas de código, KB, métrica, ou captura de tela).

Exercício 11.2 — Seu caso no formato

Escreva um acabamento seu no formato dos cases: sintoma → causa técnica → correção → lição. Guarde no Banco de Acabamento.

Capítulo 12 Prática · Plano

Plano de 30 dias + kit de acabamento

Craft é olho treinado mais repertório técnico. Este capítulo vira a apostila em rotina — e em artefatos de portfólio.

12.1 Plano de treino — 30 dias, ~30 min/dia

SemanaFocoEntregável ao fim da semana
1 — Layout e tipoCaps. 2–3: converter 2 componentes para container query; matar 1 classe de estado com :has(); montar uma escala fluida com clamp() e aplicar balance/pretty.Banco de Acabamento iniciado + 2 componentes intrínsecos + escala tipográfica
2 — Cor e composiçãoCaps. 4–5: converter a paleta para oklch(); derivar estados com cor relativa; refazer uma sombra em camadas; substituir 1 truque por mask-image.Paleta em OKLCH + estados derivados + 1 superfície refeita
3 — Estados e movimentoCaps. 6–7: os 8 estados em 2 componentes; normalizar a escala de espaçamento de 1 tela; trocar 1 animação JS por animation-timeline; 1 popover nativo.2 componentes completos + 1 animação nativa + 1 popover
4 — Performance e a11yCaps. 8, 10: caçar CLS em 1 página; escolher o feedback certo para 5 ações; teste de teclado e zoom 400%; auditoria de @supports nos recursos novos.1 página sem CLS + auditoria de teclado/zoom

12.2 Checklist de acabamento (antes de dar um componente como pronto)

Passe os 12 pontos
  1. Responde ao contêiner (não à viewport) e funciona em 3 larguras diferentes?
  2. Tipo fluido com termo em rem, medida em ch, balance/pretty aplicados?
  3. Cor em espaço perceptual, estados derivados da base?
  4. Contraste conferido — e conferido de novo no tema escuro?
  5. Os oito estados existem (padrão, hover, foco, ativo, desabilitado, carregando, erro, vazio)?
  6. :focus-visible desenhado e visível; hover só com @media (hover: hover)?
  7. Espaçamento na escala; espaço interno menor que o externo; alvo de toque ≥ 44px?
  8. Alinhamento óptico conferido no olho (ícones, cantos, linha de base)?
  9. Movimento com prefers-reduced-motion, curto, e animando só transform/opacity?
  10. Mídia com aspect-ratio, srcset/sizes, e o LCP sem lazy?
  11. Feedback de carregamento escolhido pelo tempo real da ação?
  12. Recurso novo dentro de @supports, com a base funcionando sozinha?

12.3 Como levar isso para o portfólio e a entrevista

12.4 Referências para continuar

Exercício final — o estudo de caso de acabamento

Daqui a 30 dias, escolha uma tela real e refaça-a aplicando as camadas do cap. 9. Documente: captura antes, captura depois, as decisões de cada camada, e as métricas (CLS/LCP, KB removidos, variantes eliminadas). É, ao mesmo tempo, o seu melhor exercício e a melhor peça de portfólio que você pode ter.