The Code Codex
Tema
← Guias

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.

HTML

DOM

árvore de elementos

CSS

CSSOM

árvore de regras

Style

valor final de cada

propriedade de cada nó

Layout

posição e tamanho

de cada caixa

Paint

listas de desenho

por camada

Composite

junta as camadas

na GPU

HTML

DOM

árvore de elementos

CSS

CSSOM

árvore de regras

Style

valor final de cada

propriedade de cada nó

Layout

posição e tamanho

de cada caixa

Paint

listas de desenho

por camada

Composite

junta as camadas

na GPU

  1. 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.
  2. 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.
  3. Layout: com os estilos resolvidos, calcula a geometria: largura, altura e posição de cada caixa. Elementos com display: none nem entram nesta etapa; com visibility: hidden, entram e ocupam espaço.
  4. Paint: gera as instruções de desenho (fundo, borda, texto, sombra) na ordem certa de empilhamento.
  5. 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 afetaExemplosRefaz
Geometriawidth, height, margin, padding, top, font-size, displayLayout → Paint → Composite (o mais caro, e pode afetar a página inteira)
Só a aparênciacolor, background, box-shadow, border-radiusPaint → Composite
Só a camadatransform, 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 escreve
const width = box.offsetWidth
items.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.

body

header

main

nav

a ✓

tem nav acima

a ✓

article

p

a ✗

sobe até body

sem achar nav

body

header

main

nav

a ✓

tem nav acima

a ✓

article

p

a ✗

sobe até body

sem achar 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:

Origem e importância

navegador, usuário, site

!important inverte

Contexto

shadow DOM

Estilo no elemento

atributo style

Camadas

@layer

Especificidade

IDs, classes,

elementos

Ordem

a última vence

Origem e importância

navegador, usuário, site

!important inverte

Contexto

shadow DOM

Estilo no elemento

atributo style

Camadas

@layer

Especificidade

IDs, classes,

elementos

Ordem

a última vence

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 camada utilities vence uma de base mesmo 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:

Declarado

todas as declarações

que se aplicam

Cascateado

a vencedora

Especificado

herança ou valor

inicial, se não houver

Computado

unidades relativas

resolvidas: 2em → 32px

Usado

depende do layout:

50% → 412px

Real

arredondado

para o dispositivo

Declarado

todas as declarações

que se aplicam

Cascateado

a vencedora

Especificado

herança ou valor

inicial, se não houver

Computado

unidades relativas

resolvidas: 2em → 32px

Usado

depende do layout:

50% → 412px

Real

arredondado

