Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

O que é CSS e como o navegador aplica estilo na página

CSS é a camada que decide como o HTML aparece: o que é uma regra, como o navegador casa seletor com elemento e por que a ordem importa.

Rodolfo Mori11 min de leitura

CSS é a linguagem que descreve como o HTML deve aparecer. O HTML diz que aquele texto é um título; o CSS diz que o título é verde e tem letra de 32 pixels. O navegador lê as duas descrições, combina uma com a outra e só então pinta a tela.

Os exemplos daqui montam a página da Clínica Veterinária Pata Firme, uma clínica de bairro que atende cachorro e gato e ainda não tem site. Se você nunca escreveu uma tag, comece por o que é HTML e volte depois.

Uma observação sobre as saídas: nada aqui foi digitado à mão, e o que aparece logo abaixo de cada bloco é o que o console imprimiu. A maioria dos blocos js é código de navegador, executado num Chromium 151 sem interface gráfica — esses você cola no console do DevTools e vê os mesmos números. Os três que começam com import são a exceção: rodam no Node, pelo terminal, e o texto avisa quando é o caso.

A mesma página com a folha ligada e desligada

Este é o HTML da clínica. Repare na linha do link: é ela que traz o CSS para dentro da página.

html
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>Clínica Veterinária Pata Firme</title>
  <link rel="stylesheet" href="estilo.css">
</head>
<body>
  <h1>Clínica Veterinária Pata Firme</h1>
  <p class="aviso">Atendemos sem hora marcada aos sábados.</p>
  <ul>
    <li>Consulta clínica — R$ 120</li>
    <li>Vacina antirrábica — R$ 90</li>
    <li>Banho e tosa — R$ 70</li>
  </ul>
</body>
</html>

E este é o estilo.css, com três regras curtas:

css
body {
  background-color: #f4f1ea;
  color: #2b2b2b;
}

h1 {
  color: #1f6f4a;
  font-size: 32px;
}

.aviso {
  background-color: #fff3cd;
  padding: 12px;
}

Em vez de acreditar no efeito, dá para perguntar ao navegador. A função getComputedStyle devolve o valor final que ele decidiu usar em cada propriedade, depois de somar tudo. Desligando a folha com document.styleSheets[0].disabled = true, a mesma medição roda de novo sem o nosso CSS:

js
const v = (sel, prop) => getComputedStyle(document.querySelector(sel))[prop];
const ler = () => [
  `body    fundo: ${v('body', 'background-color')} · texto: ${v('body', 'color')}`,
  `h1      cor: ${v('h1', 'color')} · tamanho: ${v('h1', 'font-size')}`,
  `.aviso  fundo: ${v('.aviso', 'background-color')} · padding: ${v('.aviso', 'padding')}`,
].join('\n');

console.log('--- com estilo.css ---\n' + ler());
document.styleSheets[0].disabled = true;
console.log('--- com a folha desligada ---\n' + ler());
--- com estilo.css --- body fundo: rgb(244, 241, 234) · texto: rgb(43, 43, 43) h1 cor: rgb(31, 111, 74) · tamanho: 32px .aviso fundo: rgb(255, 243, 205) · padding: 12px --- com a folha desligada --- body fundo: rgba(0, 0, 0, 0) · texto: rgb(0, 0, 0) h1 cor: rgb(0, 0, 0) · tamanho: 32px .aviso fundo: rgba(0, 0, 0, 0) · padding: 0px

Três coisas para anotar. Primeiro: o navegador não devolveu #f4f1ea, devolveu rgb(244, 241, 234). Ele normaliza todo valor de cor, então não se assuste ao ver a sua notação trocada no DevTools. Segundo: fundo “sem cor” não é branco, é rgba(0, 0, 0, 0) — transparente. O branco que você enxerga é a janela do navegador atrás da página.

E o terceiro é o mais interessante: o h1 continuou com 32px mesmo sem a nossa folha. A regra font-size: 32px não mudou nada; ela só repetiu um tamanho que o navegador já tinha decidido sozinho. Guarde essa pista, porque ela reaparece três seções abaixo.

Anatomia de uma regra: seletor, propriedade e valor

Toda a linguagem cabe num desenho. Uma regra tem um seletor, que diz para quem ela vale, e um bloco de declarações entre chaves. Cada declaração é um par de propriedade e valor, fechado com ponto e vírgula.

