Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
ConceitoIntermediáriocódigo testado

Herança no CSS: o que o filho recebe do pai sem pedir

Quais propriedades atravessam a árvore sozinhas, quais não, e o que inherit, initial e unset fazem quando você precisa forçar a mão.

Rodolfo Mori16 min de leitura

Herança no CSS é o mecanismo que faz um elemento usar o valor do pai quando ninguém declarou nada para ele. Ela não vale para todas as propriedades: color desce, border não. E ela é a última coisa que o navegador consulta — se existir qualquer declaração para aquele elemento, a herança nem entra na conversa.

Os exemplos deste artigo são o site de uma clínica veterinária, a Pet Girassol: ficha do paciente, agenda do dia, cartão de atendimento. Todas as saídas coladas aqui saíram de um Chrome for Testing 151.0.7922.34 dirigido por Playwright, num MacBook com M4 Pro.

Herança é o plano B, não uma regra fraca

O erro de modelo mental mais comum é imaginar a herança como uma regra de peso zero, que entra na disputa e perde para qualquer seletor. Não é isso. A especificidade só compara declarações que existem. A herança acontece depois que essa comparação terminou, e só quando ela terminou de mãos vazias.

A sequência, para uma propriedade e um elemento:

  1. o navegador junta todas as declarações daquela propriedade que casam com o elemento — as suas, as da folha do próprio navegador, as do usuário;
  2. a cascata escolhe uma vencedora;
  3. se não houver nenhuma, e só nesse caso, o navegador pergunta se a propriedade é herdada. Se for, copia o valor do pai. Se não for, usa o valor inicial da propriedade.

Guardar essa ordem resolve sozinha metade dos “por que isso não pegou” do dia a dia. Vamos ver os dois lados na ficha do paciente.

A ficha da clínica: o que desceu e o que ficou para trás

Um único bloco de CSS, aplicado só no container:

html
<style>
  .ficha {
    color: #14532d;
    font-family: Georgia, serif;
    font-size: 18px;
    line-height: 1.6;
    border: 2px solid #14532d;
  }
</style>

<section class="ficha">
  <h2 id="h2">Mel — Golden Retriever</h2>
  <p id="p">Retorno da vacina antirrábica em 12/09.</p>
  <a id="a" href="/prontuario">Ver prontuário</a>
  <button id="button">Confirmar consulta</button>
  <input id="input" value="Tutor: Ana Souza">
</section>

Nenhum dos cinco filhos tem uma linha de CSS própria. Lendo o estilo calculado de cada um:

js
const props = ['color', 'font-family', 'font-size', 'line-height', 'border-top-width'];
const ids = ['h2', 'p', 'a', 'button', 'input'];

const linha = (el) => {
  const cs = getComputedStyle(el);
  return props.map((p) => cs.getPropertyValue(p));
};

const tabela = [
  ['elemento', ...props],
  ['.ficha (pai)', ...linha(document.querySelector('.ficha'))],
  ...ids.map((id) => ['  ' + id, ...linha(document.getElementById(id))]),
];

const larguras = tabela[0].map((_, i) => Math.max(...tabela.map((l) => l[i].length)) + 2);
for (const l of tabela) console.log(l.map((c, i) => c.padEnd(larguras[i])).join('').trimEnd());
elemento color font-family font-size line-height border-top-width .ficha (pai) rgb(20, 83, 45) Georgia, serif 18px 28.8px 2px h2 rgb(20, 83, 45) Georgia, serif 27px 43.2px 0px p rgb(20, 83, 45) Georgia, serif 18px 28.8px 0px a rgb(0, 0, 238) Georgia, serif 18px 28.8px 0px button rgb(0, 0, 0) Arial 13.3333px normal 2px input rgb(0, 0, 0) Arial 13.3333px normal 2px

