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

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.

Rodolfo Mori11 min de leitura

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.

Duas coisas em disco: a página e a folha de estilo.

bash
find . -name so-link.html -o -name estilo.css -o -name catalogo.html | sort
./css/estilo.css ./paginas/catalogo.html ./so-link.html

O CSS da livraria fica em css/estilo.css, sem nenhuma tag HTML dentro — é um arquivo de regras, e só:

css
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>:

html
<!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:

so-link.html → rgb(20, 120, 60) · peso 700 · caixa do livro 454px

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

html
<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>
so-style.html → rgb(20, 120, 60) · peso 700 · caixa do livro 454px

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:

html
<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>
so-inline.html → rgb(20, 120, 60) · peso 700 · caixa do livro 454px

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.

html
<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:

tres-origens.html → color: rgb(30, 60, 200)

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.

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

html
<!-- 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" />
ordem-a.html → color: rgb(180, 40, 40) ordem-b.html → color: rgb(20, 120, 60)

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
/* css/promocao.css */
.preco {
  color: rgb(20, 120, 60) !important;
}
html
<link rel="stylesheet" href="/css/promocao.css" />
<p class="preco" style="color: rgb(30, 60, 200)">R$ 74,90</p>
importante.html → color: rgb(20, 120, 60)

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
/* 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:

html
<!-- 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:

js
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:

dois-links.html /css/base.css pedido em 4ms · pronto em 89ms /css/tema.css pedido em 4ms · pronto em 90ms primeira pintura: 96ms, 108ms, 96ms, 100ms, 100ms → média 100ms

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.

dois <link> no head primeira pintura · 100 ms base.css tema.css @import dentro do CSS primeira pintura · 178 ms base-com-import.css tema.css 0 100 ms 180 ms

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:

html
<!-- lenta.html: o servidor segura este arquivo por 600ms -->
<link rel="stylesheet" href="/lento/estilo.css" />
sem-css.html primeira pintura: 12ms, 12ms, 16ms, 12ms, 12ms → média 13ms lenta.html primeira pintura: 624ms, 624ms, 620ms, 620ms, 620ms → média 622ms

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.

html
<!-- o mesmo arquivo de 600ms, agora declarado só para impressão -->
<link rel="stylesheet" href="/lento/estilo.css" media="print" />
lenta.html primeira pintura: 620ms, 620ms, 620ms, 616ms, 620ms → média 619ms impressao.html primeira pintura: 12ms, 12ms, 12ms, 12ms, 16ms → média 13ms

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:

html
<!-- 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:

bash
curl -sI http://localhost:4300/paginas/css/estilo.css | grep -i HTTP
HTTP/1.1 404 Not Found

E, na página, o resultado foi este:

[rede] 404 /paginas/css/estilo.css [console] Failed to load resource: the server responded with a status of 404 (Not Found) [dom] link.sheet = objeto · regras = 0 · document.styleSheets.length = 1 · color = rgb(0, 0, 0)

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:

bash
curl -sI http://localhost:4300/texto/estilo.css | grep -i "HTTP\|content-type"
HTTP/1.1 200 OK content-type: text/plain; charset=utf-8
[rede] 200 /texto/estilo.css [dom] link.sheet = objeto · regras = 0 · document.styleSheets.length = 1 · color = rgb(0, 0, 0)

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:

js
[...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:

> /dois-links.html [ ["http://localhost:4300/css/base.css",2], ["http://localhost:4300/css/tema.css",2] ]

E na página com o caminho quebrado, a folha aparece — com zero regras:

> /paginas/catalogo.html [ ["http://localhost:4300/paginas/css/estilo.css",0] ]

É 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.

Ver todos os vídeos do canal
  • css
  • html
  • link
  • style
  • iniciante

Perguntas frequentes

Posso ter mais de um link de CSS na mesma página?
Pode, e é o normal em projeto grande. Os arquivos são pedidos em paralelo, não em fila. Na medição desta lição, dois links foram pedidos no mesmo milissegundo e ficaram prontos com um milissegundo de diferença entre si.
O que acontece se eu esquecer o rel="stylesheet"?
A folha não é aplicada e nenhum erro aparece. Testei uma página com apenas href, sem rel: document.styleSheets.length ficou em 0, link.sheet valeu null e nenhum pedido do arquivo saiu — a aba de rede fica vazia. Sem rel o navegador não sabe para que serve aquela linha, então ignora ela inteira.
O link precisa mesmo ficar dentro do head?
Não é obrigatório. Coloquei um link no fim do body e o estilo foi aplicado normalmente. Só que aí o navegador já pintou a página sem estilo antes de repintar com ele, e o usuário vê o texto pulando.
Ainda preciso escrever type="text/css" no link?
Não. O atributo é opcional desde o HTML5 e o navegador assume CSS por padrão. Quem manda de verdade é o cabeçalho content-type que o servidor envia junto com o arquivo.

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 via Playwright, servido por Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — O elemento <link> — developer.mozilla.org
  2. MDN — Cascata, especificidade e herança — developer.mozilla.org
  3. CSS Cascading and Inheritance Level 5 — Cascade Sorting Order — w3.org

Continue por aqui