h1 { color : #1f6f4a ; } bloco de declarações seletor propriedade valor

Essa separação não é didática, é literal: um analisador de CSS separa exatamente essas peças. O css-tree é um desses analisadores, escrito em JavaScript — e é o primeiro dos três blocos que não rodam no navegador: ele é um pacote de terminal, instalado numa pasta ao lado do estilo.css.

bash
npm init -y
npm install css-tree

Com ele instalado, dá para pedir que abra o nosso arquivo e mostre o que enxergou. Salve o trecho abaixo como anatomia.mjs (a extensão .mjs é o que libera o import) e rode com node anatomia.mjs:

js
import * as csstree from 'css-tree';
import fs from 'node:fs';

const arvore = csstree.parse(fs.readFileSync('estilo.css', 'utf8'));
let regras = 0;
let declaracoes = 0;

csstree.walk(arvore, {
  visit: 'Rule',
  enter(no) {
    regras++;
    console.log(`regra ${regras} · seletor: ${csstree.generate(no.prelude)}`);
    no.block.children.forEach((decl) => {
      declaracoes++;
      console.log(`   propriedade: ${decl.property.padEnd(17)}valor: ${csstree.generate(decl.value)}`);
    });
  },
});

console.log(`\ntotal: ${regras} regras e ${declaracoes} declarações`);
regra 1 · seletor: body propriedade: background-color valor: #f4f1ea propriedade: color valor: #2b2b2b regra 2 · seletor: h1 propriedade: color valor: #1f6f4a propriedade: font-size valor: 32px regra 3 · seletor: .aviso propriedade: background-color valor: #fff3cd propriedade: padding valor: 12px

total: 3 regras e 6 declarações

Três regras, seis declarações. O arquivo que você escreve como texto vira, para a máquina, uma lista de pares — e é só isso que ela guarda.

Como o navegador casa a regra com o elemento

O seletor é um endereço. Quando o navegador termina de ler a folha, ele pega cada seletor e procura, dentro da página, quem responde por aquele endereço. É a mesma busca que document.querySelectorAll faz — dá para reproduzir o casamento regra por regra:

js
for (const regra of document.styleSheets[0].cssRules) {
  const casaram = [...document.querySelectorAll(regra.selectorText)];
  const tags = casaram.map((el) => `<${el.tagName.toLowerCase()}>`).join(', ');
  console.log(`${regra.selectorText.padEnd(8)} casou com ${casaram.length} elemento: ${tags}`);
}
body casou com 1 elemento: <body> h1 casou com 1 elemento: <h1> .aviso casou com 1 elemento: <p>

Repare na última linha. O seletor .aviso não fala nada sobre parágrafo, mas foi um <p> que apareceu — porque o único elemento com class="aviso" na página é aquele parágrafo. O seletor procura uma condição, não uma tag. Se amanhã o aviso virar uma <div class="aviso">, a regra continua valendo sem uma linha de mudança.

Esse é o motivo de a classe ser a ferramenta preferida no dia a dia: ela deixa você marcar “isto é um aviso” sem prender o estilo à etiqueta escolhida. As outras formas de escrever endereço — descendente, filho, atributo, agrupamento — estão em seletores e cascata no CSS.

A folha de estilo que o navegador já traz de fábrica

Volte à pista da primeira seção. Abrindo a mesma página sem nenhum link de CSS, e medindo propriedades que a nossa folha nem toca:

js
const ler = (sel, props) => {
  const c = getComputedStyle(document.querySelector(sel));
  console.log(`${sel.padEnd(7)} ${props.map((p) => `${p}: ${c[p]}`).join(' · ')}`);
};

ler('h1', ['display', 'font-size', 'font-weight', 'margin-top']);
ler('p', ['display', 'font-size', 'margin-top']);
ler('ul', ['display', 'padding-left', 'list-style-type']);
ler('li', ['display']);
h1 display: block · font-size: 32px · font-weight: 700 · margin-top: 21.44px p display: block · font-size: 16px · margin-top: 16px ul display: block · padding-left: 40px · list-style-type: disc li display: list-item

Nenhum desses valores veio de nós. Título em negrito, bolinha na lista, recuo de 40 pixels, espaço entre parágrafos: tudo isso é uma folha de estilo que o navegador aplica antes da sua, chamada user agent stylesheet. Ela existe para que uma página sem CSS ainda seja legível.

E ela não é segredo: no Chrome, é um arquivo html.css dentro do motor Blink. Estes são os trechos que produziram os números acima, copiados do código-fonte:

css
h1 {
    display: block;
    font-size: 2em;
    margin-block-start: 0.67__qem;
    margin-block-end: 0.67em;
    margin-inline-start: 0;
    margin-inline-end: 0;
    font-weight: bold
}

ul, menu, dir {
    display: block;
    list-style-type: disc;
    margin-block-start: 1__qem;
    margin-block-end: 1em;
    padding-inline-start: 40px
}

Agora as contas fecham. font-size: 2em sobre os 16px padrão dá exatamente os 32px que o h1 mostrou — e explica por que a nossa regra font-size: 32px não mudou nada. A margem de 0.67em sobre 32px dá 21,44px, o valor que apareceu com duas casas. (O __qem é uma unidade interna do Blink: um em marcado para o quirks mode, o modo de compatibilidade com páginas antigas. Num arquivo seu, isso seria só em.)

Dá para medir o tamanho dessa herança. O arquivo é público, então o primeiro passo é baixar ele para o lado do anatomia.mjs:

bash
curl -s "https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/core/html/resources/html.css?format=TEXT" | base64 -d > ua.css

Agora é o mesmo css-tree da seção anterior, também no terminal, apontado para o ua.css em vez do nosso arquivo:

js
import * as csstree from 'css-tree';
import fs from 'node:fs';

const folha = fs.readFileSync('ua.css', 'utf8').replaceAll('__qem', 'em');
let regras = 0;
let declaracoes = 0;

csstree.walk(csstree.parse(folha), {
  visit: 'Rule',
  enter(no) {
    regras++;
    no.block.children.forEach(() => declaracoes++);
  },
});

console.log(`html.css do Blink: ${folha.split('\n').length} linhas`);
console.log(`${regras} regras e ${declaracoes} declarações antes de você escrever a primeira`);
html.css do Blink: 2724 linhas 334 regras e 945 declarações antes de você escrever a primeira

Novecentas e quarenta e cinco declarações já estão escritas antes de você abrir o editor. Nem todas valem na sua página — entram em jogo só as regras cujos seletores casam com as tags que você realmente usou. Mas o ponto se mantém: escrever CSS quase nunca é começar do zero, é discordar de alguma dessas.

Quatro regras para o mesmo elemento: quem ganha

Como várias folhas se somam, é normal duas regras pedirem coisas diferentes para o mesmo elemento. A palavra cascata, que dá o C do nome CSS, é justamente o critério de desempate.

A clínica quer destacar o item de urgência. Esta página tem um li só, com id e classe, e uma folha cascata.css:

html
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="cascata.css">
</head>
<body>
  <ul>
    <li id="urgencia" class="destaque">Atendimento de urgência — 24h</li>
  </ul>
</body>
</html>

E quatro regras acabaram mirando esse mesmo li:

css
li {
  color: black;
}

li {
  color: gray;
}

.destaque {
  color: orange;
}

#urgencia {
  color: crimson;
}

