Como colocar CSS no HTML: link, style e inline
As três formas de ligar CSS a uma página HTML, qual delas vence quando as três aparecem juntas e por que o link no head continua sendo a resposta.
Existem três formas de ligar CSS a uma página HTML: um arquivo .css separado,
chamado por <link rel="stylesheet"> no <head>; um bloco <style> escrito
dentro do próprio HTML; e o atributo style colado no elemento. As três
funcionam e produzem exatamente os mesmos pixels. Só a primeira serve para um
site com mais de uma página.
Todos os exemplos aqui são de uma livraria, a Página Dobrada, que mostra um
cartão de livro com título e preço. Para não medir no escuro, servi cada página
por um servidor local em Node 24.16.0 que atrasa cada arquivo CSS em 80
milissegundos de propósito — rede de verdade não é instantânea — e abri tudo no
Chromium 151 pelo Playwright, lendo getComputedStyle e a API de performance do
próprio navegador. Todo número desta página saiu dessa medição.
Se você ainda não viu o que é CSS e como o navegador aplica estilo na página, comece por lá: aqui a pergunta já é outra, é como fazer o arquivo chegar até a página.
O arquivo separado, chamado por <link> no head
Duas coisas em disco: a página e a folha de estilo.
find . -name so-link.html -o -name estilo.css -o -name catalogo.html | sortO CSS da livraria fica em css/estilo.css, sem nenhuma tag HTML dentro — é um
arquivo de regras, e só:
body {
font-family: system-ui, sans-serif;
background: #fdfaf3;
color: #2b2b2b;
margin: 0;
padding: 24px;
}
.livro {
border: 1px solid #d8cfc0;
padding: 16px;
max-width: 420px;
}
.preco {
color: rgb(20, 120, 60);
font-weight: 700;
}A página chama esse arquivo com uma linha dentro do <head>:
<!doctype html>
<html lang="pt-br">
<head>
<meta charset="utf-8" />
<title>Livraria Página Dobrada</title>
<link rel="stylesheet" href="/css/estilo.css" />
</head>
<body>
<article class="livro">
<h2>Grande Sertão: Veredas</h2>
<p class="preco">R$ 74,90</p>
</article>
</body>
</html>Abri essa página no Chromium e perguntei ao navegador o que ele calculou para o preço e qual a largura final do cartão:
Três coisas na linha do <link> importam. rel="stylesheet" diz que tipo de
relação aquele arquivo tem com a página — e sem ele o navegador nem chega a
pedir o arquivo. href é o caminho até ele. E o lugar, dentro do <head>, é
onde o navegador espera encontrar as instruções de apresentação antes de
desenhar qualquer coisa. Se a divisão entre <head> e <body> ainda não
estiver clara, vale revisar a estrutura de uma página
HTML.
A largura 454px não é mágica: são os 420px de max-width, mais 16px de padding
de cada lado, mais 1px de borda de cada lado. É o box model aparecendo pela
primeira vez.
O bloco <style>: o mesmo CSS morando dentro do HTML
As regras do cartão podem ser escritas dentro do próprio HTML, entre <style> e
</style>, também no <head>:
<head>
<meta charset="utf-8" />
<title>Livraria Página Dobrada</title>
<style>
.livro {
border: 1px solid #d8cfc0;
padding: 16px;
max-width: 420px;
}
.preco {
color: rgb(20, 120, 60);
font-weight: 700;
}
</style>
</head>Resultado idêntico. A diferença é de logística: esse CSS existe só nesta página. Se a livraria tiver dez páginas, você copia o bloco dez vezes e passa a manter dez cópias. Muda a cor do preço uma vez e você mudou em um lugar só — nos outros nove o preço continua verde antigo.
Isso não torna o <style> inútil. Ele tem um uso legítimo e específico: o CSS
mínimo da primeira dobra da tela, embutido na própria página para não depender
de nenhum arquivo externo. É a técnica que os times de performance chamam de
CSS crítico, e ela existe justamente por causa do bloqueio que a gente vai
medir mais adiante.
O atributo style: o estilo que não se reaproveita
A terceira forma dispensa seletor. Você escreve as declarações direto no elemento:
<article class="livro" style="border: 1px solid #d8cfc0; padding: 16px; max-width: 420px">
<h2>Grande Sertão: Veredas</h2>
<p class="preco" style="color: rgb(20, 120, 60); font-weight: 700">R$ 74,90</p>
</article>Três arquivos, três mecanismos, exatamente a mesma medida na tela. Guarde essa igualdade: ela mostra que a escolha entre as três não é sobre aparência. É sobre o que acontece depois — quantos lugares você precisa editar, o que o navegador consegue guardar em cache e quem vence quando duas regras discordam.
O atributo style tem um problema estrutural: ele não tem seletor, então não
tem como ser reaproveitado. Cada preço da loja carrega a própria cópia da cor.
E, como você vai ver agora, ele ganha quase todas as brigas — o que parece bom
até o dia em que você precisa mudar alguma coisa a partir da folha externa.
Quando as três aparecem na mesma página
Aqui está a experiência que decide a discussão. Uma página com as três origens
disputando a mesma propriedade, cada uma com uma cor diferente: verde na folha
externa, vermelho no bloco <style>, azul no atributo.
<head>
<link rel="stylesheet" href="/css/estilo.css" />
<!-- .preco { color: rgb(20, 120, 60) } -->
<style>
.preco {
color: rgb(180, 40, 40);
}
</style>
</head>
<body>
<p class="preco" style="color: rgb(30, 60, 200)">R$ 74,90</p>
</body>E o que o navegador respondeu quando perguntei a cor calculada:
Azul. O atributo style venceu as duas. Ele não venceu por ser “mais forte” no
sentido de estar mais perto do elemento, e sim porque, na ordem da cascata, o
estilo inline entra num degrau acima de qualquer regra com seletor. É o mesmo
assunto de especificidade no CSS, visto do lado
de fora: antes de comparar seletores, o navegador compara origens.
Empate entre link e style se decide pela ordem
Tire o atributo inline da jogada e sobram duas regras com o mesmo seletor,
.preco. Mesma especificidade, mesma origem. Quem ganha?
Montei duas páginas iguais, trocando só a ordem das duas linhas no <head>:
<!-- ordem-a.html -->
<link rel="stylesheet" href="/css/estilo.css" />
<style>
.preco {
color: rgb(180, 40, 40);
}
</style>
<!-- ordem-b.html -->
<style>
.preco {
color: rgb(180, 40, 40);
}
</style>
<link rel="stylesheet" href="/css/estilo.css" />Ganhou quem estava por último, nas duas vezes. Não existe “o <style> é mais
forte que o <link>”: em caso de empate, o que decide é a posição no documento.
Isso explica por que o CSS de um framework precisa ser carregado antes do seu
arquivo — e por que muita gente conserta o próprio bug só arrastando uma linha
para baixo.
O !important inverte até o inline
Uma exceção que vale conhecer, e usar pouco. Marquei a regra da folha externa
com !important e deixei o atributo style azul na página:
/* css/promocao.css */
.preco {
color: rgb(20, 120, 60) !important;
}<link rel="stylesheet" href="/css/promocao.css" />
<p class="preco" style="color: rgb(30, 60, 200)">R$ 74,90</p>Verde. Uma declaração !important numa folha externa passa por cima de um
atributo style normal. É a saída para quando um estilo inline vem de um script
de terceiros que você não controla — e é também a razão de o !important virar
bola de neve num projeto: assim que o primeiro entra, o próximo ajuste precisa de
outro.
@import cobra uma viagem a mais, e dá para medir
Existe uma quarta forma de puxar CSS, escrita dentro do próprio CSS:
/* css/base-com-import.css */
@import url("tema.css");
body {
font-family: system-ui, sans-serif;
margin: 0;
padding: 24px;
}Ela funciona, e é justamente aí que mora a armadilha: funciona igual e custa
mais caro. Para mostrar o quanto, montei duas páginas que carregam exatamente os
mesmos dois arquivos. Uma com dois <link>, outra com um <link> que faz
@import do segundo:
<!-- dois-links.html -->
<link rel="stylesheet" href="/css/base.css" />
<link rel="stylesheet" href="/css/tema.css" />
<!-- com-import.html -->
<link rel="stylesheet" href="/css/base-com-import.css" />Medi com o Playwright, lendo performance.getEntriesByType('resource') para
saber quando cada CSS foi pedido e quando ficou pronto, e
first-contentful-paint para saber quando a página apareceu:
import { chromium } from 'playwright-core';
const navegador = await chromium.launch();
const pagina = await navegador.newPage();
await pagina.goto('http://localhost:4300/dois-links.html', { waitUntil: 'load' });
await pagina.waitForFunction(() => performance.getEntriesByType('paint').length > 0);
const dados = await pagina.evaluate(() => {
const css = performance
.getEntriesByType('resource')
.filter((r) => r.name.endsWith('.css'))
.map((r) => ({
nome: new URL(r.name).pathname,
inicio: Math.round(r.startTime),
fim: Math.round(r.responseEnd),
}));
const fcp = performance.getEntriesByName('first-contentful-paint')[0];
return { css, fcp: Math.round(fcp.startTime) };
});
console.log(dados);
await navegador.close();Rodando isso nas duas páginas, cinco execuções de cada e descartando a primeira como aquecimento:
com-import.html /css/base-com-import.css pedido em 4ms · pronto em 86ms /css/tema.css pedido em 86ms · pronto em 168ms primeira pintura: 176ms, 184ms, 180ms, 176ms, 176ms → média 178ms
Leia a coluna “pedido em”. Com dois <link>, os dois arquivos foram pedidos no
mesmo milissegundo: o navegador leu o HTML, viu as duas linhas e disparou os dois
downloads juntos. Com @import, o segundo arquivo só foi pedido aos 86ms — ou
seja, o navegador precisou baixar e ler o primeiro CSS para só então
descobrir que existia um segundo. Uma viagem depois da outra, em fila.
Resultado: 100ms contra 178ms de primeira pintura, na mesma máquina e com o mesmo atraso de rede simulado. Setenta e oito milissegundos que existem só por causa da forma escolhida para incluir o arquivo.
O CSS trava a pintura da página, e isso é proposital
Você deve ter reparado que a página só apareceu depois que o CSS ficou pronto. Não é lentidão: é uma decisão do navegador. Ele segura o desenho até ter o estilo, porque pintar texto preto sem formatação e repintar dois quadros depois é pior para quem está olhando.
Para medir o tamanho desse efeito, coloquei um atraso de 600ms num CSS e comparei com uma página sem CSS nenhum:
<!-- lenta.html: o servidor segura este arquivo por 600ms -->
<link rel="stylesheet" href="/lento/estilo.css" />A página em branco pintou em 13ms; a mesma página esperando o CSS levou 622ms.
O navegador ficou parado o tempo inteiro do download. É o comportamento inverso
do que acontece com script, onde defer e async existem justamente para tirar
o JavaScript do caminho — vale comparar com como o HTML bloqueia o
carregamento.
Agora o detalhe que quase ninguém sabe: o bloqueio depende do atributo media.
Se a folha declara que só vale para impressão, o navegador não tem motivo para
esperar por ela antes de pintar a tela.
<!-- o mesmo arquivo de 600ms, agora declarado só para impressão -->
<link rel="stylesheet" href="/lento/estilo.css" media="print" />Mesmo arquivo, mesmo atraso de 600ms, e a pintura caiu de 619ms para 13ms. O
media não é enfeite: ele é a informação que o navegador usa para decidir se
aquela folha vale a pena esperar.
O caminho errado: o 404 que quase ninguém vê
Este é o erro número um de quem está começando, e ele não estoura na sua cara. A
página paginas/catalogo.html mora numa subpasta e chama o CSS assim:
<!-- site/paginas/catalogo.html -->
<link rel="stylesheet" href="css/estilo.css" />Sem a barra inicial, o caminho é relativo à pasta da página. Como a página
está em /paginas/, o navegador foi procurar em /paginas/css/estilo.css, que
não existe:
curl -sI http://localhost:4300/paginas/css/estilo.css | grep -i HTTPE, na página, o resultado foi este:
Repare no que o navegador fez: ele não removeu a folha. link.sheet
continua sendo um objeto, e document.styleSheets.length continua valendo 1. O
que ficou vazio foi o número de regras. Para quem só olha a tela, o sintoma é
“o CSS não está pegando”; a causa está no
404 no console.
A correção é escolher um caminho consciente. Começando com barra (/css/estilo.css),
o caminho parte da raiz do site e vale igual para qualquer página, em qualquer
subpasta. Sem barra, ele parte da pasta da página atual — e ../css/estilo.css
sobe um nível antes de procurar.
Existe uma variação pior, porque é silenciosa. Servi o mesmo CSS, byte por byte,
com o cabeçalho content-type errado:
curl -sI http://localhost:4300/texto/estilo.css | grep -i "HTTP\|content-type"Status 200, arquivo entregue, zero regras aplicadas e nenhuma mensagem no
console — capturei o log do navegador pelo protocolo do DevTools e não veio
nada. O Chromium recusa aplicar como estilo um arquivo que chega anunciado como
texto puro. Você só descobre olhando o content-type da resposta.
Confira em dez segundos se a folha chegou
Antes de sair mexendo em seletor, prove que o arquivo chegou. Abra o console do navegador e rode esta linha:
[...document.styleSheets].map((f) => [f.href ?? '<style> no HTML', f.cssRules.length]);Numa página saudável, cada folha aparece com o endereço e a quantidade de regras que ela realmente carregou:
E na página com o caminho quebrado, a folha aparece — com zero regras:
É o diagnóstico mais rápido que existe para “o CSS não está funcionando”. Zero
regras quer dizer que o problema é de entrega: caminho, status ou content-type.
Número maior que zero quer dizer que a folha chegou inteira e o problema é outro
— seletor errado, especificidade ou herança.
Sua missão: quebrar o caminho de propósito
Pegue qualquer página sua que já tenha CSS externo funcionando e faça três
coisas, nesta ordem. Primeiro, apague uma letra do href e recarregue: confirme
no console que a folha ainda aparece em document.styleSheets, agora com zero
regras. Segundo, conserte o caminho e adicione um bloco <style> antes do
<link> mudando a cor de um texto — veja a folha externa ganhar. Terceiro,
arraste o bloco <style> para depois do <link> e veja a cor virar. Se você
consegue prever as três telas antes de recarregar, a cascata deixou de ser
mistério.
Com o CSS chegando na página, o próximo passo é mirar nos elementos certos: é o assunto dos seletores, na sequência da trilha de CSS. O guia completo de CSS mostra a ordem inteira e onde cada assunto entra.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Posso ter mais de um link de CSS na mesma página?
O que acontece se eu esquecer o rel="stylesheet"?
O link precisa mesmo ficar dentro do head?
Ainda preciso escrever type="text/css" no link?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Chromium 151.0.7922.34 via Playwright, servido por Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — O elemento <link> — developer.mozilla.org
- MDN — Cascata, especificidade e herança — developer.mozilla.org
- CSS Cascading and Inheritance Level 5 — Cascade Sorting Order — w3.org



