CSS por dentro: cascata, layout e renderização
Uma imersão no CSS moderno: cascata, camadas, caixa, flexbox, grid, container queries, tipografia, cores, desempenho e acessibilidade.

Toda página da web tem duas camadas que o visitante nunca vê separadas. O HTML diz o que existe na página: um título, um formulário, uma tabela de preços. O CSS diz como isso aparece: em que cor, em que tamanho, em quantas colunas, com que espaço entre as coisas e como tudo se rearruma quando a tela encolhe do monitor para o celular.
O CSS tem fama de linguagem fácil de começar e difícil de dominar. Qualquer pessoa muda a cor de um texto em cinco minutos; poucas sabem explicar por que uma regra "não pega", por que um elemento escapa da caixa ou por que uma animação simples deixa a página travando. Quase sempre a resposta está em três ideias que o dia a dia esconde: a cascata, o modelo de caixa e o caminho de renderização do navegador.
Este artigo é uma imersão nessas ideias e no que foi construído em cima delas: os recursos de layout modernos, a responsividade, as animações, o desempenho, a acessibilidade, a depuração e a organização de estilos em projetos grandes. Há exemplos passo a passo, os erros mais comuns com o porquê de cada um e uma seção de perguntas frequentes. No fim, resumimos os princípios que seguimos na Gothor.
O que é CSS
CSS (Cascading Style Sheets, ou folhas de estilo em cascata) é uma linguagem declarativa: em vez de dar ordens passo a passo, você descreve o resultado desejado e o navegador decide como chegar lá. Uma folha de estilo é uma lista de regras, e cada regra tem duas partes:
/* seletor: a quem a regra se aplica */
.cartao-de-preco {
/* declarações: propriedade e valor */
padding: 1.5rem;
border-radius: 12px;
background: #f4f4ee;
}
O seletor escolhe os elementos; as declarações dizem o que muda neles. Parece simples, e é. A complexidade nasce de um fato: num documento real, várias regras disputam o mesmo elemento, vindas do navegador, de bibliotecas, de componentes e do seu próprio código. Decidir qual valor vence é o trabalho da cascata, e entender esse trabalho é o que separa quem escreve CSS de quem briga com ele.
A anatomia de uma folha de estilo
Além das regras comuns, uma folha de estilo tem as regras arroba (at-rules), que começam com @ e mudam o contexto das regras que estão dentro delas ou trazem algo de fora:
| Regra arroba | Para que serve |
|---|---|
@import | traz outra folha de estilo |
@media | aplica as regras só sob uma condição da tela ou do usuário |
@container | aplica as regras conforme o tamanho do container do elemento |
@supports | aplica as regras só se o navegador entender uma propriedade |
@layer | organiza as regras em camadas de prioridade |
@font-face | declara uma fonte que a página vai baixar |
@keyframes | descreve os quadros de uma animação |
@property | registra uma variável com tipo, valor inicial e herança |
Os seletores também têm família. Os seletores de tipo (p, button) pegam todos os elementos daquela marca; os de classe (.preco) pegam quem tem a classe; os de ID (#topo), o único elemento com aquele identificador; os de atributo ([type="email"], [data-estado="ativo"]), quem tem o atributo com aquele valor. As pseudoclasses (:hover, :focus-visible, :first-child) descrevem um estado ou uma posição, e os pseudoelementos (::before, ::placeholder, ::selection) criam ou alcançam partes que não existem como elemento no HTML. Os combinadores ligam seletores: o espaço quer dizer "descendente", > quer dizer "filho direto", + "o irmão logo depois" e ~ "qualquer irmão depois".
Uma linguagem que perdoa erros
Uma característica de projeto do CSS explica muito do comportamento dele: o navegador ignora o que não entende e segue em frente. Uma declaração com propriedade desconhecida ou valor inválido é descartada sozinha; um seletor inválido descarta a regra inteira; o resto da folha continua valendo. É o oposto de uma linguagem de programação, em que um erro de sintaxe para tudo.
Essa tolerância foi o que permitiu à linguagem evoluir por trinta anos sem quebrar sites antigos: um navegador velho simplesmente pula o recurso novo. Daí nasce a técnica do valor reserva, em que se escreve primeiro a versão que todos entendem e depois a moderna:
.secao {
height: 100vh; /* navegadores antigos ficam com esta */
height: 100dvh; /* os modernos entendem e ficam com a última */
}
@supports (container-type: inline-size) {
/* só entra aqui quem tem container queries */
}
O lado ruim é que o CSS nunca avisa: um colr: red não gera erro nenhum, a regra apenas não acontece.
Uma história curta
Nos primeiros anos da web, a aparência vivia dentro do HTML, em atributos e marcas como font e center, e o layout era feito com tabelas aninhadas e imagens transparentes usadas como espaçadores. Mudar a cor de um site inteiro significava editar cada página.
Em 1994, Håkon Wium Lie, então no CERN, propôs as folhas de estilo em cascata, e Bert Bos juntou-se ao trabalho. O W3C publicou o CSS1 em 1996 e o CSS2 em 1998, com posicionamento e tipos de mídia. Os anos seguintes foram de suporte desigual entre navegadores: cada um interpretava partes da especificação de um jeito, e uma geração inteira de desenvolvedores aprendeu "hacks" para cada versão do Internet Explorer. O layout de página dependia de float, um recurso pensado para texto contornar imagens, e não para montar colunas.
Dois marcos ajudaram a virar esse jogo. Em 2003, o CSS Zen Garden, de Dave Shea, publicou um único documento HTML com dezenas de folhas de estilo enviadas por designers do mundo todo, cada uma transformando a mesma página num site completamente diferente. Foi a prova visual de que separar conteúdo e apresentação funcionava. Em 2005, o teste Acid2 desenhava um rosto sorridente só quando o navegador seguia a especificação, e os fabricantes correram atrás.
Enquanto isso, a comunidade inventava ferramentas para o que a linguagem não tinha. Os pré-processadores, como o Sass (2006) e o Less (2009), trouxeram variáveis, aninhamento e funções, tudo compilado para CSS comum antes de ir ao navegador, e frameworks como o Bootstrap (2011) empacotaram grades e componentes prontos.
A partir dos anos 2000, o CSS deixou de ser uma especificação única e virou um conjunto de módulos que evoluem separadamente: seletores, cores, fontes, layout, animações. É por isso que não existe um "CSS4" oficial: cada módulo tem o seu nível. Em 2010, o artigo de Ethan Marcotte sobre design responsivo deu nome à prática de adaptar o layout ao tamanho da tela com media queries. Na década de 2010 chegaram o flexbox e, em março de 2017, o grid nos principais navegadores, os primeiros sistemas de layout pensados para isso. As variáveis (propriedades personalizadas) vieram na mesma época, e o Tailwind, com a proposta de classes utilitárias, apareceu no fim de 2017.
Os anos 2020 trouxeram uma leva de recursos que a comunidade pedia havia uma década: camadas de cascata (@layer) em 2022, o seletor **:has()** entre 2022 e 2023, container queries com suporte em todos os principais navegadores no começo de 2023, aninhamento nativo e subgrid no mesmo ano, cores em espaços como oklch e misturas com color-mix(). Iniciativas como o Interop, em que os fabricantes de navegadores combinam corrigir as mesmas diferenças a cada ano, e o selo Baseline, que indica quando um recurso já funciona em todos os navegadores principais, tornaram muito mais previsível saber o que dá para usar. Boa parte do que os pré-processadores ofereciam (variáveis, aninhamento, funções de cor) hoje é nativa.
Como o navegador transforma CSS em pixels
Antes de falar de cascata e layout, vale entender o caminho que o navegador percorre a cada página, porque ele explica tanto os resultados quanto o desempenho.

- DOM e CSSOM. O HTML vira uma árvore de elementos, o DOM. As folhas de estilo viram outra estrutura, o CSSOM. Enquanto o CSS da página não chega, o navegador não pinta nada, para não mostrar a página sem estilo e depois "pular". Por isso o CSS é um recurso que bloqueia a renderização.
- Estilo. Para cada elemento, o navegador descobre quais regras se aplicam e resolve a cascata, chegando ao valor calculado de cada propriedade.
- Layout. Com os estilos resolvidos, o navegador calcula a geometria: a largura, a altura e a posição de cada caixa. Uma mudança de tamanho num elemento pode mover todos os que vêm depois.
- Pintura. As caixas viram listas de comandos de desenho: fundos, bordas, textos, sombras, imagens.
- Composição. A página é dividida em camadas, que a placa de vídeo junta na tela. Mover ou tornar transparente uma camada já pintada é barato.
A regra prática que sai daí: mudar uma propriedade de geometria (largura, altura, margem, posição no fluxo, tamanho da fonte) refaz layout, pintura e composição; mudar uma propriedade de aparência (cor, fundo, sombra) refaz pintura e composição; mudar **transform** e **opacity** costuma refazer só a composição. É por isso que animações suaves usam essas duas propriedades.
Fontes, @import e o que atrasa a primeira pintura
Como o CSS bloqueia a renderização, tudo o que atrasa a chegada dele atrasa a página. Dois detalhes costumam passar despercebidos.
O primeiro é o **@import dentro de uma folha de estilo**. O navegador só descobre o arquivo importado depois de baixar e ler o primeiro, e só então começa o segundo download. Em cadeia, isso vira uma fila de esperas. Uma tag <link> no HTML é descoberta logo no início da leitura da página e baixada em paralelo com o resto.
O segundo são as fontes. A declaração @font-face diz onde está a fonte, mas o arquivo só é pedido quando algum texto precisa dela. Enquanto ele não chega, o navegador escolhe entre esconder o texto (o efeito chamado FOIT, flash of invisible text) ou mostrar uma fonte do sistema e trocar depois (o FOUT, flash of unstyled text). O descritor font-display, dentro do @font-face, decide: com swap, o texto aparece na hora com a fonte reserva e é trocado quando a fonte chega. A seção sobre tipografia, mais adiante, mostra como escolher entre essas estratégias e como reduzir o salto da troca.
A cascata: quem vence quando várias regras disputam o mesmo elemento
Quando duas declarações definem a mesma propriedade para o mesmo elemento, o navegador aplica uma sequência de critérios, em ordem. O primeiro critério que distingue as duas decide; os seguintes nem são consultados.

Origem e importância
As regras vêm de três origens: as do navegador (o estilo padrão de um botão, por exemplo), as do usuário (preferências como tamanho mínimo de fonte) e as do autor, que é quem escreveu o site. Normalmente o autor vence. A marca !important inverte a ordem para quem a usa, e por isso deve ser exceção: um !important só é vencido por outro !important mais forte, e o código vira uma escalada.
A ordem completa, da prioridade mais baixa para a mais alta, mostra a lógica da inversão:
- Declarações normais do navegador.
- Declarações normais do usuário.
- Declarações normais do autor.
- Animações em andamento (
@keyframes). - Declarações
!importantdo autor. - Declarações
!importantdo usuário. - Declarações
!importantdo navegador. - Transições em andamento.
A inversão existe por um motivo de acessibilidade: o usuário que precisa de letras grandes ou de alto contraste marca as próprias preferências como importantes, e nenhum site consegue passar por cima delas.
Camadas
As camadas de cascata (@layer) permitem dizer explicitamente que um bloco de estilos tem prioridade sobre outro, independentemente da especificidade. Uma regra numa camada declarada depois vence uma regra de camada anterior, mesmo que o seletor dela seja mais fraco:
@layer base, componentes, utilitarios;
@layer componentes {
.botao.primario { background: #14170f; }
}
@layer utilitarios {
.bg-volt { background: #97d700; } /* vence, mesmo com seletor mais fraco */
}
É o mecanismo que bibliotecas e frameworks usam para que as classes utilitárias sempre vençam os estilos de componente, sem !important. Estilos fora de qualquer camada vencem todos os que estão em camadas.
Com !important, a ordem das camadas se inverte, pela mesma lógica das origens: uma declaração importante numa camada anterior vence uma importante numa camada posterior, e as importantes de dentro de camadas vencem as importantes de fora. Na prática, isso permite que a camada de base proteja um valor essencial (o contorno de foco, por exemplo) contra qualquer camada que venha depois.
Especificidade
Dentro da mesma origem e camada, vence o seletor mais específico. A especificidade é contada em três colunas, comparadas da esquerda para a direita:
| Seletor | IDs | Classes, atributos e pseudoclasses | Elementos e pseudoelementos |
|---|---|---|---|
p | 0 | 0 | 1 |
.preco | 0 | 1 | 0 |
.cartao .preco:hover | 0 | 3 | 0 |
#oferta .preco | 1 | 1 | 0 |
:where(.cartao) .preco | 0 | 1 | 0 |
Um único ID vence qualquer quantidade de classes, porque a comparação é coluna a coluna, e não uma soma. Os seletores :is() e :not() assumem a especificidade do argumento mais forte; :where() tem especificidade zero, o que o torna ótimo para estilos-base fáceis de sobrescrever. Estilos escritos no atributo style do HTML ficam acima de todos os seletores. O seletor universal * e os combinadores não contam nada.
Ordem de aparição
Se tudo empatar, vence a declaração que aparece por último no código. É por isso que a ordem de importação das folhas de estilo importa, e por isso que "mudar de lugar" às vezes conserta uma regra: não é sorte, é o último critério da cascata.
Proximidade de escopo: o @scope
A regra @scope limita um bloco de estilos a um pedaço da árvore, com um começo e, se for preciso, um fim:
@scope (.cartao) to (.cartao-conteudo) {
:scope { padding: 1rem; } /* o próprio .cartao */
img { border-radius: 8px; } /* as imagens do cartão, fora do conteúdo */
}
A raiz (.cartao) entra no escopo, e o limite (.cartao-conteudo) fica de fora, o que a MDN chama de escopo em formato de rosquinha: o estilo vale entre um e outro e não vaza para o conteúdo que o componente recebe de quem o usa. Os seletores escritos dentro do bloco não ganham especificidade extra, como se começassem com :where(:scope); já o :scope explícito conta como uma classe.
O @scope também acrescenta um critério à cascata, a proximidade de escopo, consultado depois da especificidade e antes da ordem de aparição. Quando duas regras com escopo empatam no resto, vence a que tem a raiz mais próxima do elemento na árvore:
@scope (.tema-claro) { p { color: #1d2016; } }
@scope (.tema-escuro) { p { color: #e8e8e0; } }
Num cartão de tema claro dentro de um painel de tema escuro, os parágrafos do cartão ficam com a cor do tema claro, cuja raiz está mais perto, qualquer que seja a ordem das regras no arquivo. Sem o @scope, venceria a regra escrita por último, e os parágrafos do cartão poderiam ficar com a cor do painel. O recurso entrou no Baseline em março de 2026, e vale conferir se o público da página usa navegadores atualizados antes de depender dele.
Um conflito resolvido passo a passo
Vale acompanhar um caso real. Um botão de comprar tem o HTML <button id="comprar" class="botao primario"> e três regras disputam a cor de fundo:
@layer componentes {
#comprar { background: navy; } /* A */
}
.botao.primario { background: #97d700; } /* B, fora de camada */
button { background: gray !important; } /* C, no fim do arquivo */
O navegador percorre os critérios em ordem:
- Origem e importância. As três são do autor, mas C é
!important. Importantes do autor vencem normais do autor, então C ganha aqui mesmo, e o botão fica cinza. Especificidade e camadas nem chegam a ser consultadas. - Se tirarmos o
!importantde C, as três empatam na origem. Camadas: B e C estão fora de camada e vencem A, que está dentro de uma. O ID de A, que parecia imbatível, não ajuda. - Entre B e C, especificidade: B tem duas classes (0, 2, 0), C tem um elemento (0, 0, 1). B vence, e o botão fica verde.
O exercício mostra por que "aumentar a especificidade" é muitas vezes a ferramenta errada: um critério anterior pode estar decidindo, e nenhum seletor mais forte vai mudar o resultado.
Herança e valores especiais
Algumas propriedades são herdadas: se o elemento não define, recebe o valor do pai. Cor e fontes são herdadas; margens, bordas e fundos não. Quatro palavras-chave controlam isso em qualquer propriedade: inherit (pega do pai), initial (volta ao valor inicial da especificação), unset (herda se for herdável, senão volta ao inicial) e revert (volta ao estilo do navegador). Com camadas, revert-layer volta ao valor da camada anterior. A palavra currentColor, por sua vez, vale a cor do texto do elemento, e é o jeito mais simples de fazer um ícone SVG ou uma borda acompanharem a cor do texto em todos os estados.
Do valor declarado ao pixel
Entre a declaração escrita e o que aparece na tela, cada valor passa por uma sequência de etapas, e saber em qual delas um valor está explica respostas que parecem estranhas nas ferramentas e no JavaScript. A MDN descreve seis:
- Declarado: cada declaração válida que alcança o elemento. Um parágrafo pode ter três
font-sizedeclarados, vindos do navegador, de um componente e de uma classe. - Em cascata: o vencedor da cascata entre os declarados. Pode não existir, se nenhuma regra tocou a propriedade.
- Especificado: o resultado de preencher a falta. Se houve vencedor, é ele; se não houve, a propriedade herda do pai, quando é herdável, ou recebe o valor inicial.
- Calculado: os valores relativos viram absolutos, desde que isso não dependa do layout. Com fonte de 16 px, um
padding-top: 2emvira 32 px nessa etapa. As porcentagens de largura e de margem, porém, continuam porcentagens, porque dependem do tamanho do bloco que contém o elemento. É o valor calculado que os filhos herdam. - Usado: depois do layout, tudo vira medida concreta, e os 50% de largura viram, por exemplo, 400 px.
- Real: o valor usado depois dos arredondamentos do aparelho. Uma borda de 1,5 px pode ser desenhada com 1 ou 2 px numa tela que só desenha pixels inteiros.
Dois detalhes práticos saem daí. O primeiro: a herança passa o valor calculado, e não a regra escrita. Se um cartão tem fonte de 15 px e line-height: 1.1em, o título dentro dele, com fonte de 30 px, herda a altura de linha já convertida em 16,5 px, e as linhas do título se sobrepõem. Com line-height: 1.1, sem unidade, o que se herda é o fator, e cada elemento multiplica pela própria fonte; por isso a MDN recomenda a forma sem unidade. O segundo: o getComputedStyle() do JavaScript nem sempre devolve o valor calculado. Para algumas propriedades antigas, como width e height, ele devolve o valor usado, em pixels, por compatibilidade com scripts escritos quando a função funcionava assim.
O modelo de caixa
Para o layout, todo elemento é um retângulo. Entender as quatro áreas desse retângulo resolve boa parte dos "por que isso ficou maior do que eu pedi".

- Conteúdo: onde ficam o texto e os filhos.
- Padding: o espaço interno, entre o conteúdo e a borda. Recebe o fundo do elemento.
- Borda: a linha em volta do padding.
- Margem: o espaço externo, que afasta a caixa das vizinhas. É sempre transparente.
box-sizing
No comportamento original, width define só a largura do conteúdo: uma caixa com width: 300px, padding: 20px e borda de 1 px ocupa 342 px. Com box-sizing: border-box, a largura passa a incluir padding e borda, e 300 px são 300 px. Quase todo projeto moderno começa com esta regra:
*, *::before, *::after {
box-sizing: border-box;
}
Margens que colapsam
Um comportamento que surpreende: as margens verticais de blocos vizinhos no fluxo normal não se somam, colapsam, e vale a maior. Um título com margin-bottom: 24px seguido de um parágrafo com margin-top: 16px fica a 24 px dele, e não a 40. O colapso não acontece dentro de containers flex e grid, o que é uma das razões para preferir o gap desses layouts às margens.
Há uma variante mais traiçoeira: a margem do primeiro filho pode atravessar o pai. Se uma seção sem borda nem padding tem um título com margin-top: 32px, a margem do título "vaza" e empurra a seção inteira para baixo, deixando o fundo dela começar depois do espaço, e não antes. Um padding no pai, uma borda ou um display: flow-root, que cria uma região de layout independente, impedem o vazamento.
Fluxo, posicionamento e camadas de empilhamento
Por padrão, os elementos seguem o fluxo normal: os de bloco (div, p, section) empilham um embaixo do outro e ocupam a largura disponível; os de linha (span, a, strong) correm junto com o texto. A propriedade position tira os elementos desse fluxo de formas diferentes: relative desloca sem tirar do lugar, absolute posiciona em relação ao ancestral posicionado mais próximo, fixed em relação à janela e sticky acompanha a rolagem até um limite.
O z-index só compara elementos dentro do mesmo contexto de empilhamento. Propriedades como opacity menor que 1, transform e position com z-index criam um contexto novo, e é por isso que um z-index: 9999 às vezes "não funciona": o elemento está preso num contexto que, como um todo, fica abaixo de outro.

Vale pensar nos contextos como pastas: o z-index ordena os itens dentro de uma pasta, e depois a pasta inteira é ordenada entre as vizinhas pelo z-index dela. Um modal com z-index: 9999 dentro de um cartão com transform e z-index: 1 nunca vai ficar acima de um cabeçalho com z-index: 10, porque quem disputa com o cabeçalho é o cartão. A correção é tirar o modal de dentro do cartão (bibliotecas de componentes fazem isso renderizando modais direto no fim do <body>) ou usar o elemento <dialog> aberto como modal, que o navegador põe numa camada superior (top layer), acima de qualquer z-index.
Posicionamento por âncora
Dicas, menus suspensos e balões precisam ficar presos a outro elemento, como o botão que os abriu, e por anos isso exigiu JavaScript para medir posições a cada rolagem e a cada mudança de tamanho da janela. O posicionamento por âncora, Baseline desde janeiro de 2026, faz isso no CSS: um elemento recebe um nome de âncora, e outro, posicionado, se prende a ele.
.botao-ajuda { anchor-name: --ajuda; }
.dica {
position: fixed;
position-anchor: --ajuda;
position-area: top; /* acima do botão */
position-try-fallbacks: flip-block; /* se não couber em cima, vai para baixo */
}
A propriedade position-area escolhe uma região numa grade imaginária de três por três em volta da âncora, sem nenhuma conta; a função anchor() permite alinhar bordas específicas, como top: anchor(bottom). As alternativas de position-try-fallbacks só entram quando a posição preferida transbordaria o bloco que contém o elemento ou a janela: o navegador testa cada uma na ordem e fica com a primeira que cabe. Somado ao atributo popover do HTML, que leva o balão para a camada superior, o recurso resolve de uma vez o problema da posição e o do z-index, sem biblioteca.
Unidades, valores e variáveis
O CSS moderno tem unidades para cada tipo de necessidade:
| Unidade | Relativa a | Bom para |
|---|---|---|
px | o pixel de referência do CSS | bordas, sombras, detalhes finos |
rem | o tamanho da fonte da raiz | fontes e espaçamentos que respeitam a preferência do usuário |
em | o tamanho da fonte do próprio elemento | espaços que acompanham o texto do componente |
% | o tamanho do elemento pai | larguras fluidas |
vw, vh | a janela | elementos do tamanho da tela |
dvh, svh, lvh | a janela, considerando as barras do navegador no celular | seções de altura total no celular |
ch | a largura do caractere "0" | limitar o comprimento da linha de texto |
fr | a fração do espaço livre num grid | colunas proporcionais |
cqi | o tamanho do container | componentes que se adaptam ao lugar onde estão |
Entre rem e px há uma diferença que importa para a acessibilidade. Quem aumenta o tamanho padrão da fonte nas configurações do navegador muda o valor de 1rem; os valores em px continuam iguais. Um site todo em rem cresce junto, com proporções preservadas; um site com fontes em px ignora a preferência e obriga essa pessoa a usar o zoom da página inteira.
Funções completam o quadro. calc() mistura unidades; min(), max() e clamp() criam valores fluidos com limites. Um título que cresce com a tela, mas nunca fica menor que 1,45 rem nem maior que 1,8 rem, cabe numa linha:
h2 { font-size: clamp(1.45rem, 2.6vw, 1.8rem); }
O padrão serve também para larguras. width: min(1240px, 100%) quer dizer "1.240 px, mas nunca mais que o espaço disponível", e substitui a dupla width com max-width numa linha.
Variáveis: propriedades personalizadas
As propriedades personalizadas, chamadas de variáveis CSS, guardam valores reutilizáveis e, diferente das variáveis de pré-processadores, existem durante a execução: herdam pela árvore e podem ser trocadas por tema, por componente ou por JavaScript:
:root { --marca: #97d700; --texto-marca: #3e6b00; }
.dark { --texto-marca: #97d700; }
.link { color: var(--texto-marca, green); } /* o segundo valor é o reserva */
Um detalhe que confunde: o navegador não confere o valor de uma variável quando ela é definida, só quando ela é usada. Se --espaco vale azul e alguém escreve padding: var(--espaco), a declaração não é descartada como um erro comum; ela vira inválida no momento do cálculo, e a propriedade volta ao valor herdado ou inicial, como se tivesse recebido unset. O valor reserva do var() não ajuda nesse caso, porque ele só entra quando a variável não existe.
A regra @property resolve isso e acrescenta um poder novo: registrar a variável com um tipo. Uma variável registrada como cor ou como ângulo pode ser animada e transicionada, coisa impossível com variáveis comuns, que o navegador trata como texto:
@property --giro {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.borda-animada {
background: conic-gradient(from var(--giro), #97d700, transparent 40%);
transition: --giro 1s;
}
.borda-animada:hover { --giro: 180deg; }
Nas cores, espaços como oklch descrevem a cor por luminosidade, croma e matiz de um jeito mais próximo da percepção humana, o que facilita gerar escalas uniformes. E color-mix() mistura duas cores direto no CSS, por exemplo para gerar a versão translúcida da cor da marca.
Propriedades lógicas: início e fim em vez de esquerda e direita
As propriedades clássicas são físicas: margin-left, padding-top, width. As lógicas descrevem o mesmo em relação ao fluxo do texto. A dimensão em linha (inline) é a direção em que o texto corre dentro de uma linha; a dimensão de bloco é a direção em que os blocos se empilham. Num texto em português, horizontal e da esquerda para a direita, margin-inline-start é a margem esquerda e block-size é a altura. Num texto em árabe, que corre da direita para a esquerda, o mesmo margin-inline-start passa a ser a margem direita, sem nenhuma regra extra; num modo de escrita vertical, inline-size vira a altura.
.aviso {
padding-inline: 1rem; /* esquerda e direita, no português */
margin-block: 1.5rem; /* em cima e embaixo */
border-inline-start: 4px solid var(--marca); /* a barra do lado em que o texto começa */
max-inline-size: 65ch;
}
Mesmo num site só em português, as lógicas ajudam: margin-inline: auto centraliza um bloco, padding-block e padding-inline dizem em duas declarações o que antes pedia quatro, e inset: 0, o atalho de top, right, bottom e left, estica um elemento posicionado até as bordas do bloco que o contém.
Layout moderno: flexbox e grid
Os dois sistemas de layout modernos resolvem problemas diferentes, e a pergunta que decide é simples: o layout é em uma dimensão ou em duas?

Flexbox: uma dimensão, de dentro para fora
O flexbox distribui itens ao longo de um eixo, uma linha ou uma coluna. O tamanho de cada item parte do conteúdo dele e é ajustado pelas regras de crescer e encolher. É ideal para barras de navegação, grupos de botões, um ícone ao lado de um texto, cartões que se alinham numa fileira:
.barra {
display: flex;
align-items: center; /* alinhamento no eixo cruzado */
justify-content: space-between; /* distribuição no eixo principal */
gap: 1rem;
}
.barra .busca { flex: 1; } /* ocupa o espaço que sobrar */
As três propriedades dos itens dizem como dividir o espaço: flex-grow (quanto cresce se sobrar espaço), flex-shrink (quanto encolhe se faltar) e flex-basis (o tamanho de partida). O atalho flex: 1 significa "parta de zero e divida o espaço igualmente".
Passo a passo: como o flexbox divide o espaço
O algoritmo parece mágico até ser feito à mão uma vez. Ele tem três passos: medir o tamanho de partida de cada item, calcular o espaço que sobra ou falta e distribuir esse espaço conforme os fatores.

Quando sobra espaço. Um container de 600 px tem três itens com flex-basis de 200, 100 e 100 px. Somados, ocupam 400 px; sobram 200. Os fatores de crescimento são 1, 1 e 2, que somam 4. Cada unidade vale 200 ÷ 4 = 50 px. Os itens ficam com 200 + 50 = 250, 100 + 50 = 150 e 100 + 100 = 200 px. Note que o segundo e o terceiro partiram do mesmo tamanho e terminaram diferentes, e que o primeiro, com o mesmo fator do segundo, continua maior: o crescimento é somado ao tamanho de partida, e não repartido do zero.
Quando falta espaço. Agora o container tem 240 px e dois itens com flex-basis de 200 e 100 px, os dois com flex-shrink: 1. Faltam 60 px. O encolhimento não é dividido igualmente: cada fator é multiplicado pelo tamanho de partida, para que os itens grandes cedam mais que os pequenos. Os pesos são 200 × 1 e 100 × 1, que somam 300. O primeiro perde 60 × 200 ÷ 300 = 40 px e fica com 160; o segundo perde 20 e fica com 80. Ambos encolheram 20% do tamanho original, o que mantém a proporção entre eles.
**Por que flex: 1 divide igualmente.** O atalho flex: 1 vale flex: 1 1 0%: o tamanho de partida é zero, e todo o espaço do container vira "sobra" a ser dividida pelos fatores. Já flex: auto (1 1 auto) parte do tamanho do conteúdo: numa barra de abas, o primeiro deixa as abas iguais e o segundo as deixa proporcionais aos rótulos.
O item que não encolhe
Um dos defeitos mais comuns do flexbox nasce de uma regra pouco conhecida: o tamanho mínimo de um item flex é, por padrão, o tamanho mínimo do conteúdo dele (min-width: auto). Um item com um texto longo sem espaços, como um endereço de e-mail ou um código de barras, ou com uma tabela larga dentro, se recusa a encolher abaixo desse conteúdo, e empurra o layout para fora da tela.
.linha { display: flex; }
.linha .texto { flex: 1; min-width: 0; } /* agora pode encolher, e o texto quebra ou corta */
.linha .texto p { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
O min-width: 0 libera o encolhimento, e o resto decide o que fazer com o texto que não cabe: cortar com reticências ou quebrar com overflow-wrap: anywhere. No grid, o mesmo problema aparece com colunas 1fr, como veremos a seguir.
Grid: duas dimensões, de fora para dentro
O grid define linhas e colunas no container e posiciona os itens nas células. O tamanho vem da estrutura, e não do conteúdo. É a escolha natural para o layout da página, painéis, galerias e formulários com rótulo e campo alinhados:
.painel {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: 1.5rem;
}
Essa linha cria quantas colunas couberem, cada uma com pelo menos 16 rem, dividindo o que sobra, sem nenhuma media query. Com grid-template-areas, dá até para desenhar o layout com nomes, e com subgrid um item aninhado pode alinhar os próprios filhos às linhas do grid de fora, o que resolve o velho problema de cartões vizinhos com títulos e botões desalinhados.
Grid na prática: um painel passo a passo
Para ver as peças juntas, considere o layout de um painel administrativo: cabeçalho no topo, menu lateral, conteúdo e rodapé. Com áreas nomeadas, o CSS fica parecido com um desenho:
.app {
display: grid;
grid-template-columns: 16rem minmax(0, 1fr);
grid-template-rows: auto 1fr auto;
grid-template-areas:
"topo topo"
"menu conteudo"
"menu rodape";
min-height: 100dvh;
}
.app > header { grid-area: topo; }
.app > nav { grid-area: menu; }
.app > main { grid-area: conteudo; }
.app > footer { grid-area: rodape; }
@media (max-width: 48rem) {
.app {
grid-template-columns: minmax(0, 1fr);
grid-template-areas: "topo" "conteudo" "rodape";
}
.app > nav { display: none; } /* no celular, o menu vira uma gaveta */
}
Três detalhes merecem atenção. O primeiro é o minmax(0, 1fr) no lugar de 1fr: uma coluna 1fr tem como mínimo o conteúdo dela, e uma tabela larga dentro do conteúdo alarga a coluna e cria rolagem horizontal na página inteira. Com mínimo zero, a coluna respeita o espaço e a tabela rola dentro do próprio container. O segundo é a linha do meio com 1fr, que empurra o rodapé para o fim da tela mesmo quando o conteúdo é curto. O terceiro é que, no celular, basta redesenhar as áreas: o HTML continua o mesmo.
Outras ferramentas do grid aparecem cedo em projetos reais:
- **
auto-filleauto-fit.** Os dois criam quantas colunas couberem. A diferença aparece quando há poucos itens:auto-fillmantém as colunas vazias, e os itens ficam do tamanho mínimo;auto-fitrecolhe as vazias, e os itens esticam para ocupar a linha. - Posicionamento por linhas.
grid-column: 1 / -1faz um item ocupar todas as colunas, do primeiro ao último traço, o que é útil para um título de seção dentro de uma lista de cartões.
Quando usar cada um
| Situação | Melhor escolha |
|---|---|
| Itens numa fileira, com tamanhos que dependem do conteúdo | flexbox |
| Centralizar um elemento | os dois resolvem: place-items: center no grid é o mais curto |
| Estrutura de página com cabeçalho, lateral e conteúdo | grid |
| Cartões em colunas que se ajustam à largura | grid com auto-fill e minmax() |
| Formulário com rótulos e campos alinhados | grid |
| Componente interno de um cartão (ícone, título, ação) | flexbox |
| Etiquetas que quebram em várias linhas, cada uma do seu tamanho | flexbox com flex-wrap |
Na prática, os dois convivem: grid para a estrutura, flexbox dentro das peças.
Responsividade: media queries e container queries
Uma página responsiva se adapta ao espaço disponível. Durante quinze anos, a ferramenta principal foi a media query, que pergunta sobre a janela ou o dispositivo:
.produtos { display: grid; gap: 1rem; }
@media (min-width: 48rem) {
.produtos { grid-template-columns: repeat(3, 1fr); }
}
A abordagem mais usada é a mobile first: o estilo base serve para telas pequenas e as media queries acrescentam colunas e espaço à medida que a tela cresce. Além da largura, media queries também consultam preferências do usuário, como tema escuro (prefers-color-scheme) e redução de movimento (prefers-reduced-motion).
Nada disso funciona no celular sem uma linha no HTML: a meta tag viewport com width=device-width. Sem ela, o navegador do celular finge ter uma tela de cerca de 980 px, desenha a página nesse tamanho e encolhe tudo, e as media queries para telas pequenas nunca disparam.
O limite das media queries aparece com componentes. Um cartão de produto pode estar na área principal, larga, ou numa coluna lateral estreita na mesma tela. A janela é a mesma; o espaço do cartão, não. As container queries resolvem isso perguntando sobre o tamanho do container em que o componente está:

.lista { container-type: inline-size; }
.cartao { display: grid; gap: 0.75rem; }
@container (min-width: 32rem) {
.cartao { grid-template-columns: 8rem 1fr; } /* imagem ao lado do texto */
}
O componente passa a ser realmente reutilizável: ele se ajusta a onde foi colocado, sem saber nada da página. As unidades cqi e cqw completam o recurso, permitindo tamanhos proporcionais ao container.
Seletores modernos que mudaram o jogo
Alguns seletores recentes eliminam muito JavaScript e muitas classes auxiliares:
- **
:has()**, o "seletor de pai":.campo:has(input:invalid)estiliza o campo inteiro quando o input dentro dele é inválido;.cartao:has(img)muda o layout só dos cartões que têm imagem. - **
:is()e:where()** agrupam seletores sem repetição, com especificidade do mais forte ou zero, respectivamente. - **
:focus-visible** mostra o contorno de foco quando a pessoa navega pelo teclado, e não no clique do mouse, o que acabou com a desculpa para esconder o foco. - **
:user-invalid** marca o campo inválido só depois que a pessoa interagiu com ele. Com:invalid, um formulário recém-aberto já aparece todo em vermelho, porque os campos obrigatórios começam vazios. - Aninhamento nativo permite escrever regras dentro de regras, como nos pré-processadores, direto no navegador.
.cartao {
padding: 1rem;
&:hover { border-color: var(--marca); }
&:has(.selo-oferta) { outline: 2px solid var(--marca); }
}
O :has() muda o jeito de pensar: antes dele, um elemento nunca podia ser estilizado conforme os filhos, e mostrar os campos de "outro endereço de entrega" quando a caixa é marcada exigia JavaScript. Agora basta o CSS:
.entrega:has(#outro-endereco:checked) .campos-endereco { display: grid; }
.tabela:has(tbody:empty) + .aviso-vazio { display: block; }
Tipografia: o texto é a maior parte da página
Num sistema de gestão, num blog ou numa loja, a maior parte do que a pessoa vê é texto, e o CSS tem mais controle sobre ele do que a maioria dos projetos usa.
O carregamento das fontes, valor por valor
O descritor font-display, citado na seção sobre o caminho de renderização, divide a espera pela fonte em três períodos: o de bloqueio, em que o texto fica invisível; o de troca, em que ele aparece com uma fonte reserva e é trocado se a fonte chegar; e o de falha, em que o navegador desiste e fica com a reserva. Cada valor combina os períodos de um jeito, e o padrão, auto, deixa a escolha com o navegador:
| Valor | Bloqueio | Troca | Bom para |
|---|---|---|---|
block | curto | sem fim | ícones desenhados como fonte, que não fazem sentido com outra letra |
swap | quase nenhum | sem fim | títulos e marcas, que precisam da fonte certa mesmo que ela chegue tarde |
fallback | quase nenhum | curto | texto corrido: se a fonte demorar, a página fica com a reserva |
optional | quase nenhum | nenhum | páginas em que a estabilidade vale mais: a fonte só é usada se já estiver disponível |
A especificação não fixa a duração de "curto" e "quase nenhum"; cada navegador escolhe a sua. O que importa é a intenção: swap prioriza a fonte da marca, optional prioriza que nada se mexa.
Reduzir o salto da troca
Com swap, o texto aparece cedo, mas a troca de fonte costuma mudar a largura das letras, e o parágrafo inteiro se rearruma, o que entra na conta da mudança de layout acumulada. O descritor **size-adjust**, no @font-face, multiplica a escala de uma fonte para que ela ocupe um espaço parecido com o de outra. Aplicado à fonte reserva, ele a aproxima das proporções da fonte da marca e encolhe o salto. Ele funciona em todos os principais navegadores desde setembro de 2023.
@font-face {
font-family: "Reserva Ajustada";
src: local("Arial");
size-adjust: 104%;
}
body { font-family: "Marca", "Reserva Ajustada", sans-serif; }
O percentual certo depende do par de fontes e se encontra comparando as duas na tela, ou com ferramentas de build que calculam o valor a partir das métricas dos arquivos de fonte.
Linhas, quebras e hifenização
- Altura de linha sem unidade.
line-height: 1.5é herdado como fator, como visto na seção sobre as etapas de um valor. A MDN recomenda pelo menos 1,5 no texto corrido principal, o que ajuda pessoas com baixa visão e com dislexia. - Comprimento da linha. Linhas muito longas cansam a leitura;
max-inline-size: 65chlimita o parágrafo a algo perto de 65 caracteres. - Títulos equilibrados.
text-wrap: balance, que entrou no Baseline em 2024, distribui as palavras de um título curto entre as linhas e evita a última linha com uma palavra só. É um cálculo caro, por isso os navegadores o aplicam só a blocos curtos: até seis linhas no Chromium e até dez no Firefox. O valorprettyusa um algoritmo mais lento, que reduz as palavras solitárias no fim dos parágrafos, e deve ficar nos blocos em que a aparência importa mais que a velocidade. - Hifenização.
hyphens: autodeixa o navegador partir palavras longas com hífen, mas ele só faz isso quando o idioma está marcado no HTML com o atributolange quando tem o dicionário de hifenização daquela língua. Sem olang="pt-BR", a hifenização não acontece. Em colunas estreitas de texto justificado, ela evita os buracos entre as palavras. - Números alinhados.
font-variant-numeric: tabular-numsdá a todos os algarismos a mesma largura, o que alinha valores em tabelas e evita que um contador ou um cronômetro "dance" a cada mudança.
Cores e temas
As cores são o ponto em que a identidade visual, a acessibilidade e as preferências da pessoa se encontram, e o CSS ganhou ferramentas próprias para cada uma dessas frentes.
Tema claro e escuro sem duplicar a folha
O caminho clássico para o tema escuro é a media query prefers-color-scheme, que redefine as variáveis de cor quando o sistema da pessoa está no modo escuro. Duas peças simplificam o trabalho. A propriedade **color-scheme**, disponível em todos os principais navegadores desde janeiro de 2022, declara quais esquemas a página suporta; com ela, o navegador ajusta ao esquema certo o que é dele, como a cor de fundo da tela, as barras de rolagem e os controles de formulário. O resto da página continua por conta do autor. E a função **light-dark()**, Baseline desde maio de 2024, recebe duas cores e devolve a primeira no esquema claro e a segunda no escuro, desde que o color-scheme declare os dois:
:root {
color-scheme: light dark;
--fundo: light-dark(#f4f4ee, #14170f);
--texto: light-dark(#1d2016, #e8e8e0);
}
body { background: var(--fundo); color: var(--texto); }
/* um seletor de tema na página só troca o esquema da raiz */
:root[data-tema="claro"] { color-scheme: light; }
:root[data-tema="escuro"] { color-scheme: dark; }
As cores ficam declaradas uma vez, lado a lado, e um seletor de tema com "claro", "escuro" e "automático" se resume a trocar um atributo na raiz.
Cores derivadas
A sintaxe de cor relativa, Baseline desde setembro de 2024, cria uma cor a partir de outra, mexendo só nos canais que interessam:
.botao { background: var(--marca); }
.botao:hover { background: oklch(from var(--marca) calc(l - 0.15) c h); }
.selo { background: color-mix(in oklch, var(--marca) 20%, transparent); }
A cor do :hover é a da marca com a luminosidade reduzida em 0,15, numa escala de 0 a 1, mantendo o croma e o matiz: um estado mais escuro sem escolher uma segunda cor à mão. O oklch é o espaço adequado para isso porque nele a luminosidade acompanha a percepção, e escurecer dois tons diferentes na mesma medida dá resultados parecidos aos olhos. O selo usa o color-mix() para uma versão translúcida. Nada disso dispensa a conferência de contraste descrita na seção de acessibilidade: uma cor derivada continua precisando do mínimo de 4,5:1 com o texto que vai por cima dela.
Transições e animações
O CSS tem dois mecanismos de movimento. As transições suavizam a mudança entre dois estados: quando uma propriedade muda, por um :hover ou uma classe nova, o navegador interpola do valor antigo ao novo ao longo do tempo pedido. As animações descrevem uma sequência de quadros com @keyframes e rodam por conta própria, uma vez, várias vezes ou para sempre.
.botao {
transition: transform 160ms ease-out, background-color 160ms;
}
.botao:hover { transform: translateY(-2px); }
@keyframes girar { to { rotate: 1turn; } }
.carregando { animation: girar 1s linear infinite; }
A função de tempo dá o caráter do movimento: linear serve para rotações contínuas, ease-out começa rápido e freia, como objetos reais que param, e cubic-bezier() permite desenhar a curva exata.
Alguns cuidados evitam a maior parte dos problemas:
- Transição de propriedades específicas.
transition: allanima tudo o que mudar, inclusive propriedades caras de layout e mudanças que ninguém pretendia animar. - Elementos que aparecem. Uma transição não roda quando o elemento sai de
display: none, porque não há "valor antigo" para interpolar. A regra@starting-styledefine esse ponto de partida, e permite animar a entrada de um diálogo ou de um menu só com CSS. - O truque do traço. Um caminho SVG com
pathLength="1"passa a ter comprimento 1, qualquer que seja o desenho. Comstroke-dasharray: 1estroke-dashoffsetindo de 1 a 0, o traço parece ser desenhado na tela. É a técnica dos logotipos que "se desenham".
Transições de visualização
As transições comuns animam uma propriedade de um elemento. As transições de visualização animam a passagem de um estado inteiro da tela para outro: o navegador tira uma foto do estado antigo, aplica a mudança e anima de uma foto para a outra. Numa aplicação de página única, a mudança vai dentro de document.startViewTransition():
function mostrarPedido(html) {
if (!document.startViewTransition) {
renderizar(html); // navegadores sem o recurso trocam sem animação
return;
}
document.startViewTransition(() => renderizar(html));
}
O CSS dá nome aos elementos que devem se mover por conta própria, com view-transition-name, e estiliza a animação pelos pseudoelementos ::view-transition-old() e ::view-transition-new(). A miniatura de um produto numa lista e a foto grande na página do produto podem receber o mesmo nome, e o navegador anima uma se transformando na outra. As transições dentro da mesma página entraram no Baseline em 2025. Para sites de várias páginas, a regra @view-transition { navigation: auto; }, colocada nas duas páginas, pede a mesma animação na navegação entre elas; é uma variante mais recente, e um navegador que não a conhece ignora a regra e navega sem animação, como o CSS faz com tudo o que não entende. Como todo movimento grande, a transição deve respeitar o prefers-reduced-motion.
Desempenho: o custo de cada estilo
Na maioria dos sites, o CSS raramente é o gargalo, mas alguns erros custam caro, e eles se explicam pelo caminho de renderização.
- **Anime
transformeopacity.** Animartop,left,widthoumarginrefaz o layout a cada quadro. Mover comtransform: translate()mantém a animação só na composição. - Evite o vaivém de leitura e escrita. Em JavaScript, ler uma medida (
offsetHeight,getBoundingClientRect) logo depois de mudar um estilo força o navegador a recalcular o layout na hora. Feito num laço, isso trava a página. Leia tudo primeiro e escreva depois. - Reserve espaço para o que carrega depois. Imagens sem dimensões, anúncios e fontes que chegam tarde empurram o conteúdo e geram a "mudança de layout acumulada" (CLS), uma das métricas Core Web Vitals.
widtheheightnas imagens, ouaspect-ratio, resolvem a maior parte. - **
content-visibility: auto** deixa o navegador pular o layout e a pintura de seções fora da tela, um ganho grande em páginas longas. - **
will-changecom moderação.** Ele pede ao navegador que prepare uma camada própria para o elemento. Em poucos elementos que vão animar, ajuda; espalhado pela página, consome memória. - CSS enxuto e entregue cedo. Como bloqueia a renderização, o CSS da primeira dobra deve chegar rápido. Ferramentas que geram só as classes usadas evitam mandar quilos de estilos que a página nunca aplica.
Como medir em vez de adivinhar
As métricas que importam para o usuário são as Core Web Vitals: o LCP (Largest Contentful Paint, quanto tempo o maior elemento visível leva para aparecer), o INP (Interaction to Next Paint, quanto a página demora a responder a uma interação) e o CLS (o quanto o conteúdo pula enquanto carrega). O CSS influencia as três: atrasa o LCP quando bloqueia a renderização por muito tempo, piora o INP quando cada clique dispara um recálculo de estilo caro e causa CLS quando não reserva espaço.
Para investigar, o painel de desempenho das ferramentas do navegador grava alguns segundos de uso e mostra, quadro a quadro, quanto tempo foi gasto em estilo, layout, pintura e composição. Um bloco roxo de layout repetido dentro de um laço é o sinal clássico do vaivém de leitura e escrita.
Acessibilidade começa no CSS
Muitas barreiras de acessibilidade são decisões de estilo:
- Contraste. As diretrizes WCAG pedem contraste de pelo menos 4,5:1 para texto comum e 3:1 para texto grande. Uma cor de marca vibrante pode servir de fundo de botão e ser ilegível como cor de texto sobre fundo claro.
- Foco visível. Remover o contorno de foco sem colocar outro no lugar deixa quem navega pelo teclado sem saber onde está. Use
:focus-visiblepara desenhar um foco bonito, e não para escondê-lo. - Movimento reduzido. Pessoas com sensibilidade a movimento ativam essa preferência no sistema. Com
prefers-reduced-motion: reduce, animações grandes devem virar transições discretas ou nenhuma. - Tamanhos relativos. Fontes em
remrespeitam a preferência de tamanho do usuário; fontes fixas empxem todo lugar podem ignorá-la. - Ordem visual e ordem do código. Grid e flexbox permitem reordenar visualmente os elementos, mas leitores de tela e o teclado seguem a ordem do HTML. As duas devem contar a mesma história.
- Esconder do jeito certo.
display: noneesconde de todos; um texto só para leitores de tela usa uma classe que o tira da tela sem tirá-lo da árvore de acessibilidade. - Alvos de toque. A versão 2.2 das WCAG pede que botões e links tenham pelo menos 24 × 24 px de área clicável, ou espaço suficiente em volta. Um ícone de lixeira de 16 px colado ao de editar é um convite ao clique errado, principalmente no celular.
- Modos de cores forçadas. O Windows tem temas de alto contraste que substituem as cores de todos os sites por uma paleta do sistema. A media query
forced-colors: activedetecta esse modo. Nele, um botão que só se distingue pela cor de fundo vira texto solto; uma borda transparente devolve o contorno.
Depurando CSS sem adivinhação
Boa parte do tempo perdido com CSS vem de mudar regras às cegas até algo funcionar. As ferramentas do desenvolvedor dos navegadores (a tecla F12 no Chrome, no Edge e no Firefox) transformam isso numa investigação:
- Inspecione o elemento certo. Clique com o botão direito no elemento e escolha "Inspecionar". Muitas vezes a regra está certa e o elemento é outro: um
spandentro do botão, e não o botão. - Leia o painel de estilos de cima para baixo. As regras aparecem na ordem da cascata, da que vence para a que perde. Uma declaração riscada foi vencida por outra mais acima; uma com ícone de aviso é inválida ou não tem efeito naquele contexto, como
justify-contentnum elemento que não é flex. - Confira o valor calculado. A aba "Computado" mostra o valor final de cada propriedade e, ao expandir, de qual regra ele veio. É o jeito mais rápido de achar uma herança inesperada.
- Veja o modelo de caixa desenhado. O diagrama de conteúdo, padding, borda e margem mostra os números reais, e passar o mouse sobre o elemento pinta cada área na página.
- Ligue as sobreposições de layout. Os navegadores desenham as linhas, os números e as áreas de um grid, e os eixos e espaços de um flexbox, por cima da página. Um item que "não vai para a coluna certa" fica evidente.
- Force estados. Dá para travar
:hover,:focuse:focus-visiblenum elemento, para estilizar um menu que some quando o mouse sai de cima dele.
CSS para o papel
Relatórios, pedidos, romaneios e etiquetas ainda são impressos ou salvos em PDF pelo navegador, que usa o mesmo CSS para isso, com a media query print:
@media print {
nav, .barra-de-acoes, .botao { display: none; } /* o que não serve no papel */
body { font-size: 11pt; color: #000; background: #fff; }
a[href^="http"]::after { content: " (" attr(href) ")"; } /* o endereço do link aparece impresso */
tr, .cartao-pedido { break-inside: avoid; } /* não parte a linha ao meio */
.selo-status { print-color-adjust: exact; } /* a cor do selo carrega informação */
}
@page {
size: A4;
margin: 15mm;
}
Cada peça tem um motivo. O display: none tira menus e botões, que não fazem sentido no papel. O pseudoelemento depois dos links escreve o endereço ao lado do texto, já que no papel não há como clicar. As propriedades break-before, break-after e break-inside dizem onde a página pode ou não ser quebrada, e o break-inside: avoid impede que a linha de uma tabela ou o cartão de um pedido fique metade numa folha e metade na outra. A regra @page, que entrou no Baseline em 2024, define o tamanho do papel e as margens.
A última peça é a das cores. Por padrão, a propriedade print-color-adjust vale economy, que deixa o navegador adaptar a página à impressão, por exemplo omitindo imagens de fundo e ajustando cores de texto para o papel branco. Com exact, Baseline desde maio de 2025, a página pede que um elemento seja impresso como foi desenhado, o que importa quando a cor carrega informação, como um selo de "vencido" em vermelho. A configuração de impressão escolhida pela pessoa ainda prevalece sobre o pedido da página. Antes de entregar um relatório, vale abrir a visualização de impressão do navegador, ou simular o modo de impressão nas ferramentas do desenvolvedor, e conferir as quebras de página.
Organizando CSS em projetos grandes
O CSS é global por natureza: qualquer regra pode afetar qualquer elemento. Em projetos grandes, isso vira conflito de nomes, especificidade crescente e medo de apagar código. As abordagens para domar isso evoluíram:
| Abordagem | Ideia | Prós | Contras |
|---|---|---|---|
| Convenção de nomes (BEM) | nomes como cartao__titulo--destaque | só disciplina, nenhuma ferramenta | depende de todos seguirem |
| CSS Modules | o build gera nomes únicos por arquivo | escopo garantido, CSS comum | estilos espalhados por componente |
| CSS-in-JS | estilos escritos no JavaScript do componente | dinâmico, junto da lógica | custo em tempo de execução em algumas bibliotecas |
| Utilitários (Tailwind) | classes pequenas compostas no HTML | sem nomes para inventar, CSS final pequeno | HTML com muitas classes |
Em comum, as abordagens maduras usam tokens de design: cores, espaçamentos, raios e fontes definidos uma vez, como variáveis, e usados em todo lugar. Trocar a cor da marca vira uma mudança de uma linha.
Tailwind, em especial
O Tailwind é um framework de classes utilitárias: em vez de criar uma classe .cartao e escrever os estilos dela num arquivo, você compõe classes prontas no próprio HTML, como rounded-xl p-6 bg-white shadow-sm. Na hora do build, o Tailwind lê os arquivos do projeto e gera só o CSS das classes que aparecem. A versão 4 mudou bastante o jeito de configurar: a configuração passou a ser escrita no próprio CSS, com diretivas como @theme para os tokens e @custom-variant para variantes próprias, e o motor foi reescrito para builds mais rápidos. Container queries passaram a ser nativas, com classes como @container e @lg:grid-cols-2.
Por dentro, o Tailwind 4 se apoia nos recursos que este artigo descreveu. Cada token declarado em @theme vira uma variável CSS de verdade, disponível também para o CSS escrito à mão. O CSS gerado é organizado em camadas de cascata (theme, base, components e utilities, nessa ordem), o que garante que uma classe utilitária vença um estilo de componente sem precisar de !important. As variantes são prefixos que embrulham a classe numa condição: hover: vira uma pseudoclasse, md: vira uma media query, motion-safe: só vale quando o usuário não pediu menos movimento, e has-[...]: usa o :has().
A crítica clássica, "o HTML fica poluído de classes", tem fundamento, mas na prática os projetos com componentes (React, Vue) encapsulam essas classes dentro de cada componente, e o ganho de não inventar nomes nem navegar entre arquivos costuma compensar.
Erros comuns
- Brigar com a especificidade acrescentando seletores ou
!importantaté a regra "pegar". Entenda qual critério da cascata está decidindo e corrija na origem, com camadas ou seletores mais simples. - **Esquecer o
box-sizing: border-box** e somar padding à largura sem perceber. - Usar margens para espaçar itens de flex e grid, em vez de
gap. - Alturas fixas em contêineres de texto. O texto cresce com a tradução, o zoom ou um dado maior, e transborda.
- **
100vhno celular**, que ignora as barras do navegador. Use100dvhou100svh. - Animar propriedades de layout, como
widthetop, em vez detransform. - Esconder o contorno de foco com
outline: nonee não oferecer outro. - Cor da marca como texto sem conferir o contraste.
- Media queries para tudo, quando o componente depende do espaço do container.
- Valores mágicos espalhados, como
margin-top: 13pxaqui e17pxali, em vez de uma escala de espaçamento. - **Item flex ou coluna
1frque não encolhe.** O mínimo automático é o tamanho do conteúdo. A correção émin-width: 0no item ouminmax(0, 1fr)na coluna. - **
z-indexcada vez maior.** Se 9999 não resolve, 99999 também não vai resolver: o elemento está num contexto de empilhamento abaixo de outro. Descubra qual ancestral cria o contexto. - **
line-heightcomemou porcentagem no elemento pai**, que os filhos herdam já convertido em pixels. Use o valor sem unidade. - **Hifenização sem
lang** no HTML: ohyphens: autosimplesmente não funciona. - Esquecer a impressão. Relatórios impressos com menus, botões e linhas de tabela partidas ao meio.
Boas práticas
- Comece com uma base consistente:
border-box, tipografia emreme uma escala de espaçamento. - Defina cores, espaçamentos e fontes como tokens, e use-os em todo lugar.
- Grid para a estrutura, flexbox para os componentes,
gappara o espaçamento entre itens. - Mobile first, com container queries nos componentes que mudam de lugar.
- Seletores simples e especificidade baixa; camadas para ordenar bibliotecas, componentes e utilitários.
- Anime
transformeopacity, e respeiteprefers-reduced-motion. - Confira o contraste de toda combinação de texto e fundo, nos temas claro e escuro.
- Reserve espaço para imagens e conteúdos que carregam depois.
- Use as ferramentas do navegador para descobrir qual regra está vencendo, antes de escrever outra.
Perguntas frequentes
Vale a pena aprender CSS "puro" se o projeto usa Tailwind? Vale, e muito. O Tailwind é uma forma de escrever CSS, não um substituto: cada classe é uma declaração. Quem não entende cascata, modelo de caixa, flexbox e grid escreve classes sem saber por que o layout quebrou, e quem entende lê min-w-0 e sabe exatamente o que está resolvendo.
Flexbox ou grid para centralizar uma coisa? Os dois funcionam. display: grid; place-items: center no container é a forma mais curta. Para centralizar só na horizontal um bloco de largura definida, margin-inline: auto continua sendo a resposta mais simples.
Media query ou container query? Media query para o que depende da tela ou das preferências da pessoa: o layout geral da página, o tema escuro, o movimento reduzido. Container query para o que depende do espaço que um componente recebeu: um cartão que vai tanto na área principal quanto na lateral.
Como usamos CSS na Gothor
O site e este blog seguem os princípios deste artigo: layout com grid e flexbox, variáveis para as cores e os espaçamentos da identidade visual, responsividade pensada desde o celular, respeito a quem prefere menos animação e contraste conferido para uma leitura confortável.
Resumo
- CSS é declarativo: regras com seletores e declarações, e várias regras disputando o mesmo elemento. Ele ignora o que não entende, sem avisar.
- O navegador transforma estilos em pixels em etapas: estilo, layout, pintura e composição. Quanto mais cedo uma mudança recomeça o caminho, mais ela custa.
- A cascata decide quem vence: origem e importância, camadas, especificidade e ordem. A especificidade compara IDs, classes e elementos, coluna a coluna.
- Toda caixa tem conteúdo, padding, borda e margem;
border-boxfaz a largura significar o que parece. Contextos de empilhamento explicam oz-indexque "não funciona". - Variáveis existem em tempo de execução; com
@property, ganham tipo e podem ser animadas. - Flexbox distribui itens em um eixo, dividindo a sobra e a falta conforme os fatores; grid organiza linhas e colunas.
min-width: 0eminmax(0, 1fr)resolvem os transbordamentos mais comuns. - Media queries olham a janela; container queries olham o espaço do componente.
- Anime
transformeopacity, meça com as ferramentas do navegador, reserve espaço para o que carrega depois e respeite contraste, foco, alvos de toque, cores forçadas e movimento reduzido. - Cada valor passa por etapas, de declarado a real; a herança leva o valor calculado, por isso a altura de linha vai sem unidade.
color-schemeelight-dark()resolvem o tema escuro sem duplicar a folha; a sintaxe de cor relativa deriva estados a partir da cor da marca.- Recursos recentes, como
@scope, posicionamento por âncora e transições de visualização, substituem bibliotecas inteiras; o selo Baseline diz quando já dá para usá-los. - Tokens de design com papéis semânticos e uma estratégia de organização mantêm projetos grandes sob controle.
Para ir além
- CSS na MDN, a referência mais completa, em português.
- Cascata, especificidade e herança e o modelo de caixa, no curso da MDN.
- Container queries e grid layout, na MDN.
- Contextos de empilhamento, na MDN, para o mistério do
z-index. - Learn CSS, o curso do web.dev, e o Baseline, para saber o que já funciona em todos os navegadores.
- O trabalho atual do W3C em CSS, com o estado de cada módulo.
- Contraste mínimo e tamanho do alvo nas WCAG 2.2.
- O CSS Zen Garden, ainda no ar, para ver a mesma página com centenas de estilos.
- O processamento dos valores das propriedades e a regra @scope, na MDN.
- Posicionamento por âncora e a API de transições de visualização, na MDN.
- A função light-dark() e o descritor font-display, na MDN.
- CSS no web.dev, com os recursos que entraram no Baseline.
- Documentação do Tailwind CSS e do shadcn/ui.
Este post foi escrito com a ajuda da Juniper, a assistente de IA da Gothor, e revisado pela nossa equipe antes de ir ao ar.