Cinco leituras dessa tabela:

  • border não desceu para ninguém. O pai tem 2px, os filhos têm 0px. Borda não é propriedade herdada — se fosse, cada parágrafo dentro de um cartão com borda ganharia a própria moldura.
  • font-family e line-height desceram para os três primeiros filhos. O h2, o p e o a estão todos em Georgia, sem que ninguém tenha escrito isso para eles.
  • A color desceu para dois deles, e o link ficou azul. Guarde isso, é a próxima seção.
  • button e input ignoraram tudo. Não é que controles de formulário sejam imunes à herança: é que a folha do navegador declara font e color neles. Havendo declaração, não há herança.
  • O h2 ficou com 27px sem ninguém pedir. A folha do navegador diz h2 { font-size: 1.5em }, e dentro de font-size o em se resolve contra o font-size do pai, nunca contra o do próprio elemento. O pai aqui é o .ficha, com 18px. Herança e unidade relativa trabalham juntas: 18 × 1.5 = 27.

Contei quantas propriedades herdam de verdade

Toda lista de “propriedades herdadas” que circula por aí é copiada de outra lista. Preferi medir. O script abaixo pega o catálogo de propriedades do mdn-data, e para cada uma tenta um valor no pai e verifica se o filho passou a computar o mesmo valor:

js
import { chromium } from 'playwright-core';
import props from 'mdn-data/css/properties.json' with { type: 'json' };

const CHROME = process.env.HOME + '/Library/Caches/ms-playwright/chromium-1234/' +
  'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing';

const lista = Object.entries(props)
  .filter(([nome, d]) => d.status === 'standard' && !nome.startsWith('-'))
  .map(([nome, d]) => ({ nome, mdn: d.inherited === true }));

const browser = await chromium.launch({ executablePath: CHROME });
const page = await browser.newPage();
await page.setContent(`
  <div id="pai"><div id="filho">texto</div></div>
  <div><div id="controle">texto</div></div>
`);

const medir = (solto) => page.evaluate(({ lista, solto }) => {
  // um valor plausível para cada família de propriedade
  const candidatos = ['3px', '3', '30%', 'rgb(1, 2, 3)', 'hidden', 'collapse',
    'italic', 'bold', 'uppercase', 'serif', '"X"', '0.5', '17deg', '1.7s',
    'ease-in', 'thick', 'square', 'crosshair', 'vertical-rl', 'break-all',
    'pre', 'nowrap', 'ellipsis', 'both', 'center', 'flex', 'none', 'auto',
    'scaleX(2)', 'blur(3px)', 'url("a.png")', 'always', 'dark', 'sideways',
    'under', 'wavy', 'balance', 'anywhere', 'contents', 'invert', 'reverse',
    'slice', 'space', 'round', 'stretch', 'each-line'];
  // estas mudam a caixa de todo mundo e envenenariam a medição
  const pular = new Set(['all', 'display', 'position', 'contain', 'content',
    'content-visibility', 'container', 'container-name', 'container-type']);

  const pai = document.getElementById('pai');
  const filho = document.getElementById('filho');
  const controle = document.getElementById('controle');
  for (const el of [filho, controle]) el.style.position = solto ? 'absolute' : 'static';
  const cp = getComputedStyle(pai);
  const cf = getComputedStyle(filho);
  const cc = getComputedStyle(controle);

  const herdam = [], naoHerdam = [], semMedida = [];
  for (const item of lista) {
    if (pular.has(item.nome)) { semMedida.push(item.nome); continue; }
    const base = cc.getPropertyValue(item.nome);   // valor de fábrica, num pai intocado
    if (base === '') { semMedida.push(item.nome); continue; }

    let usado = null;                              // acha um valor que o Chrome aceite
    for (const valor of candidatos) {
      pai.style.setProperty(item.nome, valor);
      if (cp.getPropertyValue(item.nome) !== base) { usado = valor; break; }
      pai.style.removeProperty(item.nome);
    }
    if (usado === null) { semMedida.push(item.nome); continue; }

    const desceu = cf.getPropertyValue(item.nome) === cp.getPropertyValue(item.nome)
      && cf.getPropertyValue(item.nome) !== base;
    (desceu ? herdam : naoHerdam).push(item);
    pai.style.removeProperty(item.nome);
  }
  return { herdam, naoHerdam, semMedida };
}, { lista, solto });

const fluxo = await medir(false);   // filho no fluxo normal
const solta = await medir(true);    // filho em position: absolute

