Guias / css
CSS por dentro
Como o navegador transforma CSS em pixels: o pipeline de renderização, como seletores são avaliados, a cascata e as etapas de um valor, contextos de formatação, os algoritmos de flexbox e grid, ordem de pintura, composição e performance, e o que o CSS moderno consegue fazer sozinho.
31 jan 2025· 20 min· 16 seções
Neste guia · 16 seções
Por que entender o CSS por dentro
Escrever CSS ficou fácil: qualquer assistente de IA gera um layout com flexbox, um card com sombra ou um tema escuro em segundos. O que continua difícil, e é aí que se ganha tempo de verdade, é entender por que algo não funciona: o z-index: 9999 que fica atrás de tudo, o item flex que estoura a tela, a animação que trava no celular, a margem que some.
Esses problemas não se resolvem com mais código; resolvem-se sabendo o que o navegador faz com o CSS. Este guia é sobre isso: os mecanismos por trás e as capacidades que eles abrem. A sintaxe de cada propriedade fica com a MDN e com o Playground de CSS, onde dá para ver cada recurso funcionando.
Do HTML aos pixels
O navegador transforma HTML e CSS em imagem numa sequência de etapas, e repete parte delas toda vez que algo muda.
- Parsing: o HTML vira o DOM e o CSS vira o CSSOM. O CSS é bloqueante de renderização: o navegador não pinta nada enquanto não tiver todo o CSS da página, para não mostrar uma versão sem estilo e depois “pular”. Por isso CSS grande no
<head>atrasa a primeira tela. - Style: para cada elemento, o navegador descobre quais regras se aplicam e resolve a cascata até chegar a um valor para cada propriedade, inclusive as que você nunca escreveu.
- Layout: com os estilos resolvidos, calcula a geometria: largura, altura e posição de cada caixa. Elementos com
display: nonenem entram nesta etapa; comvisibility: hidden, entram e ocupam espaço. - Paint: gera as instruções de desenho (fundo, borda, texto, sombra) na ordem certa de empilhamento.
- Composite: a página é dividida em camadas, rasterizadas em blocos e combinadas pela GPU numa thread separada, a compositor thread.
O custo de cada mudança
Quando uma propriedade muda (por hover, por classe nova, por JavaScript), o navegador refaz o pipeline a partir da etapa que ela afeta:
| A mudança afeta | Exemplos | Refaz |
|---|---|---|
| Geometria | width, height, margin, padding, top, font-size, display | Layout → Paint → Composite (o mais caro, e pode afetar a página inteira) |
| Só a aparência | color, background, box-shadow, border-radius | Paint → Composite |
| Só a camada | transform, opacity (e filter, em vários navegadores) | Só Composite, na compositor thread |
Daí a regra de ouro: anime transform e opacity. Elas rodam na compositor thread e continuam suaves mesmo se a thread principal estiver ocupada com JavaScript. Animar left ou width refaz o layout a cada quadro, e a 60 quadros por segundo sobram cerca de 16 ms para tudo.
Layout thrashing
O navegador acumula mudanças e recalcula o layout uma vez por quadro. Mas se o JavaScript lê uma medida (offsetHeight, getBoundingClientRect()) logo depois de escrever um estilo, o navegador é obrigado a recalcular na hora. Em um laço, isso vira dezenas de layouts por quadro:
// ruim: escreve e lê alternadamente (um layout por volta)items.forEach((el) => { el.style.width = `${box.offsetWidth}px` })
// bom: lê tudo uma vez, depois escreveconst width = box.offsetWidthitems.forEach((el) => { el.style.width = `${width}px` })Como um seletor é avaliado
Seletores são padrões sobre a árvore do DOM. O que surpreende é a direção: o navegador avalia da direita para a esquerda. Para nav a, ele não procura o nav e desce; ele pega cada a da página e sobe pelos ancestrais procurando um nav.
Começar pela direita (o seletor-chave) descarta rápido a maioria dos elementos: só os a são candidatos. As relações entre partes do seletor descrevem caminhos nessa árvore:
- espaço (
nav a): qualquer ancestral acima; >(ul > li): só o pai imediato;+e~(h2 + p,h2 ~ p): irmãos seguintes, no mesmo nível;:has(): a exceção que olha para baixo ou para os lados, a partir do elemento.
Na prática, com os motores atuais, seletores raramente são o gargalo de performance; o que pesa é a quantidade de elementos afetados quando algo muda. Trocar uma classe no body invalida o estilo da página inteira; trocar no item de uma lista, só daquele trecho.
A cascata e as etapas de um valor
Quando várias declarações disputam a mesma propriedade de um elemento, a cascata escolhe uma, comparando estes critérios na ordem, e só passando para o seguinte em caso de empate:
Dois detalhes que explicam muita briga com CSS:
- Especificidade é comparada em colunas (IDs, classes, elementos), não somada. Uma única classe vence qualquer quantidade de elementos; um único ID vence qualquer quantidade de classes.
:where()tem especificidade zero, o que o torna ideal para estilos base fáceis de sobrescrever. - Camadas vêm antes da especificidade. Com
@layer reset, base, components, utilities;, uma regra na camadautilitiesvence uma debasemesmo com seletor muito mais fraco. Estilos fora de qualquer camada vencem todas as camadas. É a ferramenta nativa para parar de inflar seletores e espalhar!important.
As seis etapas de um valor
Escolher a declaração vencedora é só o começo. A especificação define as etapas pelas quais todo valor passa até virar pixel, e cada uma explica um comportamento:
- Especificado: toda propriedade de todo elemento tem um valor, mesmo sem declaração. Se a propriedade é herdada (as de texto:
color,font-*,line-height), vem do pai; se não é (margin,border,background), vem o valor inicial da especificação. É por isso quedisplayde umdivnão éblock“por natureza”: é a folha de estilos do navegador que declara isso, edisplay: revertvolta a ela. - Computado: é o que os filhos herdam. Como
emvira pixels aqui, um filho herda os pixels do pai, não oem. É o motivo do efeito cascata defont-size: 0.8emem listas aninhadas (cada nível encolhe de novo), e por querem(relativo à raiz) não tem esse problema. - Usado: porcentagens de largura dependem do layout, então só são resolvidas nesta etapa. Por isso
getComputedStyle(), apesar do nome, devolve o valor resolvido: para propriedades de geometria comowidth, é o valor usado, em pixels; para a maioria das outras, o computado.
Variáveis CSS por dentro
Custom properties são substituídas na etapa de valor computado: o navegador troca var(--x) pelo texto da variável e só então verifica se o resultado é válido. Duas consequências:
- Se o resultado for inválido (ex.:
--x: 20pxusado emcolor: var(--x)), a declaração não é descartada na leitura do CSS: ela vira inválida no momento do cálculo, e a propriedade se comporta comounset(herda ou volta ao valor inicial), em vez de cair na declaração anterior. - Variáveis herdam como propriedades de texto. Redefinir
--spacedentro de.cardmuda tudo lá dentro, e é assim que temas funcionam: um[data-theme="dark"]nohtmlredefine meia dúzia de variáveis e a página inteira muda.
Como o navegador não sabe o tipo de uma variável comum (para ele é só texto), não consegue interpolar entre dois valores, e por isso variáveis não animam. Registrar um tipo com @property resolve isso (demo).
Contextos de formatação: quem decide o layout
Não existe um único algoritmo de layout no CSS: existem vários, e o display do pai decide qual se aplica aos filhos. Na prática, display tem duas partes: como a caixa se comporta por fora (bloco ou inline) e qual algoritmo ela usa por dentro.
- Formatação em bloco: caixas empilhadas de cima para baixo, cada uma ocupando a largura disponível. É aqui que acontece o margin collapse: margens verticais de blocos vizinhos não se somam, prevalece a maior.
- Formatação inline: o conteúdo flui em line boxes, como texto. Elementos inline ignoram
widthe margens verticais, e imagens se alinham à linha de base do texto, o que deixa aquele espaço misterioso embaixo de uma<img>(resolve comdisplay: block). - Flex e grid: dentro deles, as regras do fluxo normal não valem. Não há margin collapse,
floaté ignorado nos filhos evertical-alignnão faz nada.
Block formatting context (BFC)
Um BFC é uma “região isolada” de layout: o que acontece dentro não vaza para fora. Margens dos filhos não colapsam com as do pai, e floats ficam contidos. Criam um BFC, entre outros: display: flow-root (que existe só para isso), overflow diferente de visible, itens flex e grid, position: absolute e display: inline-block. Quando uma margem “vaza” do pai ou um float sai de dentro do container, a resposta é quase sempre criar um BFC.
Containing block
Porcentagens e posicionamento são relativos ao containing block, que nem sempre é o pai:
| Elemento | Containing block |
|---|---|
| No fluxo normal | O ancestral bloco mais próximo |
position: absolute | O ancestral posicionado mais próximo (não static) |
position: fixed | A janela, exceto se um ancestral tiver transform, filter ou contain: aí é esse ancestral |
A última linha explica um bug clássico: um modal fixed que de repente rola junto com a página porque alguém colocou um transform num ancestral.
Como o tamanho de uma caixa é calculado
Todo dimensionamento no CSS é uma negociação entre dois tipos de tamanho:
- Extrínseco: imposto de fora (
width: 300px,50%, ou o espaço do pai). - Intrínseco: vem do conteúdo.
min-contenté a menor largura sem estourar (a palavra mais longa);max-contenté a largura sem quebrar nenhuma linha;fit-contentfica entre os dois, limitado pelo espaço disponível.
Um bloco com width: auto ocupa todo o espaço disponível (extrínseco); um item flex ou um elemento inline-block começa do tamanho do conteúdo (intrínseco). Saber em qual dos dois mundos uma caixa está explica a maioria dos “por que isso não ocupa a largura toda?”.
O algoritmo do flexbox
O flexbox resolve os tamanhos em passos:
Três consequências que parecem bugs, mas são o algoritmo:
flex: 1iguala os itens;flex: autonão.flex: 1significa base0%: todo o espaço é “livre” e é dividido por igual.flex: autousa o tamanho do conteúdo como base e divide só a sobra, então itens com mais texto ficam maiores.- O encolhimento é ponderado pelo tamanho. Quando falta espaço, um item com base de 400px encolhe o dobro de um com 200px, mesmo com o mesmo
flex-shrink. - Itens flex não encolhem abaixo do conteúdo, porque o
min-widthpadrão deles éauto(=min-content) no passo 2. Uma URL longa ou um bloco de código estoura o layout mesmo comflex: 1. A correção émin-width: 0.
O algoritmo do grid
O grid dimensiona as faixas (linhas e colunas) primeiro e só depois coloca os itens nelas: faixas de tamanho fixo primeiro, depois as que dependem do conteúdo (auto, min-content, minmax()), e por último o espaço restante é dividido entre as faixas fr. Por isso o grid é ótimo para estrutura (o layout manda) e o flexbox para conteúdo (os itens mandam).
Algumas capacidades que saem desse modelo:
repeat(auto-fill, minmax(240px, 1fr)): o grid calcula quantas colunas de pelo menos 240px cabem, ou seja, um layout responsivo sem nenhuma media query (demo).grid-template-areas: o layout descrito como um desenho, que pode ser redesenhado inteiro num breakpoint.- Subgrid: um item reaproveita as faixas do grid pai, alinhando título, texto e botão de cards vizinhos mesmo com conteúdos de tamanhos diferentes.
Ordem de pintura e empilhamento
Depois do layout, o navegador precisa decidir o que fica na frente. Dentro de cada stacking context (contexto de empilhamento), a pintura segue sempre a mesma ordem: o navegador pinta de baixo para cima nesta pilha, e o que é pintado depois fica na frente:
O ponto crucial: um stacking context é pintado inteiro, como uma unidade, dentro do contexto pai. O z-index de um filho só ordena os elementos dentro do contexto dele. Se o main tem z-index: 1 e o header tem z-index: 10, nada dentro do main passa à frente do header, nem com z-index: 9999.
Criam um stacking context, entre outros:
positiondiferente destaticcomz-indexdiferente deauto(efixed/stickysempre);opacitymenor que 1,transform,filter,backdrop-filteroumix-blend-mode;- itens flex ou grid com
z-index; isolation: isolate, que existe justamente para criar um contexto de propósito.
Por isso uma simples animação de opacity ou um transform num ancestral pode “quebrar” um dropdown: criou um contexto novo sem ninguém perceber.
A top layer
<dialog> aberto com showModal() e elementos com o atributo popover vão para a top layer, uma camada acima de todos os stacking contexts da página, independente de onde estão no HTML. É a solução nativa para modais, menus e tooltips, e dispensa a corrida por z-index (demo).
Composição e performance
Na etapa de composição, alguns elementos ganham camada própria na GPU: os que têm animação de transform ou opacity, will-change: transform, vídeos, canvas, elementos fixed em alguns casos. Mover uma camada é barato; criar camadas demais não é, porque cada uma ocupa memória de GPU. will-change é um aviso para o navegador se preparar, e deve ser usado pontualmente, não em tudo.
O CSS também permite dizer ao navegador o que ele pode pular:
contain: layout paint: promete que nada dentro do elemento afeta o layout ou a pintura de fora. Uma mudança lá dentro não obriga o navegador a recalcular a página inteira.content-visibility: auto: o navegador pula estilo, layout e pintura do conteúdo de seções fora da tela até que se aproximem dela. Num exemplo do web.dev, o tempo de renderização inicial de uma página longa caiu de 232 ms para 30 ms. Use comcontain-intrinsic-size: auto 500px, para a seção reservar uma altura estimada e a barra de rolagem não pular.
.article-section { content-visibility: auto; contain-intrinsic-size: auto 600px;}Animações por dentro
Animar é interpolar: calcular os valores intermediários entre um início e um fim, quadro a quadro. Isso só é possível quando o navegador sabe o tipo do valor:
- Interpoláveis: números, comprimentos, cores, transformações.
width: 100pxpara200pxgera todos os valores do meio. - Discretos: valores sem meio-termo, como
displayouvisibility. Eles trocam de uma vez. Comtransition-behavior: allow-discrete, a troca acontece no momento certo da transição, o que permite animar a saída de um elemento antes dodisplay: none. - Estado inicial: um elemento que acabou de aparecer não tem um “antes” para interpolar.
@starting-styledeclara esse estado de partida e permite animar entradas só com CSS.
transition reage a uma mudança de valor (dois estados); animation com @keyframes segue uma linha do tempo própria (vários estados, repetição). E a linha do tempo não precisa ser o relógio: com animation-timeline, ela pode ser a rolagem da página (demo).
Respeite prefers-reduced-motion: há quem sinta tontura com movimento, e o sistema operacional permite pedir menos animação.
Responsivo de dentro para fora
O modelo antigo de responsividade era a página decide: media queries olham a largura da janela e redesenham tudo. O CSS atual permite que cada parte se adapte sozinha:
| Nível | Ferramenta | O que reage |
|---|---|---|
| Página | Media queries (@media (width >= 768px)) | A janela |
| Componente | Container queries (@container) | O espaço do container do componente (demo) |
| Conteúdo | clamp(), minmax(), auto-fill, flex-wrap | O espaço disponível, continuamente, sem breakpoints (demo) |
| Usuário | prefers-color-scheme, prefers-reduced-motion, prefers-contrast | As preferências do sistema |
O ideal é resolver o máximo possível no nível do conteúdo (layouts intrínsecos), usar container queries para componentes e reservar media queries para a estrutura da página. Propriedades lógicas (margin-inline, padding-block) completam o quadro: o layout funciona também em idiomas escritos da direita para a esquerda.
Unidades também evoluíram com esse modelo: dvh acompanha a altura real da tela no celular (quando a barra do navegador aparece e some), cqi é relativa à largura do container, e oklch() define cores por luminosidade percebida, o que torna paletas e contrastes previsíveis (demo).
O que o CSS faz sozinho hoje
Muita coisa que exigia JavaScript ou uma biblioteca hoje é comportamento nativo. Isso significa menos código, e principalmente comportamento acessível e performático de graça (teclado, leitores de tela, compositor). Cada linha abre a demonstração no Playground de CSS:
| Antes, com JavaScript | Hoje, nativo | Suporte | Demo |
|---|---|---|---|
| Biblioteca de tooltip ou dropdown (posicionar, fechar ao clicar fora) | popover + anchor positioning | Novo (2026) | abrir |
| Componente que mede o próprio tamanho (ResizeObserver) | Container queries | Amplo | abrir |
| Adicionar classe no pai quando um filho muda | :has() | Amplo | abrir |
| Listener de scroll para cabeçalho fixo | position: sticky | Amplo | abrir |
| Biblioteca de carrossel | Scroll snap | Amplo | abrir |
| Acordeão que fecha os outros | <details name> | Novo (2024) | abrir |
| Textarea que cresce ao digitar | field-sizing: content | Novo (2026) | abrir |
| Gerar tons de uma cor e o tema escuro | color-mix() + light-dark() | Novo (2024) | abrir |
| Calcular atrasos e cores por índice | sibling-index() | Novo (2026) | abrir |
| Animar o ângulo de um gradiente | @property | Novo (2024) | abrir |
| Balancear quebras de título manualmente | text-wrap: balance | Novo (2024) | abrir |
| Efeitos ligados à rolagem | animation-timeline | Limitado | abrir |
A coluna de suporte segue o Baseline do web.dev: amplo = em todos os navegadores principais há mais de dois anos e meio; novo = chegou a todos há menos tempo (o ano é quando ficou completo); limitado = ainda falta algum. Para usar o que é novo com segurança, @supports (propriedade: valor) { … } aplica um bloco só onde o navegador entende, e o resto continua funcionando.
CSS em escala
O CSS é global: qualquer regra pode atingir qualquer elemento, e a cascata decide os conflitos. Toda estratégia de organização é uma forma de controlar esse alcance:
| Abordagem | Como controla o alcance |
|---|---|
BEM (.card__title--destaque) | Convenção de nomes: especificidade baixa e plana, cada classe tem um dono |
| Utilitários (Tailwind, como neste site) | Quase não há cascata: cada classe faz uma coisa e fica no próprio HTML |
| CSS Modules, CSS-in-JS | O build gera nomes únicos por componente |
@layer | Nativo: a ordem das camadas decide conflitos, não a especificidade |
@scope | Nativo: limita onde um estilo vale, inclusive com limite inferior (@scope (.card) to (.content)); em todos os navegadores desde dezembro de 2025 |
| Shadow DOM | Nativo: isolamento total para web components |
Depurando como o navegador vê
- Elements → Computed: o valor final de cada propriedade e, ao expandir, a declaração que venceu a cascata e as que perderam.
- Overlays de flex e grid: o selo
flexougridno painel Elements desenha linhas, faixas e espaços. - Rendering → Paint flashing: pisca em verde o que é repintado. Se a página inteira pisca numa animação, algo está disparando paint ou layout.
- Layers: mostra quais elementos ganharam camada própria e por quê.
- Performance: grave uma interação e procure blocos longos de Recalculate Style e Layout, sinal de layout thrashing.
- Um contorno temporário em tudo mostra qual caixa está estourando a largura:
* { outline: 1px solid rgb(255 0 0 / 0.4); }.
Sintomas e causas
| Sintoma | O que está acontecendo |
|---|---|
z-index alto continua atrás | O elemento está preso num stacking context criado por um ancestral |
Modal fixed rola com a página | Um ancestral com transform, filter ou contain virou o containing block |
| Item flex estoura o container | min-width: auto impede o item de encolher abaixo do conteúdo |
| Margem do filho “vaza” para fora do pai | Margin collapse; crie um BFC (display: flow-root) ou use gap |
| Espaço embaixo de uma imagem | Imagens são inline e se alinham à linha de base do texto |
| Animação travando no celular | Está animando uma propriedade de layout; troque por transform |
100vh corta a tela no celular | A barra do navegador muda a altura da viewport; use dvh |
| Variável CSS “não funciona” e a propriedade volta ao padrão | O valor substituído é inválido para aquela propriedade (inválido no momento do cálculo) |
| Fontes encolhendo em listas aninhadas | em é herdado já convertido em pixels; use rem |
Vídeos
- In The Loop (Jake Archibald, JSConf.Asia 2018, 35 min): como o event loop do navegador intercala JavaScript, estilo, layout e pintura a cada quadro. Explica por que algumas mudanças de estilo “não animam” e onde entra o
requestAnimationFrame(é o vídeo embutido no topo). - Learn CSS Grid the easy way (Kevin Powell): grid na prática, usando o inspetor do navegador para ver as faixas.
Referências
- Inside look at modern web browser, parte 3 (Mariko Kosaka): o pipeline de renderização ilustrado, de parsing a compositor thread.
- Rendering performance (Paul Lewis, web.dev): o pixel pipeline e os três caminhos de custo.
- CSS Cascade 5: a especificação da cascata, das camadas e das etapas de um valor.
- content-visibility (web.dev): como pular a renderização do que está fora da tela.
- How to @scope CSS now that it’s Baseline (Frontend Masters).
- Baseline (web.dev): o que significa cada nível de suporte.
- MDN: CSS: a referência de propriedades, seletores e guias.