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

background, border-radius e box-shadow no CSS

Como pintar o fundo com cor, imagem e gradiente, arredondar cantos sem deformar e usar sombra para criar profundidade, com cada cor medida no pixel.

Rodolfo Mori12 min de leitura

background pinta o fundo, border-radius arredonda os cantos e box-shadow desenha a sombra. As três mudam a aparência da caixa sem mexer no tamanho dela. A quarta propriedade desta lição, a border, é a exceção: ela soma na largura e empurra os vizinhos.

Os exemplos desta lição montam o painel da Clínica Veterinária Pata Amiga: ficha de consulta, banner de campanha de vacinação, medidor de vacinas em dia e campo de busca. O box model já explicou como o tamanho de cada caixa é calculado; agora a pergunta é o que aparece dentro dela.

Como cada cor desta página foi medida

Nenhuma cor aqui é chute. Cada trecho foi renderizado no Chrome for Testing 151.0.7922.34, controlado pelo Playwright, com deviceScaleFactor: 1 para que um pixel do CSS seja um pixel do arquivo. A captura vira PNG, o PNG passa pelo pngjs, e aí dá para ler o valor exato de qualquer ponto.

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

const browser = await chromium.launch();
const page = await browser.newPage({ deviceScaleFactor: 1 });

export async function pixels(seletor) {
  const png = PNG.sync.read(await page.locator(seletor).screenshot());
  return (x, y) => {
    const i = (png.width * y + x) << 2;
    return `rgb(${png.data[i]},${png.data[i + 1]},${png.data[i + 2]})`;
  };
}

Quando você ler rgb(127,134,127) mais adiante, esse é o número que saiu do navegador, não uma aproximação.

background-color e as camadas que se empilham

background não é uma propriedade só: é uma pilha. Cada valor separado por vírgula em background-image é uma camada, e a primeira da lista fica por cima. A background-color é sempre a última — o chão de tudo.

Este banner tem três camadas: dois retângulos de cor sólida (escritos como gradiente de uma cor só, o truque mais curto para isso) e a cor de fundo.