Em vez de discutir, dá para apagar uma regra por vez e ver a cor mudar ao vivo. deleteRule remove a regra pelo índice, e getComputedStyle diz quem assumiu o lugar:

js
const item = document.querySelector('li');
const folha = document.styleSheets[0];
const cor = () => getComputedStyle(item).color;

console.log(`as 4 regras no ar ............. ${cor()}`);
folha.deleteRule(3);
console.log(`sem #urgencia ................. ${cor()}`);
folha.deleteRule(2);
console.log(`sem #urgencia e sem .destaque . ${cor()}`);
folha.deleteRule(1);
console.log(`só a primeira regra li ........ ${cor()}`);
as 4 regras no ar ............. rgb(220, 20, 60) sem #urgencia ................. rgb(255, 165, 0) sem #urgencia e sem .destaque . rgb(128, 128, 128) só a primeira regra li ........ rgb(0, 0, 0)

A ordem de vitória foi #urgencia, depois .destaque, depois o segundo li, depois o primeiro. Dois critérios diferentes decidiram isso.

Entre #urgencia, .destaque e li, venceu quem é mais específico: um id vale mais que uma classe, e uma classe vale mais que um nome de tag. Já entre os dois li, que têm a mesma força, venceu quem veio por último no arquivo — o gray sobrescreveu o black. É por isso que colar uma regra no fim da folha costuma “resolver” o problema, e é também por isso que a folha vira uma bagunça quando você faz só isso. A conta completa está em especificidade no CSS.