para o dispositivo

  • 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 que display de um div não é block “por natureza”: é a folha de estilos do navegador que declara isso, e display: revert volta a ela.
  • Computado: é o que os filhos herdam. Como em vira pixels aqui, um filho herda os pixels do pai, não o em. É o motivo do efeito cascata de font-size: 0.8em em listas aninhadas (cada nível encolhe de novo), e por que rem (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 como width, é 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: 20px usado em color: 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 como unset (herda ou volta ao valor inicial), em vez de cair na declaração anterior.
  • Variáveis herdam como propriedades de texto. Redefinir --space dentro de .card muda tudo lá dentro, e é assim que temas funcionam: um [data-theme="dark"] no html redefine 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.

display do pai

block / inline

fluxo normal

flex

algoritmo do flexbox

grid

algoritmo do grid

table

algoritmo de tabela

Formatação em bloco

caixas empilhadas

na vertical

Formatação inline

texto e caixas inline

em line boxes

display do pai

block / inline

fluxo normal

flex

algoritmo do flexbox

grid

algoritmo do grid

table

algoritmo de tabela

Formatação em bloco

caixas empilhadas

na vertical

Formatação inline

texto e caixas inline

em line boxes

  • 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 width e margens verticais, e imagens se alinham à linha de base do texto, o que deixa aquele espaço misterioso embaixo de uma <img> (resolve com display: block).
  • Flex e grid: dentro deles, as regras do fluxo normal não valem. Não há margin collapse, float é ignorado nos filhos e vertical-align nã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:

ElementoContaining block
No fluxo normalO ancestral bloco mais próximo
position: absoluteO ancestral posicionado mais próximo (não static)
position: fixedA 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-content fica 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:

1 · Tamanho base

flex-basis, ou o

tamanho do conteúdo

2 · Limites

aplica min-width

e max-width

3 · Espaço livre

container menos

a soma das bases

Sobra espaço:

divide pelo

flex-grow

Falta espaço:

tira pelo flex-shrink

× tamanho base

1 · Tamanho base

flex-basis, ou o

tamanho do conteúdo

2 · Limites

aplica min-width

e max-width

3 · Espaço livre

container menos

a soma das bases

Sobra espaço:

divide pelo

flex-grow

Falta espaço:

tira pelo flex-shrink

× tamanho base

Três consequências que parecem bugs, mas são o algoritmo:

  • flex: 1 iguala os itens; flex: auto não. flex: 1 significa base 0%: todo o espaço é “livre” e é dividido por igual. flex: auto usa 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-width padrão deles é auto (= min-content) no passo 2. Uma URL longa ou um bloco de código estoura o layout mesmo com flex: 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:

↑ frente

7 · Posicionados com z-index positivo

6 · Posicionados com z-index auto ou 0

5 · Conteúdo inline: texto

4 · Floats

3 · Blocos no fluxo normal

2 · Filhos com z-index negativo

1 · Fundo e borda do próprio contexto

↓ fundo

↑ frente

7 · Posicionados com z-index positivo

6 · Posicionados com z-index auto ou 0

5 · Conteúdo inline: texto

4 · Floats

3 · Blocos no fluxo normal

2 · Filhos com z-index negativo

1 · Fundo e borda do próprio contexto

↓ fundo

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:

  • position diferente de static com z-index diferente de auto (e fixed/sticky sempre);
  • opacity menor que 1, transform, filter, backdrop-filter ou mix-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 com contain-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: 100px para 200px gera todos os valores do meio.
  • Discretos: valores sem meio-termo, como display ou visibility. Eles trocam de uma vez. Com transition-behavior: allow-discrete, a troca acontece no momento certo da transição, o que permite animar a saída de um elemento antes do display: none.
  • Estado inicial: um elemento que acabou de aparecer não tem um “antes” para interpolar. @starting-style declara 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ívelFerramentaO que reage
PáginaMedia queries (@media (width >= 768px))A janela
ComponenteContainer queries (@container)O espaço do container do componente (demo)
Conteúdoclamp(), minmax(), auto-fill, flex-wrapO espaço disponível, continuamente, sem breakpoints (demo)
Usuárioprefers-color-scheme, prefers-reduced-motion, prefers-contrastAs 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 JavaScriptHoje, nativoSuporteDemo
Biblioteca de tooltip ou dropdown (posicionar, fechar ao clicar fora)popover + anchor positioningNovo (2026)abrir
Componente que mede o próprio tamanho (ResizeObserver)Container queriesAmploabrir
Adicionar classe no pai quando um filho muda:has()Amploabrir
Listener de scroll para cabeçalho fixoposition: stickyAmploabrir
Biblioteca de carrosselScroll snapAmploabrir
Acordeão que fecha os outros<details name>Novo (2024)abrir
Textarea que cresce ao digitarfield-sizing: contentNovo (2026)abrir
Gerar tons de uma cor e o tema escurocolor-mix() + light-dark()Novo (2024)abrir
Calcular atrasos e cores por índicesibling-index()Novo (2026)abrir
Animar o ângulo de um gradiente@propertyNovo (2024)abrir
Balancear quebras de título manualmentetext-wrap: balanceNovo (2024)abrir
Efeitos ligados à rolagemanimation-timelineLimitadoabrir

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:

AbordagemComo 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-JSO build gera nomes únicos por componente
@layerNativo: a ordem das camadas decide conflitos, não a especificidade
@scopeNativo: limita onde um estilo vale, inclusive com limite inferior (@scope (.card) to (.content)); em todos os navegadores desde dezembro de 2025
Shadow DOMNativo: 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 flex ou grid no 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

SintomaO que está acontecendo
z-index alto continua atrásO elemento está preso num stacking context criado por um ancestral
Modal fixed rola com a páginaUm ancestral com transform, filter ou contain virou o containing block
Item flex estoura o containermin-width: auto impede o item de encolher abaixo do conteúdo
Margem do filho “vaza” para fora do paiMargin collapse; crie um BFC (display: flow-root) ou use gap
Espaço embaixo de uma imagemImagens são inline e se alinham à linha de base do texto
Animação travando no celularEstá animando uma propriedade de layout; troque por transform
100vh corta a tela no celularA barra do navegador muda a altura da viewport; use dvh
Variável CSS “não funciona” e a propriedade volta ao padrãoO valor substituído é inválido para aquela propriedade (inválido no momento do cálculo)
Fontes encolhendo em listas aninhadasem é 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

1/16 · Por que entender o CSS por dentro