css
.banner {
  width: 300px;
  height: 80px;
  background-image:
    linear-gradient(#0038ff, #0038ff),
    linear-gradient(#ffd400, #ffd400);
  background-size: 80px 100%, 160px 100%;
  background-position: left, left;
  background-repeat: no-repeat, no-repeat;
  background-color: #16a34a;
}
x= 40 rgb(0,56,255) x=120 rgb(255,212,0) x=250 rgb(22,163,74)

A camada azul tem 80px e ganha nos primeiros 80. A amarela tem 160px, mas só aparece de 80 a 160, porque a azul está por cima dela. Do 160 em diante não sobra imagem nenhuma e o verde da background-color aparece. Se você inverter a ordem das duas linhas de background-image, o amarelo cobre o azul inteiro.

background-image, size, position e repeat

Com imagem de verdade, três propriedades resolvem quase tudo: onde ela começa (background-position), quanto ela ocupa (background-size) e se ela se repete (background-repeat). A diferença que mais confunde é contain contra cover.

Aqui a capa mede 200×120 e a imagem é um retângulo 300×100, ou seja, mais “deitada” que a caixa. O fundo amarelo só existe para denunciar o espaço vazio.

css
.capa {
  width: 200px;
  height: 120px;
  background-color: #ffd400;
  background-image: url('campanha.svg');
  background-position: center;
  background-repeat: no-repeat;
}

.contain { background-size: contain }
.cover   { background-size: cover }
.auto    { background-size: auto }
contain topo(100,4) rgb(255,212,0) meio(100,60) rgb(0,56,255) canto(4,4) rgb(255,212,0) cover topo(100,4) rgb(0,56,255) meio(100,60) rgb(0,56,255) canto(4,4) rgb(0,56,255) auto topo(100,4) rgb(255,212,0) meio(100,60) rgb(0,56,255) canto(4,4) rgb(255,212,0)

Leia o topo da caixa. Com contain ele é amarelo: a imagem inteira coube, e sobrou fundo em cima e embaixo. Com cover ele é azul: a imagem foi ampliada até cobrir tudo, e o que passou do limite foi cortado. Regra prática: cover para foto de capa, contain para logotipo, que não pode ser cortado.

linear-gradient: o ângulo, as paradas e a faixa dura

O ângulo do linear-gradient mede a direção para onde o gradiente vai, com 0deg apontando para cima e crescendo no sentido horário. Muita gente lê 90deg como “de cima para baixo”; ele é da esquerda para a direita.

Duas paradas no mesmo ponto criam um corte seco em vez de uma transição — é assim que se faz uma faixa de duas cores sem imagem nenhuma.

css
.g90  { background-image: linear-gradient(90deg,  #0038ff 0 30%, #ffd400 30%) }
.g180 { background-image: linear-gradient(180deg, #0038ff 0 30%, #ffd400 30%) }
g90 topo-esq rgb(0,56,255) topo-dir rgb(255,212,0) base-esq rgb(0,56,255) g180 topo-esq rgb(0,56,255) topo-dir rgb(0,56,255) base-esq rgb(255,212,0)

Em g90 o canto de baixo à esquerda continua azul, porque o gradiente anda na horizontal. Em g180 é o canto de cima à direita que continua azul. Se você tem dúvida na hora de escrever, use as palavras: to right e to bottom fazem o mesmo e não deixam margem para erro.

radial-gradient e conic-gradient em uso real

O radial-gradient serve para luz: uma mancha clara que some nas bordas, sem imagem e sem opacity no elemento inteiro. O conic-gradient gira ao redor de um centro, o que dá um medidor de progresso de graça.

css
.medidor {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  background: conic-gradient(#0038ff 0 65%, #e2e8f0 65%);
}

.luz {
  width: 200px;
  height: 120px;
  background-color: #0f172a;
  background-image: radial-gradient(circle 70px at 30% 50%, rgba(255, 212, 0, 0.45), transparent);
}
conic 10graus: rgb(0,56,255) conic 200graus: rgb(0,56,255) conic 230graus: rgb(0,56,255) conic 250graus: rgb(226,232,240) radial x= 60: rgb(122,107,23) radial x=100: rgb(60,58,34) radial x=140: rgb(15,23,42) radial x=190: rgb(15,23,42)

Sessenta e cinco por cento de 360 graus dá 234. A amostragem confirma: em 230 graus ainda é azul, em 250 já é cinza. No radial-gradient, o centro da luz está em 30% de 200px, ou seja, em x=60 — e é lá que a cor mais se afasta do fundo. Em x=140, exatos 80px de distância, a luz de raio 70px já acabou e o valor voltou a ser o #0f172a puro.

border, outline e a diferença que aparece no foco do teclado

border faz parte da caixa: ela soma na largura e empurra os vizinhos. outline é desenhada por fora, sem ocupar espaço. E uma sombra com spread e sem borrado (0 0 0 3px) desenha um anel que também não ocupa espaço, e ainda acompanha o border-radius.

A diferença aparece na hora de marcar o botão que recebeu o foco pelo teclado. A barra de ações da clínica é uma linha de três botões distribuída com Flexbox, então qualquer pixel a mais empurra os vizinhos.

html
<div class="barra">
  <button class="botao">Agendar</button>
  <button class="botao alvo">Histórico</button>
  <button class="botao">Vacinas</button>
</div>

O estado de foco entra em um botão só, o do meio. Cada linha da medição abaixo é uma das três classes aplicada ao .alvo, e mais nada muda na página — por isso qualquer deslocamento do terceiro botão veio do botão do meio ter engordado.

css
body { margin: 0 }

.barra { display: flex; gap: 12px; width: 600px }
.botao { padding: 10px 16px; border: 0; border-radius: 8px; background: #e2e8f0 }

.borda   { border: 3px solid #1e5fd0 }
.outline { outline: 3px solid #1e5fd0 }
.anel    { box-shadow: 0 0 0 3px #1e5fd0 }
sem estado botão: 84x35 x do terceiro botão: 190 borda botão: 90x41 x do terceiro botão: 196 outline botão: 84x35 x do terceiro botão: 190 anel botão: 84x35 x do terceiro botão: 190

A borda engordou o botão de 84×35 para 90×41 e empurrou o terceiro botão seis pixels para a direita. outline e anel de sombra não moveram nada. É por isso que estado de foco nunca deve ser feito com border: a barra inteira treme cada vez que o cursor anda com o Tab.

recurso ocupa espaço segue border-radius serve para
border sim sim separar áreas de forma permanente
outline não sim (Chrome/Firefox 2021, Safari 2023) foco do teclado, :focus-visible
box-shadow com spread não sim anel colorido, várias camadas

border-radius com dois raios: a elipse que quase ninguém usa

border-radius: 40px desenha um quarto de círculo em cada canto. Mas a propriedade aceita uma barra: antes dela vão os raios horizontais, depois os verticais. Isso transforma o canto em um quarto de elipse, que é o que você quer quando o elemento é largo e baixo, como um selo.

Para medir, andei pela linha de cima e pela coluna da esquerda de um selo de 200×80 até achar o primeiro pixel já quase todo azul — o ponto em que a curva do canto acabou. A última coluna conta quantos pixels da linha do topo são azul cravado, ou seja, quanta aresta reta sobrou entre os dois cantos.

css
.selo { width: 200px; height: 80px; background: #0038ff }

.circular  { border-radius: 40px }
.eliptico  { border-radius: 40px / 16px }
.oval      { border-radius: 50% }
border-radius: 40px -> canto ocupa 38px na horizontal e 38px na vertical topo reto: 120px border-radius: 40px / 16px -> canto ocupa 35px na horizontal e 15px na vertical topo reto: 122px border-radius: 50% -> canto ocupa 94px na horizontal e 39px na vertical topo reto: 2px

Os 38 no lugar de 40 são a borda suavizada: a ponta da curva ainda mistura azul com branco por alguns pixels, e a varredura para antes do azul cravado. No canto de 40px essa faixa tem dois pixels de largura — o primeiro pixel 100% azul está mesmo em 40.

O que importa é a proporção. Em 40px / 16px o canto se espalha 35px na horizontal e só 15px na vertical: a elipse deitada, que é o formato de selo.

50% é metade de cada lado — raio de 100px na horizontal e 40px na vertical nesta caixa de 200×80. Essa é a curva mais rasa das três, e quanto mais rasa a curva, mais larga fica a faixa suavizada: aqui ela come seis pixels, e por isso a leitura sai 94 em vez de 100. Repare também no efeito colateral, que está na última coluna. Com o raio horizontal valendo metade da largura, os dois cantos de cima se encontram no meio e não sobra aresta reta nenhuma: 50% deixa 2 pixels de topo reto contra os 120 do 40px. O selo inteiro virou uma elipse.

Ou seja, a cápsula — lados retos e pontas redondas — não é o 50%, é o primeiro caso. Em 40px o raio bate exatamente a metade da altura, as pontas fecham e os 120 pixels de topo reto continuam lá. Escrever um valor absurdo como border-radius: 999px dá no mesmo: o CSS reduz todos os raios até caberem, e eles param nos mesmos 40px.

O canto arredondado que não recorta o filho

Este é o erro que mais aparece em cartão com foto. O pai tem border-radius, mas a imagem dentro dele continua quadrada e vaza pelo canto.

css
.moldura { width: 200px; height: 100px; border-radius: 16px }
.foto    { width: 100%; height: 60px; background: #0038ff }
sem overflow canto (2,2): rgb(0,56,255) overflow: hidden canto (2,2): rgb(255,255,255)

Sem overflow: hidden no pai, o pixel (2,2) — que deveria estar fora da curva — é azul: o filho passou por cima do arredondamento. Com overflow: hidden ele é branco, ou seja, o recorte funcionou. A propriedade que resolve isso tem outros efeitos colaterais, e eles estão em overflow no CSS.

box-shadow: offset, blur, spread e a sombra interna

A ordem dos quatro números é sempre a mesma: deslocamento horizontal, vertical, borrado e espalhamento. Só os dois primeiros são obrigatórios.

offset-x e offset-y move para o lado e para baixo
plaintext
<text x="240" y="24" font-size="13" fill="currentColor">spread</text>
<rect x="255" y="54" width="110" height="70" rx="10" stroke="currentColor" fill="none" stroke-width="2"/>
<rect x="240" y="39" width="140" height="100" rx="18" stroke="currentColor" fill="none" stroke-width="1" stroke-dasharray="5 4"/>
<text x="240" y="182" font-size="12" fill="currentColor">cresce antes de borrar</text>

<text x="450" y="24" font-size="13" fill="currentColor">blur</text>
<rect x="470" y="54" width="110" height="70" rx="10" stroke="currentColor" fill="none" stroke-width="2"/>
<rect x="463" y="47" width="124" height="84" rx="14" stroke="currentColor" fill="none" stroke-width="1" stroke-opacity="0.7"/>
<rect x="456" y="40" width="138" height="98" rx="18" stroke="currentColor" fill="none" stroke-width="1" stroke-opacity="0.45"/>
<rect x="449" y="33" width="152" height="112" rx="22" stroke="currentColor" fill="none" stroke-width="1" stroke-opacity="0.2"/>
<text x="449" y="182" font-size="12" fill="currentColor">a beirada some aos poucos</text>

Com a palavra inset, a mesma sombra é pintada para dentro da caixa. É o efeito de campo afundado, útil na busca do painel.

css
.campo {
  width: 240px;
  height: 44px;
  border-radius: 8px;
  background: #f8fafc;
  border: 1px solid #cbd5e1;
  box-shadow: inset 0 2px 4px rgba(15, 23, 42, 0.12);
}
y= 2 rgb(231,233,236) y= 4 rgb(241,243,246) y= 8 rgb(248,250,252) y=16 rgb(248,250,252) y=30 rgb(248,250,252) y=41 rgb(248,250,252)

O fundo do campo é #f8fafc, que é rgb(248,250,252). A dois pixels do topo a leitura é rgb(231,233,236): a sombra interna escureceu ali. A oito pixels o valor já voltou ao fundo puro. Repare que a sombra existe só na faixa de cima, porque o offset-y é positivo — ela desce a partir da borda superior.

Empilhar sombras: a receita de elevação em três camadas

Sombra de verdade não é uniforme. Perto do objeto ela é escura e definida; longe ela é clara e larga. Uma única box-shadow só consegue imitar um dos dois. A saída é declarar três, separadas por vírgula, com blur crescente.

Medi as duas versões da ficha da clínica lendo os pixels abaixo da borda de baixo do cartão. O número é o quanto o pixel escureceu em relação ao fundo #f1f5f9: zero é fundo limpo, quanto maior mais escuro.

css
/* uma camada só */
.ficha-plana {
  box-shadow: 0 10px 20px rgba(15, 23, 42, 0.22);
}

/* três camadas */
.ficha-elevada {
  box-shadow:
    0 1px 2px rgba(15, 23, 42, 0.12),
    0 3px 6px rgba(15, 23, 42, 0.10),
    0 12px 24px rgba(15, 23, 42, 0.12);
}
1 camada 1px: 41 2px: 39 4px: 35 8px: 29 16px: 14 24px: 3 32px: 0 3 camadas 1px: 44 2px: 34 4px: 27 8px: 18 16px: 11 24px: 4 32px: 1

As duas começam quase iguais no contato: 41 contra 44. A diferença está na queda. A sombra única desce devagar nos primeiros oito pixels (41, 39, 35, 29): perde menos de um terço da força onde deveria despencar. A de três camadas cai quase 60% no mesmo trecho (44, 34, 27, 18) e depois se arrasta mais longe do objeto, ainda marcando 1 aos 32 pixels, onde a outra já sumiu. Contraste alto perto e cauda longe: é isso que o olho lê como um objeto encostado na superfície.

Guarde a receita em variáveis CSS e você tem uma escala de elevação inteira sem repetir número:

css
:root {
  --elev-1: 0 1px 2px rgba(15, 23, 42, 0.12), 0 3px 6px rgba(15, 23, 42, 0.10);
  --elev-2: var(--elev-1), 0 12px 24px rgba(15, 23, 42, 0.12);
}

.ficha        { box-shadow: var(--elev-1) }
.ficha:hover  { box-shadow: var(--elev-2) }
computado: rgba(15, 23, 42, 0.12) 0px 1px 2px 0px, rgba(15, 23, 42, 0.1) 0px 3px 6px 0px, rgba(15, 23, 42, 0.12) 0px 12px 24px 0px var(--elev-2): 1px:44 2px:34 4px:27 8px:18 16px:11 24px:4 32px:1

Uma variável pode chamar outra e as duas listas se juntam. O getComputedStyle mostra as três camadas montadas, e a medição dos pixels dá exatamente os mesmos números da versão escrita à mão.

O gradiente que fica cinza no meio, e o oklab resolvendo

Este é o defeito mais chato do linear-gradient, e ele não é culpa sua. Por padrão o navegador interpola as cores em sRGB, canal por canal. Quando as duas pontas são quase opostas, a média dos canais cai perto do cinza — e o meio da faixa morre.

O banner da campanha de vacinação vai de azul a amarelo. É o pior caso possível.

css
.srgb  { background-image: linear-gradient(90deg, #0038ff, #ffd400) }
.oklab { background-image: linear-gradient(in oklab 90deg, #0038ff, #ffd400) }
srgb 25%: rgb(64,95,191) 50%: rgb(127,134,127) 75%: rgb(191,173,64) oklab 25%: rgb(63,119,224) 50%: rgb(127,158,188) 75%: rgb(190,188,139)

Olhe o meio da primeira linha: rgb(127,134,127). O maior e o menor canal estão a sete unidades de distância — vermelho e azul empatados, verde um fio acima. Isso é cinza, medido, bem no meio de um banner que deveria ser colorido. Na segunda linha o mesmo ponto é rgb(127,158,188), com 61 unidades entre o menor e o maior canal: um azul acinzentado, mas ainda azul.

A correção é escrever o espaço de cor dentro do próprio gradiente: in oklab antes do ângulo. O oklab foi desenhado para que a distância entre duas cores corresponda ao que o olho percebe, então o caminho entre elas não passa pelo buraco cinza. A mesma família de espaços aparece na função oklch, que a lição de cores no CSS detalha.

O erro mais comum: o atalho background apagando a cor

Você define a cor do cartão numa regra e, mais tarde, adiciona um ícone com o atalho background. O ícone aparece e a cor some. É o comportamento correto de qualquer atalho em CSS: as partes que você não escreveu voltam ao valor inicial, e o inicial de background-color é transparent.

css
.ficha {
  width: 300px;
  height: 100px;
  background-color: #0038ff;
  border-radius: 12px;
}

/* apaga a cor sem avisar */
.ficha.destaque { background: url('pata.svg') right 16px center / 48px no-repeat }

/* preserva a cor */
.ficha.certa {
  background-image: url('pata.svg');
  background-position: right 16px center;
  background-size: 48px;
  background-repeat: no-repeat;
}
destaque pixel do meio-esquerda: rgb(255,255,255) background-color computado: rgba(0, 0, 0, 0) certa pixel do meio-esquerda: rgb(0,56,255) background-color computado: rgb(0, 56, 255)

O getComputedStyle entrega a prova: na versão com o atalho, o background-color virou rgba(0, 0, 0, 0), transparente, e o branco da página atravessou o cartão. Na versão com as propriedades separadas, o azul continua rgb(0, 56, 255).

Duas saídas: ou você usa sempre as propriedades longas quando já existe uma cor declarada, ou você repete a cor dentro do atalho — background: #0038ff url('pata.svg') right 16px center / 48px no-repeat. Essa armadilha vale para todo atalho do CSS, de font a grid-area.

O que vem depois

A caixa já tem tamanho, fundo, canto e sombra. O próximo passo da trilha de CSS é decidir como ela se comporta ao lado das outras: display, e depois posicionamento. Se quiser ver a ordem completa dos assuntos antes de seguir, o guia de CSS mostra onde cada peça entra.

Prefere aprender em vídeo?

Tem uma aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • css
  • background
  • gradiente
  • box-shadow
  • border-radius

Perguntas frequentes

Qual a diferença entre box-shadow e filter drop-shadow?
box-shadow sempre desenha a sombra da caixa retangular, respeitando o border-radius. filter drop-shadow segue o formato real do conteúdo, inclusive a transparência de um PNG ou de um SVG. Para cartão, use box-shadow; para um ícone recortado, drop-shadow.
Dá para arredondar só dois cantos?
Dá. border-radius aceita até quatro valores, na ordem superior-esquerdo, superior-direito, inferior-direito e inferior-esquerdo. Para arredondar apenas o topo, use os valores 12px 12px 0 0 nessa ordem.
Como faço uma borda com gradiente?
border-color não aceita gradiente. O caminho mais simples é usar dois backgrounds com background-origin e background-clip, ou desenhar o anel com box-shadow e um padding que reserve o espaço.
Sombra deixa a página mais lenta?
Uma sombra estática custa quase nada. O problema aparece quando você anima o box-shadow, porque cada quadro força o navegador a repintar o borrado. Para animar elevação, anime opacity de um pseudo-elemento que já carrega a sombra pronta.

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

Fontes consultadas

  1. MDN — background — developer.mozilla.org
  2. MDN — box-shadow — developer.mozilla.org
  3. MDN — border-radius — developer.mozilla.org
  4. CSS Backgrounds and Borders Module Level 3 — W3C — w3.org
  5. CSS Images Module Level 4 — W3C — w3.org

Continue por aqui