A declaração que o navegador joga fora sem avisar

Este é o tropeço número um de quem começa, e ele não parece um erro. Veja a regra abaixo: tem uma propriedade escrita em inglês britânico e um font-size sem ponto e vírgula e sem unidade.

css
h1 {
  colour: #1f6f4a;
  font-size: 32
  text-align: center;
}

Salve isso como quebrado.css, ligue numa página que tenha só o <h1> da clínica e abra. O navegador não reclama de nada: nenhuma linha vermelha no console, nenhum aviso. Perguntando o que ele guardou:

js
const regra = document.styleSheets[0].cssRules[0];
const c = getComputedStyle(document.querySelector('h1'));

console.log(`declarações que sobreviveram: ${regra.style.length}`);
console.log(`regra como o navegador guardou: ${regra.cssText}`);
console.log(`color no h1: ${c.color}`);
console.log(`font-size no h1: ${c.fontSize}`);
console.log(`text-align no h1: ${c.textAlign}`);
declarações que sobreviveram: 0 regra como o navegador guardou: h1 { } color no h1: rgb(0, 0, 0) font-size no h1: 32px text-align no h1: start

Zero. Das três declarações escritas, nenhuma sobrou, e a regra virou um par de chaves vazio. colour foi descartada porque essa propriedade não existe. O 32 sem unidade também é inválido — e, como faltou o ponto e vírgula, o navegador continuou lendo o valor até achar um, engolindo o text-align: center junto.

O font-size: 32px que aparece na saída é a armadilha completa: ele veio da folha de fábrica, não da sua regra. A página parece meio certa, e é por isso que o erro sobrevive tanto tempo.

Como o navegador não avisa, use um analisador que avisa. De volta ao terminal, o css-tree lê o mesmo quebrado.css, compara cada declaração com a gramática oficial da propriedade e aponta a linha:

js
import * as csstree from 'css-tree';
import fs from 'node:fs';

const arvore = csstree.parse(fs.readFileSync('quebrado.css', 'utf8'), { positions: true });

csstree.walk(arvore, {
  visit: 'Declaration',
  enter(no) {
    const { error } = csstree.lexer.matchDeclaration(no);
    console.log(`linha ${no.loc.start.line}: ${no.property} — ${error ? error.message : 'ok'}`);
  },
});
linha 2: colour — Unknown property `colour` linha 3: font-size — Mismatch syntax: <absolute-size> | <relative-size> | <length-percentage [0,∞]> | math value: 32 text-align: center --------^

A segunda mensagem mostra o estrago inteiro: no campo value do font-size está escrito 32 e, embaixo, text-align: center. A seta aponta onde a gramática se perdeu. Na prática do dia a dia você não roda isso na mão — o DevTools risca a declaração inválida no painel Styles, e uma extensão de lint no editor faz o mesmo enquanto você digita.

CSSOM: o estilo também vira estrutura de dados

Você já viu document.styleSheets sendo usado o artigo inteiro sem explicação. Ele é o CSSOM, o modelo de objetos do CSS: assim como o HTML vira uma árvore de elementos, a folha de estilo vira uma lista de objetos consultáveis e editáveis.

js
const folha = document.styleSheets[0];
const regra = folha.cssRules[1];

console.log(`folhas carregadas: ${document.styleSheets.length}`);
console.log(`regras nesta folha: ${folha.cssRules.length}`);
console.log(`tipo do objeto: ${regra.constructor.name}`);
console.log(`seletor: ${regra.selectorText}`);
console.log(`declarações: ${regra.style.length} (${[...regra.style].join(', ')})`);

regra.style.color = 'crimson';
console.log(`h1 depois da mudança: ${getComputedStyle(document.querySelector('h1')).color}`);

folha.insertRule('li { color: #1f6f4a; }', folha.cssRules.length);
console.log(`regras agora: ${folha.cssRules.length} · cor do li: ${getComputedStyle(document.querySelector('li')).color}`);
folhas carregadas: 1 regras nesta folha: 3 tipo do objeto: CSSStyleRule seletor: h1 declarações: 2 (color, font-size) h1 depois da mudança: rgb(220, 20, 60) regras agora: 4 · cor do li: rgb(31, 111, 74)

Cada regra é um objeto CSSStyleRule com selectorText e style. Mudar style.color repintou o título na hora, e insertRule acrescentou uma quarta regra com a página já aberta.