// só vale como herança o que desceu nas duas rodadas
const noAbsoluto = new Set(solta.herdam.map((x) => x.nome));
const confirmadas = fluxo.herdam.filter((x) => noAbsoluto.has(x.nome));
const layout = fluxo.herdam.filter((x) => !noAbsoluto.has(x.nome)).map((x) => x.nome);
const nomes = (arr) => arr.map((x) => x.nome).join(', ') || '(nenhuma)';

console.log('propriedades padronizadas na lista .......', lista.length);
console.log('mediveis (o Chrome respondeu a um valor) .', fluxo.herdam.length + fluxo.naoHerdam.length);
console.log('sem medida possivel ......................', fluxo.semMedida.length);
console.log('desceram com o filho no fluxo ............', fluxo.herdam.length);
console.log('desceram com o filho absoluto ............', solta.herdam.length);
console.log('CONFIRMADAS COMO HERDADAS ................', confirmadas.length);
console.log('descartadas (era efeito de layout) .......', layout.join(', '));
console.log('\nherdam no Chrome, mas o mdn-data diz que nao:', nomes(confirmadas.filter((x) => !x.mdn)));
console.log('o mdn-data diz que herdam, e nao desceram ...:', nomes(lista.filter((x) => x.mdn
  && !confirmadas.some((c) => c.nome === x.nome) && !fluxo.semMedida.includes(x.nome))));

await browser.close();

A segunda rodada, com o filho em position: absolute, existe por um motivo específico: propriedades de tamanho enganam a medição. Se o pai recebe width: 30%, o filho width: auto computa exatamente a mesma largura em pixels — e parece herança, sem ser. Tirando o filho do fluxo, ele encolhe para o conteúdo e a coincidência desaparece. Só entram na lista as propriedades que desceram nos dois cenários.

propriedades padronizadas na lista ....... 484 mediveis (o Chrome respondeu a um valor) . 373 sem medida possivel ...................... 111 desceram com o filho no fluxo ............ 83 desceram com o filho absoluto ............ 81 CONFIRMADAS COMO HERDADAS ................ 81 descartadas (era efeito de layout) ....... inline-size, width

herdam no Chrome, mas o mdn-data diz que nao: user-select o mdn-data diz que herdam, e nao desceram …: interest-delay, interest-delay-end, interest-delay-start, object-position

81 de 373. Pouco mais de um quinto. É por isso que a herança não resolve layout: ela cuida de texto, cor e alguns detalhes de interação, e larga completamente a caixa.

As 81 confirmadas cabem em famílias:

família propriedades confirmadas por que faz sentido descer
fonte font-family, font-size, font-style, font-weight, font-variant, font-kerning, font-size-adjust, font-synthesis, font-palette um bloco de texto é um bloco de texto até o fim
ritmo do texto line-height, letter-spacing, word-spacing, text-indent, text-align, text-transform, tab-size mudam a mancha de texto inteira, não um elemento
quebra de linha white-space, text-wrap, word-break, overflow-wrap, line-break, hyphens, widows, orphans a regra de quebra vale para o parágrafo todo
cor color, accent-color, caret-color, color-scheme tom da região, não do elemento
listas list-style-type, list-style-image a lista aninhada continua sendo lista
tabelas border-collapse, border-spacing são decisões da tabela, aplicadas nas células
interação cursor, pointer-events, visibility, user-select valem para a área, e a área tem filhos
SVG fill, stroke, stroke-width, marker-* um <g> pinta o grupo inteiro

Duas surpresas na saída merecem nota. user-select está marcado como não herdado no mdn-data 2.27.1, mas o Chrome propaga o valor para os filhos — o comportamento é definido na especificação por uma regra própria, e não pela herança normal. E object-position, que o mesmo arquivo marca como herdado, simplesmente não desce:

html
<style>
  .cartao { object-position: 30% 10%; user-select: none; }
</style>
<div class="cartao"><img id="foto" src="x.png" alt=""><span id="texto">Mel</span></div>
pai object-position 30% 10% img object-position 50% 50% pai user-select none span user-select none

Moral prática: quando a dúvida for cara, meça no navegador em que o seu código vai rodar. Tabela é ponto de partida, não sentença.

Volte ao link azul da ficha. A tentação é subir o peso da regra do pai. Quatro rodadas, medindo color no p e no a a cada mudança:

css
/* rodada 1 */  .ficha { color: #14532d; }
/* rodada 2 */  .ficha { color: #14532d !important; }
/* rodada 3 */  .ficha { color: #14532d !important; }
                html body div.ficha a#a { color: #14532d; }
/* rodada 4 */  .ficha { color: #14532d; }
                .ficha a { color: inherit; }
regra normal no pai p: rgb(20, 83, 45) a: rgb(0, 0, 238) com !important no pai p: rgb(20, 83, 45) a: rgb(0, 0, 238) seletor de peso 1-1-3 no filho p: rgb(20, 83, 45) a: rgb(20, 83, 45) color: inherit no filho p: rgb(20, 83, 45) a: rgb(20, 83, 45)

O !important no pai não mudou nada no filho, e não mudaria nunca. Ele decide qual declaração vence no .ficha. O a tem a disputa dele, e nessa disputa a única candidata é a regra do navegador. Não existe declaração do autor para comparar, então não existe nada para o !important ganhar.

Dá para confirmar quem declarou, perguntando ao próprio motor do Chrome pelo protocolo de depuração — é a mesma informação que o painel Styles do DevTools mostra:

js
import { chromium } from 'playwright-core';

const CHROME = process.env.HOME + '/Library/Caches/ms-playwright/chromium-1234/' +
  'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing';

const browser = await chromium.launch({ executablePath: CHROME });
const page = await browser.newPage();
await page.setContent(`
  <style> .ficha { color: #14532d } .ficha a { color: inherit } </style>
  <div class="ficha"><a id="a" href="/prontuario">Ver prontuário</a></div>
`);

const cdp = await page.context().newCDPSession(page);
await cdp.send('DOM.enable');
await cdp.send('CSS.enable');
const { root } = await cdp.send('DOM.getDocument');
const { nodeId } = await cdp.send('DOM.querySelector', { nodeId: root.nodeId, selector: '#a' });
const m = await cdp.send('CSS.getMatchedStylesForNode', { nodeId });

const cor = (r) => r.rule.style.cssProperties.find((p) => p.name === 'color');
for (const r of m.matchedCSSRules ?? []) {
  if (cor(r)) console.log(`origem=${r.rule.origin.padEnd(10)} seletor=${r.rule.selectorList.text.padEnd(24)} color: ${cor(r).value}`);
}
for (const h of m.inherited ?? []) {          // os ancestrais vêm num grupo à parte
  for (const r of h.matchedCSSRules ?? []) {
    if (cor(r)) console.log(`herdado  origem=${r.rule.origin.padEnd(10)} seletor=${r.rule.selectorList.text.padEnd(15)} color: ${cor(r).value}`);
  }
}

await browser.close();
origem=user-agent seletor=a:-webkit-any-link color: -webkit-link origem=regular seletor=.ficha a color: inherit herdado origem=regular seletor=.ficha color: #14532d

A leitura foi feita com o CSS da quarta rodada no lugar, e as três linhas contam a história inteira. A primeira é a culpada: a:-webkit-any-link { color: -webkit-link }, origem user-agent. Nas rodadas 1 e 2 ela estava sozinha na lista, e por isso ganhava sem esforço. A segunda linha é a correção que entrou na rodada 4 — uma declaração do autor, origem regular, que agora ocupa a mesma vaga e vence. A terceira aparece num grupo à parte, rotulado como herdado: é a regra do .ficha, que o Chrome guarda como candidata a fornecer o valor por herança.

O que desce é o valor computado, não o que você escreveu

Herança copia o valor computado do pai — o valor já resolvido, com as unidades relativas transformadas em absolutas. Não copia o que está escrito na folha de estilo. A diferença aparece na hora que você usa porcentagem.

Na agenda da clínica, cada nível aninhado pede 80% da fonte:

css
.agenda { font-size: 20px; }
.agenda .grupo { font-size: 80%; }
html
<div class="agenda" id="n0">Agenda da semana
  <div class="grupo" id="n1">Segunda
    <div class="grupo" id="n2">09:00 Mel — vacina
      <div class="grupo" id="n3">observação do tutor</div>
    </div>
  </div>
</div>
n0 especificado: 20px computado: 20px n1 especificado: 80% computado: 16px n2 especificado: 80% computado: 12.8px n3 especificado: 80% computado: 10.24px

Três níveis e o texto virou 10 pixels. Cada 80% é calculado sobre o computado do pai, que já era 80% do avô. Porcentagem de fonte acumula, e o efeito é multiplicativo — este é o motivo pelo qual rem, que sempre se resolve contra a raiz, ganhou o dia a dia. As unidades do CSS merecem uma leitura só delas por causa disso.

O mesmo mecanismo produz o bug clássico do line-height:

css
.porcento { font-size: 20px; line-height: 150%; }
.numero   { font-size: 20px; line-height: 1.5; }
.titulo   { font-size: 40px; }
html
<div class="porcento"><p class="titulo" id="p1">Mel — Golden Retriever</p></div>
<div class="numero"><p class="titulo" id="p2">Mel — Golden Retriever</p></div>
line-height: 150% pai: font-size 20px / line-height 30px filho: font-size 40px / line-height 30px altura da linha renderizada: 30px line-height: 1.5 pai: font-size 20px / line-height 30px filho: font-size 40px / line-height 60px altura da linha renderizada: 60px

Com 150%, o computado do pai é 30px, e é esse 30px que desce. O título de 40 pixels ficou com linha de 30 — a letra é maior que a linha, e as linhas se encavalam. Com 1.5, o valor computado continua sendo o número 1.5; cada descendente multiplica pelo próprio font-size e o título chega a 60px.

inherit, initial, unset e revert: forçar a mão

As quatro palavras-chave existem justamente para você intervir em cada etapa daquele fluxo de decisão. Elas funcionam em qualquer propriedade, herdada ou não. O teste: um link dentro da ficha, com uma regra do autor pintando color e background-color, e cada palavra-chave aplicada por cima.

css
.ficha   { color: #14532d; background-color: #ecfdf5; }
.ficha a { color: #b91c1c; background-color: #fee2e2; }

/* e, em cada rodada, uma destas: */
.ficha a#alvo { color: inherit; background-color: inherit; }
.ficha a#alvo { color: initial; background-color: initial; }
.ficha a#alvo { color: unset;   background-color: unset;   }
.ficha a#alvo { color: revert;  background-color: revert;  }
palavra-chave color background-color (sem nada) rgb(185, 28, 28) rgb(254, 226, 226) inherit rgb(20, 83, 45) rgb(236, 253, 245) initial rgb(0, 0, 0) rgba(0, 0, 0, 0) unset rgb(20, 83, 45) rgba(0, 0, 0, 0) revert rgb(0, 0, 238) rgba(0, 0, 0, 0)

Leia a linha do unset com atenção: a mesma palavra produziu resultados diferentes nas duas propriedades. color é herdada, então unset virou inherit e pegou o verde do pai. background-color não é herdada, então unset virou initial e ficou transparente. É a única das quatro que muda de comportamento conforme a propriedade.

E o revert na cor do link é o detalhe que separa quem entendeu de quem decorou: rgb(0, 0, 238) é o azul de link do navegador. Ele desfez só o CSS do autor — inclusive a regra .ficha a — e deixou de pé o que a folha padrão declara. unset, na mesma propriedade e no mesmo elemento, ignorou essa folha e foi buscar o verde no pai.

palavra-chave o que faz resultado no link do teste
inherit copia o valor computado do pai, seja a propriedade herdada ou não verde do .ficha
initial usa o valor inicial da propriedade, definido na especificação preto e transparente
unset inherit se a propriedade é herdada, initial se não é verde e transparente
revert apaga as declarações do autor e devolve o valor do navegador azul de link e transparente
revert-layer apaga só a camada @layer atual, mantendo as anteriores depende das camadas

A mesma diferença fica gritante com o atalho all, que aplica a palavra-chave a todas as propriedades de uma vez. Num botão que o autor já tinha estilizado:

css
.barra { font-family: Georgia, serif; font-size: 18px; color: #14532d; }
.barra button { font: inherit; color: #14532d; background: none; border: 0; padding: 0; }
html
<div class="barra">
  <button id="b1">Confirmar</button>
  <button id="b2" style="all: revert">Confirmar</button>
  <button id="b3" style="all: unset">Confirmar</button>
</div>
sem nada color: rgb(20, 83, 45) font-family: Georgia, serif border: 0px background: rgba(0, 0, 0, 0) all: revert color: rgb(0, 0, 0) font-family: Arial border: 2px background: rgb(239, 239, 239) all: unset color: rgb(20, 83, 45) font-family: Georgia, serif border: 0px background: rgba(0, 0, 0, 0)

all: revert devolveu o botão cinza de fábrica, com borda e fundo. all: unset fez o oposto: apagou tudo, inclusive o que o navegador tinha posto, e deixou o botão herdando a fonte e a cor do pai. Quando você quer transformar um botão em texto puro, all: unset é o caminho — mas ele leva junto o anel de foco: no mesmo teste, o botão intacto computa outline: auto 1px rgb(0, 95, 204) quando recebe foco pelo teclado, e o botão com all: unset computa outline: none. Reponha o :focus-visible na mão, e um cursor: pointer se quiser o ponteiro.

Três atalhos que a herança te dá de graça

A herança não é só um comportamento a ser contornado. Três padrões vivem dela.

currentColor empresta a cor para propriedades que não herdam. Borda não é herdada, mas border-color: currentColor faz a borda seguir o color — que é herdado. Variáveis CSS são herdadas por padrão, então redefinir uma no meio da árvore muda o subconjunto inteiro abaixo. Os dois juntos:

css
.agenda  { --acento: #14532d; color: var(--acento); }
.cartao  { border: 2px solid currentColor; border-left: 6px solid var(--acento); }
.urgente { --acento: #b91c1c; color: var(--acento); }
html
<div class="agenda">
  <div class="cartao" id="c1">09:00 Mel — vacina</div>
  <div class="urgente">
    <div class="cartao" id="c2">10:30 Thor — sutura
      <div class="cartao" id="c3">observação do tutor</div>
    </div>
  </div>
</div>
c1 --acento: #14532d color: rgb(20, 83, 45) border-top-color: rgb(20, 83, 45) border-left-color: rgb(20, 83, 45) c2 --acento: #b91c1c color: rgb(185, 28, 28) border-top-color: rgb(185, 28, 28) border-left-color: rgb(185, 28, 28) c3 --acento: #b91c1c color: rgb(185, 28, 28) border-top-color: rgb(185, 28, 28) border-left-color: rgb(185, 28, 28)

Nenhum dos três cartões declara color, border-top-color nem border-left-color: os três valores chegaram de graça, e o .urgente repintou o cartão, a borda e o cartão aninhado dentro dele.

Só que o crédito não é todo da variável, e vale desmontar — porque é aqui que muita gente escreve o tema e não entende por que metade não pegou. O .urgente tem duas declarações. Tirando a segunda e deixando só a variável:

css
.urgente { --acento: #b91c1c; }   /* sem o color: var(--acento) */
c1 --acento: #14532d color: rgb(20, 83, 45) border-top-color: rgb(20, 83, 45) border-left-color: rgb(20, 83, 45) c2 --acento: #b91c1c color: rgb(20, 83, 45) border-top-color: rgb(20, 83, 45) border-left-color: rgb(185, 28, 28) c3 --acento: #b91c1c color: rgb(20, 83, 45) border-top-color: rgb(20, 83, 45) border-left-color: rgb(185, 28, 28)

A variável desceu — --acento está vermelho em c2 e c3 —, e mesmo assim o color continuou verde. O motivo é a regra da seção anterior: o que a herança copia é o valor computado. O var(--acento) do .agenda foi resolvido lá em cima, virou verde ali, e é verde que desce — não “o que --acento valer aqui”. Só o border-left-color mudou sozinho, porque o var(--acento) dele está no .cartao e é resolvido de novo em cada cartão.

A regra prática: redefinir a variável muda tudo que lê a variável naquele nível para baixo. Uma propriedade que já resolveu o var() num ancestral precisa da linha que relê — e é essa linha que arrasta o currentColor da borda junto. É o mecanismo que sustenta tema claro e escuro sem duplicar folha de estilo, e o assunto da lição de variáveis CSS.

Pseudo-elementos herdam do elemento que os originou. ::before não tem pai no HTML, mas herda do elemento em que foi criado:

css
.agenda { color: #14532d; font-style: italic; letter-spacing: 2px; }
.cartao::before { content: "▸ "; }
color .cartao: rgb(20, 83, 45) .cartao::before: rgb(20, 83, 45) font-style .cartao: italic .cartao::before: italic letter-spacing .cartao: 2px .cartao::before: 2px

O marcador sai verde, itálico e com o mesmo espaçamento, sem uma linha a mais. As pseudo-classes e pseudo-elementos têm mais detalhes sobre onde esses elementos fantasmas se encaixam.

Herança segue a árvore de elementos, não o que aparece na tela

Herança é definida sobre a árvore do documento — a mesma que o navegador monta a partir do seu HTML. Ela não tem nada a ver com o que está visível. A prova mais limpa disso é comparar visibility, que é herdada, com display, que não é:

css
.escondido { visibility: hidden; }
.escondido .aviso { visibility: visible; }

.sumido { display: none; }
.sumido .aviso { display: block; }
html
<div class="escondido">bloco<span class="aviso" id="v1">vaga cancelada</span></div>
<div class="sumido">bloco<span class="aviso" id="v2">vaga cancelada</span></div>
pai visibility: hidden visibility: visible display: inline caixa renderizada: 97.265625x18 pai display: none visibility: visible display: block caixa renderizada: 0x0

No primeiro caso o filho reapareceu: o pai passou hidden por herança, e uma declaração no filho sobrescreveu esse valor herdado — herança é sobrescrevível, como qualquer valor. No segundo caso não há herança nenhuma para desfazer. display: none no pai remove a subárvore inteira da renderização, e o display: block do filho, embora tenha vencido a cascata dele, não tem onde ser desenhado. Repare que o filho computa display: block mesmo assim — o valor existe, a caixa é que não.

O conserto de duas linhas que todo projeto precisa

Volte à primeira tabela: button e input em Arial 13.3333px, pretos, no meio de uma página em Georgia 18px verde. Isso não é bug do navegador, é a folha padrão dele fazendo o trabalho dela. Mas quase nunca é o que você quer.

css
body { font-family: Georgia, serif; font-size: 18px; color: #14532d; }

.consertado input,
.consertado button,
.consertado select,
.consertado textarea {
  font: inherit;
  color: inherit;
}
input cru font-family: Arial font-size: 13.3333px color: rgb(0, 0, 0) button cru font-family: Arial font-size: 13.3333px color: rgb(0, 0, 0) input consertado font-family: Georgia, serif font-size: 18px color: rgb(20, 83, 45) button consertado font-family: Georgia, serif font-size: 18px color: rgb(20, 83, 45)

Duas declarações, quatro seletores, e os formulários passam a fazer parte da página. font: inherit é o atalho: ele resolve família, tamanho, peso, estilo e altura de linha de uma vez. Essas linhas estão em praticamente todo reset de CSS sério, e agora você sabe exatamente por quê — elas não estão “estilizando” o formulário, estão abrindo a vaga para a herança entrar.

O caminho de um valor, do CSS até o pixel

Juntando tudo, o percurso de uma propriedade num elemento:

declarações que casam com o elemento cascata: origem, camada, especificidade, ordem alguma declaração venceu? a propriedade é herdada? o valor que venceu valor computado do pai valor inicial da propriedade valor especificado → computado → usado não sim sim não

Repare que a herança fica num galho só do desenho, e que esse galho começa numa resposta negativa. Toda vez que uma propriedade “não está herdando”, a resposta está lá em cima: alguma declaração venceu a cascata e desviou o fluxo para a direita.

Descobrir na hora quem passou o valor

O painel Computed do DevTools já mostra a origem de cada valor. Mas dá para responder pela linha de comando com uma função de sete linhas: subir a árvore enquanto o pai computar o mesmo valor. O último ancestral que ainda bate é onde o valor nasceu.

js
function origemDoValor(el, prop) {
  const valor = getComputedStyle(el).getPropertyValue(prop);
  let dono = el;
  while (dono.parentElement &&
    getComputedStyle(dono.parentElement).getPropertyValue(prop) === valor) {
    dono = dono.parentElement;
  }
  const nome = dono.tagName.toLowerCase() + (dono.className ? '.' + dono.className : '');
  const onde = dono === el ? 'definido neste elemento' : `herdado de <${nome}>`;
  return `${prop.padEnd(12)} ${valor.padEnd(24)} ${onde}`;
}

const alvo = document.querySelector('.observacao');
for (const p of ['color', 'line-height', 'font-style', 'font-family', 'padding-top']) {
  console.log(origemDoValor(alvo, p));
}

Rodando na estrutura da agenda — body > main.pagina > section.agenda > article.cartao > p.observacao, com color e line-height só no .agenda, font-style só no .observacao e a fonte lá no body:

color rgb(20, 83, 45) herdado de <section.agenda> line-height 25.6px herdado de <section.agenda> font-style italic definido neste elemento font-family system-ui, sans-serif herdado de <body> padding-top 0px definido neste elemento

Cole essa função no console e você para de adivinhar de onde vem a cor cinza que apareceu no rodapé. É uma heurística, não uma prova, e ela erra de dois jeitos: se dois ancestrais declararem o mesmo valor por coincidência, ela aponta o de cima; e valor que ninguém declarou aparece como “definido neste elemento” — foi o que aconteceu com o padding-top: 0px da última linha, que é só o valor inicial da propriedade. Para o dia a dia, resolve; para a prova, o painel Styles mostra a regra com nome e arquivo.

O que vem depois

Herança e cascata são os dois mecanismos que decidem tudo no CSS, e agora você tem os dois no lugar certo da fila: a cascata escolhe entre o que existe, a herança preenche o que não existe. Se a parte de “o que existe” ainda está nebulosa, a lição de especificidade no CSS destrincha o desempate declaração por declaração — e a de o que é CSS mostra a folha do navegador que apareceu aqui várias vezes.

Depois disso, o próximo passo é layout, onde a herança não ajuda em nada e cada propriedade precisa ser declarada onde é usada. O guia de CSS tem a ordem completa de estudo, da primeira folha de estilo até Grid.

Prefere aprender em vídeo?

Tem uma aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • css
  • heranca
  • cascata
  • computed style
  • currentColor

Perguntas frequentes

Herança é a mesma coisa que cascata?
Não. A cascata escolhe entre declarações que existem para aquele elemento. A herança só é consultada quando a cascata termina sem nenhuma declaração — ela não disputa, ela preenche o vazio.
Por que a cor do link não muda quando eu pinto o container?
Porque a folha de estilo do navegador declara color diretamente no elemento a. Existindo declaração, a herança nem é consultada. A correção é declarar você também, com .container a { color: inherit; }.
opacity: 0.5 no pai deixa o filho translúcido. Isso é herança?
Não. Medindo no Chrome, o filho continua com opacity 1. O que acontece é que o pai vira um grupo de composição: o navegador desenha a subárvore inteira e só então aplica os 50% no resultado. Por isso não existe filho que "volte" a ser opaco.
Herança atravessa um iframe?
Não. Cada documento tem a própria árvore e a própria cascata, então o CSS da página de fora não chega ao conteúdo do iframe por herança nem por seletor. Dentro de um shadow DOM é diferente: propriedades herdadas atravessam a fronteira normalmente.
Por que line-height sem unidade é a recomendação?
Porque o valor sem unidade desce como número e é multiplicado pelo font-size de cada descendente. Com 150% ou 1.5em, o que desce já é o resultado em pixels calculado no ancestral, e um filho com fonte maior herda uma altura de linha pequena demais.
Variável CSS herda?
Herda por padrão, e é isso que permite redefinir --acento num trecho da página e ver todo aquele subconjunto mudar. A exceção é a custom property registrada com @property e inherits: false: aí o filho ignora o valor do pai e usa o initial-value declarado.

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 Chrome for Testing 151.0.7922.34 via Playwright 1.62.1 no Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — Inheritance — developer.mozilla.org
  2. CSS Cascading and Inheritance Level 5 — Inheritance — w3.org
  3. MDN — Value definition: inherit, initial, unset, revert — developer.mozilla.org
  4. HTML Standard — Rendering (a folha de estilo padrão do navegador) — html.spec.whatwg.org

Continue por aqui