Isso responde uma dúvida comum de quem está começando: o CSS não é uma imagem congelada. Ele fica vivo na memória, ao lado da árvore DOM que o navegador montou, e é dessa dupla que sai o desenho final da tela.

O que você consegue mudar hoje, com cinco propriedades

Cinco propriedades já tiram uma página do estado cru. Estas são as que valem a pena decorar primeiro:

propriedade o que muda exemplo
color a cor do texto color: #1f6f4a
background-color a cor do fundo da caixa background-color: #fff3cd
font-size o tamanho da letra font-size: 32px
padding o espaço entre o texto e a borda padding: 12px
border-radius o arredondamento dos cantos border-radius: 8px

A folha final da clínica usa todas elas:

css
body {
  background-color: #f4f1ea;
  color: #2b2b2b;
  font-family: system-ui, sans-serif;
}

h1 {
  color: #1f6f4a;
  font-size: 32px;
}

.aviso {
  background-color: #fff3cd;
  padding: 12px;
  border-radius: 8px;
}

Das cinco, padding é a que mais confunde no começo, porque ela não muda cor nenhuma: ela muda tamanho. Medindo a altura real que o aviso ocupa na tela, com e sem a folha:

js
const aviso = document.querySelector('.aviso');
const medir = () => {
  const c = getComputedStyle(aviso);
  console.log(`fundo: ${c.backgroundColor} · padding: ${c.padding} · cantos: ${c.borderRadius}`);
  console.log(`altura ocupada na tela: ${aviso.getBoundingClientRect().height}px`);
};

console.log('--- com final.css ---');
medir();
document.styleSheets[0].disabled = true;
console.log('--- com a folha desligada ---');
medir();
--- com final.css --- fundo: rgb(255, 243, 205) · padding: 12px · cantos: 8px altura ocupada na tela: 42px --- com a folha desligada --- fundo: rgba(0, 0, 0, 0) · padding: 0px · cantos: 0px altura ocupada na tela: 18px

A linha de texto tem 18 pixels de altura. Com 12 de folga em cima e 12 embaixo, a caixa passou a ocupar 42. Uma declaração de uma linha mexeu na geometria da página, e essa conta entre conteúdo, espaço interno e borda é o assunto do box model.

O que vem depois

O próximo passo é o mais prático de todos: descobrir onde esse CSS mora. São três formas de ligar a folha à página — arquivo externo, bloco <style> e atributo style — e elas não são equivalentes, como colocar CSS no HTML mostra. Depois disso vêm os seletores de verdade, e aí a linguagem começa a render.

A ordem inteira de estudo, do primeiro seletor ao layout responsivo, está na trilha de CSS; o mapa do assunto, com o que estudar em cada etapa, fica no guia de CSS.

Prefere aprender em vídeo?

Tem aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • css
  • iniciante
  • seletor
  • estilo
  • navegador

Perguntas frequentes

CSS é uma linguagem de programação?
Não no sentido usual. CSS é declarativo: você descreve o resultado e o navegador decide como chegar lá. Ele tem variáveis (as custom properties, escritas como --cor) e até condições, como @media e @supports, mas não tem laço, não tem função que você chama e nada roda passo a passo como em JavaScript.
Preciso saber HTML antes de aprender CSS?
Precisa do básico. Todo seletor de CSS aponta para alguma coisa do HTML, então sem saber o que é uma tag, uma classe e um id você fica escrevendo endereço para uma casa que não existe. Duas semanas de HTML já bastam para começar.
Por que a minha regra funciona no computador e some no celular?
Quase sempre porque existe outra regra dentro de uma media query sobrescrevendo a sua em telas menores, ou porque o valor foi escrito em pixels fixos e não cabe na largura do aparelho. Abra o DevTools do celular pelo cabo e veja qual regra está riscada no painel Styles.
CSS trava a página enquanto carrega?
Sim, e isso é de propósito. O navegador segura a pintura da tela até terminar de baixar e ler as folhas de estilo do head, para você não ver a página crua por um instante. Por isso folha gigante e desnecessária atrasa o primeiro desenho.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Chromium 151.0.7922.34 headless e Node 24.16.0 com css-tree 3.2.1, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — What is CSS? — developer.mozilla.org
  2. W3C — CSS Cascading and Inheritance Level 5 — w3.org
  3. Chromium — html.css, a folha de estilo do user agent do Blink — chromium.googlesource.com

Continue por aqui