Pular para o conteúdo
Gustavo Tutorial

HTML puro

A base de tudo. Aqui não entra framework nenhum: é HTML para estrutura, CSS para estilo e JavaScript para comportamento, do jeito que o navegador entende sem intermediário. A trilha é longa e foi dividida em duas páginas — esta é a primeira metade, dos fundamentos até o CSS moderno.

Revisão já conversamos sobre isso Novo ainda não vimos 6 partes · 41 tópicos · parte 1 de 2

Nenhum tópico com esse filtro.

PARTE 1

Fundamentos

Os conceitos que organizam todo o resto. Sem isso, os nomes de framework viram sopa de letrinha.

Front-end vs Back-end

Revisão

Front-end é tudo que roda no navegador do visitante: o que ele vê, lê e clica. Back-end é o que roda no servidor, invisível para ele: banco de dados, login, envio de e-mail, processamento de pagamento.

O exemplo que fecha a ideia é uma loja online. A vitrine com fotos, o carrinho que você vê na tela e o botão “comprar” são front-end. Quando você clica em comprar, quem confere estoque, calcula frete, cobra o cartão e grava o pedido é o back-end.

PerguntaFront-endBack-end
Onde roda?No navegador do visitanteNo servidor
O visitante vê o código?Sim — é só abrir o “ver fonte”Não, nunca
Linguagens típicasHTML, CSS, JavaScriptJavaScript (Node), Python, PHP, Java, Ruby
Guarda dados?Não de forma confiávelSim — é a função dele
Seus projetos usam?Sim, integralmenteNão — a conversão vai pro WhatsApp
Por que isso importa pra você: como sua landing page não tem back-end, nada que exija segredo pode estar nela. Uma chave de API escrita no seu HTML está pública para qualquer visitante. Se um dia precisar de algo assim, esse é exatamente o momento em que entra um back-end.

UI vs UX

Revisão

UI (User Interface) é o desenho: cores, tipografia, espaçamento, ícones, o formato do botão. UX (User Experience) é a experiência de usar: se a pessoa entende onde clicar, se o caminho até o objetivo é curto, se ela não fica perdida.

São independentes. Dá pra ter UI lindo com UX péssimo — um site premiado onde ninguém acha o telefone. E o contrário: uma página feia, sem graça, mas que resolve o problema da pessoa em dois cliques.

Numa landing page, a decisão mais cara costuma ser de UX, não de UI: quantos campos o formulário tem, se o botão de WhatsApp aparece antes ou depois da rolagem, se a pessoa precisa entender a oferta antes de agir. Cada campo a mais no formulário derruba conversão — isso é decisão de UX, e nenhuma escolha de cor conserta.

As três camadas: HTML, CSS e JavaScript

Revisão

Front-end é feito de três linguagens com papéis separados. Elas não competem — se empilham.

CamadaPapelNa sua landing page
HTMLEstrutura — o esqueleto e o conteúdoTítulos, textos, imagens, o formulário
CSSEstilo — como cada coisa apareceCores da marca do cliente, layout, espaçamento
JSComportamento — o que acontece ao interagirMenu que abre, botão que monta a mensagem do WhatsApp

O teste que mostra a separação: desligue o CSS de um site e ele vira um documento sem estilo, mas legível — o conteúdo continua lá. Desligue o JavaScript e ele fica estático, mas ainda navegável. É por isso que a regra “a página funciona com JS desligado” é razoável de exigir: o essencial não deveria depender da terceira camada.

html
<!-- HTML: a estrutura, sem estilo nenhum -->
<button class="cta">Falar no WhatsApp</button>
css
/* CSS: como esse botão aparece */
.cta {
  background: #7A1C30;
  color: #fff;
  padding: 14px 28px;
  border: 0;
  border-radius: 4px;
  cursor: pointer;
}
javascript
// JS: o que acontece quando clicam nele
document.querySelector('.cta').addEventListener('click', () => {
  window.open('https://wa.me/5567999999999', '_blank', 'noopener');
});
PARTE 2

Frameworks e ecossistema

O mapa dos nomes que aparecem em toda vaga, tutorial e menu de ferramenta — e onde cada um se encaixa.

O que é framework (e o que é biblioteca)

Revisão

Framework é uma estrutura de código pronta que já resolve os problemas comuns de um tipo de projeto, para você não escrever tudo do zero.

A diferença para uma biblioteca está em quem manda no fluxo. Numa biblioteca, você chama ela quando precisa — uma função de formatar data, por exemplo; o controle é seu. Num framework, ele chama você: define a estrutura do projeto, as pastas, o ciclo de vida, e o seu código se encaixa nas regras dele.

Isso tem um nome clássico: inversão de controle. Biblioteca é ferramenta na sua mão. Framework é a oficina — você trabalha dentro dela.

O custo que ninguém menciona: framework economiza tempo no começo e cobra depois, em manutenção. Um projeto React de 2021 parado não “continua funcionando”: as dependências envelhecem, quebram entre si, e atualizar exige trabalho. Um HTML puro de 2015 abre hoje igual. Isso não é argumento contra frameworks — é o motivo de escolher um só quando ele paga o próprio custo.

Frameworks de front-end

Revisão JS

Todos resolvem o mesmo problema central: manter a tela em sincronia com os dados sem você atualizar cada pedaço na mão.

NomeO que éQuando faz sentido
ReactBiblioteca de interface da Meta, baseada em componentes. É a mais usada do mercadoPadrão de mercado; base de várias outras ferramentas
Next.jsReact + estrutura pronta: rotas por pasta, renderização no servidor, otimizaçõesSites com conteúdo dinâmico e SEO importante
AngularFramework completo do Google, opinativo e estruturadoSistemas grandes, times grandes, ambiente corporativo
VueMais leve e com curva de aprendizado mais suaveProjetos médios; dá pra adotar aos poucos
SvelteCompila para JS puro — não envia framework junto para o navegadorQuando performance e peso final são prioridade

A relação React → Next.js é de camada, não de rivalidade: Next.js é construído em cima do React. Trocar um pelo outro não é migração de linguagem, é reorganização de estrutura.

Um detalhe que explica a confusão nas listas de ferramentas: quando um app te deixa escolher entre “React, Next.js, Angular e Android”, os três primeiros são web e o quarto é uma plataforma de celular. Estão na mesma lista porque são destinos possíveis do código gerado, não porque sejam a mesma categoria de coisa.

Node.js — o runtime, não um framework

Revisão

Antes do Node, JavaScript só rodava dentro do navegador. O Node pegou o motor que o Chrome usa para executar JS (o V8) e passou a rodá-lo fora do navegador — no seu computador ou num servidor. Com isso, JavaScript virou uma linguagem que também serve para back-end.

Node não é framework: é o ambiente onde o código roda. A hierarquia fica assim:

hierarquia
JavaScript  ← a linguagem
   │
   ├── roda no navegador   →  front-end (React, Vue, ou JS puro)
   │
   └── roda no Node.js     →  back-end
                                 └── Express, NestJS, Fastify (frameworks)

Vale saber mesmo sem fazer back-end: quase toda ferramenta moderna de desenvolvimento roda sobre Node. Se um dia você instalar o Tailwind, um compilador de Sass ou um minificador, é o Node que executa essas ferramentas na sua máquina — mesmo que o site entregue não tenha uma linha de Node.

Frameworks de back-end

Revisão

Em cima do Node

NomeCaracterística
ExpressO mais usado. Minimalista — dá liberdade e pouca estrutura
NestJSEstruturado e opinativo, filosofia parecida com a do Angular
FastifySemelhante ao Express, com foco em performance

Em outras linguagens

NomeLinguagemCaracterística
DjangoPython“Pilhas incluídas”: já vem com banco, painel admin e autenticação
LaravelPHPMuito popular no Brasil para sites e sistemas web
Ruby on RailsRubyPioneiro do modelo; inspirou boa parte dos outros
SpringJavaPadrão em bancos e empresas grandes

Para o seu trabalho, o momento em que um deles entra é bem definido: quando o cliente precisar de algo que não cabe no navegador — área de aluno com login, catálogo que ele edita sozinho, cobrança recorrente, envio de e-mail pelo próprio site.

Frameworks e ferramentas de CSS

Revisão CSS
TipoExemplosComo funciona
CSS puro—Você escreve as regras. Zero dependência. É o seu caso
UtilitárioTailwindClasses prontas no HTML: class="flex p-4 text-lg"
ComponentesBootstrapBlocos já estilizados: grid, botões, cards, modais
Pré-processadorSass/SCSS, LessCSS com variáveis, aninhamento e funções — precisa ser compilado
CSS-in-JSStyled ComponentsEstilo escrito dentro do componente JavaScript
Sobre pré-processadores: o Sass existia principalmente porque o CSS não tinha variáveis nem aninhamento. Hoje o CSS tem os dois nativamente (--cor e o aninhamento moderno). Boa parte do motivo de usar Sass em projeto pequeno desapareceu — e com ele desapareceu a necessidade de uma etapa de compilação.

Quando sair do vanilla

Revisão

O gatilho não é o número de páginas. Sete páginas estáticas continuam sendo sete arquivos HTML. O gatilho é a origem do conteúdo e a existência de estado compartilhado.

árvore de decisão
O conteúdo muda sem você editar o código?
  ├── NÃO → HTML puro resolve. Mesmo com 20 páginas.
  └── SIM ↓

Precisa de login, banco de dados ou pagamento?
  ├── SIM → back-end + framework. Sem alternativa.
  └── NÃO ↓

É muito item repetido com o mesmo layout
(catálogo, blog, portfólio grande)?
  ├── SIM → gerador de site estático ou Next.js
  └── NÃO → HTML puro ainda ganha.

Sobre o caminho de migração: vanilla → React é reescrita, não upgrade. O React trabalha com componentes e estado, uma lógica diferente da manipulação direta do DOM; o CSS aproveita bastante, a lógica não. Já React → Next.js é tranquilo, porque um é construído sobre o outro: muda a organização de pastas e o roteamento, não a lógica dos componentes.

npm e package.json

Novo

npm (Node Package Manager) é o instalador de pacotes do mundo JavaScript. Você pede uma biblioteca pelo nome e ele baixa ela e tudo de que ela depende.

O package.json é a ficha do projeto: nome, versão, lista de dependências e atalhos de comando. É o arquivo que permite outra pessoa clonar seu projeto, rodar um comando e ter exatamente o mesmo ambiente.

terminal
npm init -y              # cria o package.json
npm install nome-do-pacote   # instala e anota na lista
npm run dev              # roda um atalho definido em "scripts"

A pasta node_modules que aparece guarda os pacotes baixados. Ela é gigante e nunca vai pro Git nem pro servidor — o package.json é que descreve o que precisa ser reinstalado.

No seu fluxo: entregando HTML único, você não usa npm no projeto do cliente. Mas vai encontrar npm em qualquer tutorial moderno, e é assim que você instalaria uma ferramenta de apoio — um minificador, por exemplo — na sua máquina.

Bundlers: Vite e Webpack

Novo

Um bundler (empacotador) pega dezenas de arquivos de código-fonte e produz poucos arquivos otimizados para o navegador: junta, remove o que não é usado, minifica, converte sintaxe nova para versões que navegadores antigos entendem.

Webpack é o veterano: poderoso, configurável e trabalhoso. Vite é a geração seguinte: praticamente sem configuração, com recarga instantânea durante o desenvolvimento. Hoje, projeto novo começa com Vite.

É aqui que mora a palavra build. “Precisa de build” significa: o código que você escreve não é o código que vai pro ar — existe uma etapa de processamento no meio.

Por que isso é relevante pra você: a maior vantagem da sua entrega é justamente não ter essa etapa. O arquivo que você escreve é o arquivo que roda. Isso elimina uma classe inteira de problema — configuração quebrada, versão incompatível, build que falha no servidor.

TypeScript

Novo JS

TypeScript é JavaScript com tipos declarados. Você diz que uma variável é texto ou número, e o editor avisa antes de rodar quando algo não bate.

comparação
// JavaScript — só descobre o erro rodando
function calcularFrete(peso) {
  return peso * 12.5;
}
calcularFrete("dois quilos");   // NaN, silenciosamente

// TypeScript — o editor acusa antes de rodar
function calcularFrete(peso: number): number {
  return peso * 12.5;
}
calcularFrete("dois quilos");   // erro apontado na hora

O navegador não entende TypeScript — ele é convertido para JavaScript antes de ir pro ar. Ou seja: exige build.

Compensa em projeto grande, com várias pessoas, onde um erro de tipo custa caro. Em landing page de uma página, o custo da etapa de build supera o benefício.

PARTE 3

HTML na prática

A camada que quase todo mundo acha que já sabe — e que decide sozinha o SEO e a acessibilidade do site.

Anatomia de um documento HTML

Novo HTML

Todo arquivo HTML tem duas partes: o <head>, que ninguém vê mas os robôs leem, e o <body>, que é a página visível.

html
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Título que aparece na aba e no Google</title>
  <meta name="description" content="Resumo de até 155 caracteres.">
</head>
<body>
  <!-- tudo que a pessoa vê -->
</body>
</html>
LinhaPara que serveSe faltar
<!DOCTYPE html>Diz que é HTML modernoNavegador entra em “modo de compatibilidade” e o CSS se comporta diferente
lang="pt-BR"Idioma da páginaLeitor de tela lê português com sotaque inglês; o Chrome oferece traduzir
charset="UTF-8"Codificação dos caracteresAcentos viram ção
viewportManda o celular usar a largura real da telaO site abre “dado zoom out” no celular. Erro clássico
titleAba do navegador e título no GoogleAparece o nome do arquivo na busca
Atenção: essas cinco linhas resolvem sozinhas metade dos bugs de “o site tá estranho no celular” e “os acentos quebraram”. Vale decorar.

HTML semântico

Novo HTML

Semântico significa usar a tag que descreve o que aquilo é, em vez de <div> para tudo. Visualmente dá no mesmo — mas Google, leitores de tela e navegação por teclado leem a diferença.

evite
<div class="topo">
  <div class="menu">...</div>
</div>
<div class="conteudo">
  <div class="titulo-grande">Bar de drinks</div>
</div>
<div class="rodape">...</div>
prefira
<header>
  <nav>...</nav>
</header>
<main>
  <h1>Bar de drinks</h1>
  <section>...</section>
</main>
<footer>...</footer>

As tags que resolvem 90% dos casos

TagUso
<header>Topo da página ou de uma seção
<nav>Bloco de navegação (menu)
<main>Conteúdo principal. Um por página
<section>Seção temática, normalmente com título próprio
<article>Conteúdo que faz sentido sozinho (post, card de depoimento)
<aside>Conteúdo lateral, complementar
<footer>Rodapé da página ou da seção
<figure> / <figcaption>Imagem com legenda

Hierarquia de títulos

Um <h1> por página — é o título principal, e o Google usa ele com peso. Os demais descem em ordem: h2 para seções, h3 para subseções. Não pule níveis para conseguir um tamanho de fonte: tamanho é trabalho do CSS, hierarquia é trabalho do HTML.

Regra prática: se você está prestes a escrever <div class="alguma-coisa">, pergunte se existe uma tag com esse nome. Muitas vezes existe — e ela vem com comportamento de teclado e significado de graça.

Atributos essenciais

Novo HTML
AtributoO que faz
classNome de grupo — pode repetir. É o que o CSS usa
idIdentificador único na página. Serve para âncoras (#contato) e para o JS
altDescrição da imagem. Obrigatório. Decorativa? alt="" vazio
hrefDestino do link
target="_blank"Abre em nova aba. Use junto com rel="noopener"
loading="lazy"Só carrega a imagem quando ela chega perto da tela
width / heightReserva o espaço da imagem e evita a página “pular” ao carregar
data-*Dado personalizado que o JS lê: data-num="300"
imagem bem escrita
<img src="bar-montado.webp"
     alt="Bar montado em festa de casamento ao ar livre"
     width="1200" height="800"
     loading="lazy">

Sobre data-*: é a ponte oficial entre HTML e JS. Você guarda o valor no HTML e lê pelo dataset — sem precisar escrever o número dentro do JavaScript.

javascript
// <span class="contador" data-num="300">0</span>
const alvo = document.querySelector('.contador');
console.log(alvo.dataset.num);   // "300"

Formulários e validação nativa

Novo HTML

O navegador já valida formulário sozinho, em português, sem uma linha de JavaScript. Muita gente reimplementa isso à toa.

html
<form id="orcamento">
  <label for="nome">Seu nome</label>
  <input id="nome" name="nome" type="text" required
         minlength="3" autocomplete="name">

  <label for="fone">WhatsApp</label>
  <input id="fone" name="fone" type="tel" required
         inputmode="numeric" autocomplete="tel">

  <label for="data">Data do evento</label>
  <input id="data" name="data" type="date" required>

  <label for="pessoas">Convidados</label>
  <input id="pessoas" name="pessoas" type="number"
         min="10" step="10" required>

  <button type="submit">Pedir orçamento</button>
</form>
AtributoEfeito
requiredBloqueia o envio vazio e mostra a mensagem do navegador
type="tel" / "email" / "date"Muda o teclado no celular e o formato aceito
inputmode="numeric"Abre teclado numérico sem mudar a validação
min / max / stepFaixa permitida em número e data
autocompleteDeixa o navegador preencher — aumenta conversão de verdade
patternExpressão regular para formatos específicos

O <label> não é opcional

O for do label tem que bater com o id do campo. Isso faz o clique no texto focar o campo, permite o leitor de tela anunciar o rótulo, e aumenta a área de toque no celular. Usar só placeholder como rótulo é um erro: ele some quando a pessoa começa a digitar.

javascript
// Deixa o navegador validar; só depois monta a mensagem
form.addEventListener('submit', (e) => {
  e.preventDefault();
  if (!form.reportValidity()) return;   // mostra os erros nativos

  const d = new FormData(form);
  const msg = `Olá! Vim pelo site.\n\n`
            + `Nome: ${d.get('nome')}\n`
            + `Convidados: ${d.get('pessoas')}`;

  window.open(`https://wa.me/5567999999999?text=${encodeURIComponent(msg)}`,
              '_blank', 'noopener');
});
encodeURIComponent é obrigatório. Sem ele, acento, quebra de linha e o símbolo & quebram a URL do WhatsApp e a mensagem chega cortada.

Acessibilidade

Novo HTML

Acessibilidade não é recurso extra: é o site funcionar para quem usa leitor de tela, navega por teclado, enxerga pouco contraste ou está no sol com o brilho no mínimo. E o que resolve a maior parte é barato.

ItemO mínimo
Imagensalt descritivo; alt="" quando for decoração
Contraste4.5:1 para texto normal, 3:1 para texto grande
FocoContorno visível ao navegar por Tab. Nunca outline: none sem substituto
FormulárioTodo campo com <label> ligado pelo for
Botão vs linkLink leva a algum lugar; botão executa ação. Não troque os dois
Ícone sozinhoaria-label dizendo o que ele faz
Menu mobilearia-expanded mudando de false para true
MovimentoRespeitar prefers-reduced-motion
html
<!-- botão só com ícone precisa de nome acessível -->
<button aria-label="Abrir menu" aria-expanded="false"
        aria-controls="painel-menu">☰</button>

<!-- link de WhatsApp: o texto já descreve, não precisa de aria -->
<a href="https://wa.me/5567999999999">Falar no WhatsApp</a>
Teste de 30 segundos: aperte Tab repetidamente na sua página. Você consegue ver onde está o foco em todos os passos? Consegue chegar no botão de WhatsApp e acioná-lo com Enter? Se sim, você já está acima da média do mercado.
PARTE 4

CSS na prática

Onde você passa a maior parte do tempo. Os conceitos aqui explicam quase todo “por que isso não está funcionando”.

Box model

Novo CSS

Todo elemento é uma caixa com quatro camadas, de dentro para fora: conteúdo, padding (espaço interno), border (borda) e margin (espaço externo).

as quatro camadas
┌─────────── margin ────────────┐   ← empurra os vizinhos
│  ┌──────── border ─────────┐  │
│  │  ┌───── padding ─────┐  │  │   ← afasta o conteúdo da borda
│  │  │                   │  │  │
│  │  │     conteúdo      │  │  │
│  │  │                   │  │  │
│  │  └───────────────────┘  │  │
│  └─────────────────────────┘  │
└───────────────────────────────┘

A linha que você sempre escreve (e por quê)

Por padrão, width: 300px define a largura só do conteúdo. Se você somar padding e borda, a caixa fica maior que 300px — e o layout estoura. O border-box muda isso: a largura passa a incluir padding e borda.

css
* { box-sizing: border-box; }

/* Agora isto ocupa exatamente 300px, não 340px: */
.card {
  width: 300px;
  padding: 20px;   /* 20px de cada lado */
}

Colapso de margem

Duas margens verticais que se encostam não somam — vence a maior. Margem de 30px embaixo de um elemento e 20px em cima do próximo resultam em 30px, não 50px. É a causa mais comum de “o espaçamento não está batendo”. Usar gap no flex ou grid evita o problema inteiro.

Seletores e especificidade

Novo CSS

Quando duas regras mandam na mesma propriedade, vence a mais específica — não a que veio depois. Esta é a resposta para “editei a cor e não mudou nada”.

SeletorExemploPeso
Elementop, button1
Classe.botao10
Atributo / pseudo-classe[type="text"], :hover10
ID#contato100
Estilo direto no HTMLstyle="..."1000
!important—Atropela tudo
css
.botao        { background: red; }    /* peso 10  */
#cta .botao   { background: blue; }   /* peso 110 — vence */

/* Mesmo escrevendo a regra vermelha depois, azul continua ganhando */
Sobre !important: resolve na hora e cria dívida. Depois de dois ou três, você precisa de !important para sobrescrever !important, e o arquivo vira areia movediça. Antes de usar, veja se não dá para baixar a especificidade da regra concorrente.

Seletores que vale conhecer

css
.card .titulo      { }   /* descendente: título dentro de card */
.card > .titulo    { }   /* filho direto apenas */
.card + .card      { }   /* um card imediatamente após outro */
.card:not(.ativo)  { }   /* todos menos os ativos */
a[href^="https"]   { }   /* link cujo href começa com https */
a[href$=".pdf"]    { }   /* link cujo href termina em .pdf */

Variáveis CSS (custom properties)

Novo CSS

Você declara um valor num lugar só e usa em todo o arquivo. Mudar a paleta do cliente vira editar cinco linhas em vez de caçar hexadecimais.

css
:root {
  --marsala:   #7A1C30;
  --preto:     #0B0B0B;
  --off:       #F9F8F6;
  --espaco:    24px;
}

.botao {
  background: var(--marsala);
  color: var(--off);
  padding: calc(var(--espaco) / 2) var(--espaco);
}

/* valor reserva, caso a variável não exista */
.card { border-color: var(--linha, #ddd); }

Diferente das variáveis do Sass, estas são lidas pelo navegador em tempo real — o JavaScript consegue trocar uma e a página inteira muda na hora. É assim que se faz tema claro/escuro sem duplicar CSS.

javascript
document.documentElement.style.setProperty('--marsala', '#0D6B62');
No seu fluxo: deixe um bloco :root no topo do CSS com todas as cores e medidas do cliente. Quando ele pedir “deixa mais escuro”, é um lugar só para mexer.

Unidades de medida

Novo CSS
UnidadeRelativa aUse para
pxNada — é fixoBordas, sombras, detalhes que não devem escalar
remFonte da raiz (16px por padrão)Fontes e espaçamentos. A escolha padrão
emFonte do próprio elementoPadding que deve acompanhar o tamanho do texto
%Elemento paiLarguras dentro de um container
vw / vhLargura / altura da janelaBlocos de tela cheia
chLargura do caractere “0”Limitar linha de texto (60–70ch lê bem)
frFração do espaço livreSó dentro de Grid

clamp() — tipografia que se ajusta sozinha

Define mínimo, valor ideal e máximo numa linha só. Substitui várias media queries para tamanho de fonte.

css
h1 {
  /* nunca menor que 32px, nunca maior que 64px,
     entre os dois acompanha a largura da tela */
  font-size: clamp(32px, 6vw, 64px);
}
Cuidado com 100vh no celular: a barra de endereço do navegador entra e sai ao rolar, e o vh muda junto — o conteúdo “pula”. A unidade dvh (dynamic viewport height) existe justamente para isso: height: 100dvh.

Flexbox

Revisão CSS

Organiza elementos numa direção — linha ou coluna — e distribui o espaço entre eles. É a ferramenta para menu, barra de botões, card com ícone ao lado do texto.

css
.menu {
  display: flex;
  align-items: center;        /* alinha no eixo vertical  */
  justify-content: space-between;  /* distribui no horizontal */
  gap: 24px;                  /* espaço entre os filhos   */
  flex-wrap: wrap;            /* quebra linha se faltar espaço */
}
ABC
justify-content ao vivodisplay: flex
PropriedadeValores úteis
flex-directionrow (padrão), column
justify-contentflex-start, center, space-between
align-itemsstretch (padrão), center, flex-start
gapQualquer medida. Substitui margens entre filhos
flex: 1No filho: “ocupe todo o espaço que sobrar”
flex: noneNo filho: “não encolha nem cresça”
O truque da linha pontilhada de cardápio: rótulo, um elemento vazio com flex: 1 e border-bottom: 1px dotted, e o valor. O elemento do meio estica e preenche o espaço disponível, seja qual for o tamanho do texto dos lados.

Grid

Revisão CSS

Organiza em duas dimensões — linhas e colunas ao mesmo tempo. É a ferramenta para grade de cards, galeria, layout de página com barra lateral.

css
.galeria {
  display: grid;
  grid-template-columns: repeat(3, 1fr);  /* 3 colunas iguais */
  gap: 24px;
}

/* Grade que se adapta sozinha, sem media query:
   cada card tem no mínimo 260px; cabem quantos couberem */
.galeria-auto {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 24px;
}
12345678

auto-fit + minmax: arraste o canto da caixasem media query

Aquele auto-fit com minmax é o padrão mais útil do Grid: resolve responsividade de galeria sem escrever uma media query. Vale decorar.

Flex ou Grid?

SituaçãoEscolha
Uma fila de itens (menu, botões)Flex
Grade de cardsGrid
Conteúdo define o tamanhoFlex
Layout define o tamanhoGrid
Página inteira com áreasGrid

Responsividade e media queries

Novo CSS

Mobile-first significa escrever o CSS base para a tela pequena e ir adicionando o que muda conforme a tela cresce. Fica menos código, porque o layout de celular é quase sempre o mais simples: tudo empilhado numa coluna.

css
/* base: celular — uma coluna */
.secao {
  padding: 48px 16px;
}
.cards {
  display: grid;
  gap: 16px;
}

/* tablet pra cima */
@media (min-width: 700px) {
  .cards { grid-template-columns: repeat(2, 1fr); }
}

/* desktop */
@media (min-width: 1024px) {
  .secao { padding: 96px 24px; }
  .cards { grid-template-columns: repeat(3, 1fr); gap: 24px; }
}

Breakpoints que funcionam na prática

LarguraAlvo
até 480pxCelular em pé
700–768pxTablet / celular deitado
1024pxNotebook pequeno
1280px+Desktop

Não persiga modelos de aparelho. Escolha o ponto onde o seu layout começa a ficar feio — esse é o breakpoint certo, mesmo que seja 843px.

Outras media queries úteis

css
@media (prefers-reduced-motion: reduce) { /* desliga animação   */ }
@media (prefers-color-scheme: dark)     { /* tema escuro do SO  */ }
@media (hover: none)                    { /* telas sem mouse    */ }
@media print                            { /* versão impressa    */ }

Pseudo-classes e pseudo-elementos

Novo CSS

Pseudo-classe (um :) estiliza um estado do elemento. Pseudo-elemento (dois ::) cria uma parte visual que não existe no HTML.

css
/* estados */
.botao:hover        { background: #5A1827; }
.botao:focus-visible{ outline: 2px solid #C9A66B; outline-offset: 2px; }
.botao:disabled     { opacity: .5; cursor: not-allowed; }
.campo:invalid      { border-color: #B8461F; }

/* posição na lista */
.card:first-child   { }
.card:last-child    { }
.card:nth-child(2n) { }   /* os pares */

/* partes criadas pelo CSS */
.rotulo::before {
  content: "";
  display: inline-block;
  width: 28px; height: 1px;
  background: currentColor;
  margin-right: 12px;
}
:focus-visible em vez de :focus: o contorno aparece para quem navega por teclado e não aparece no clique de mouse. Resolve a reclamação de “esse contorno feio ao clicar” sem quebrar a acessibilidade — que é o que outline: none faz.

O ::before e o ::after precisam de content para existir, nem que seja vazio. São a forma de criar detalhes gráficos — filetes, aspas decorativas, marcadores — sem sujar o HTML com <div> vazia.

Position e empilhamento

Novo CSS
ValorComportamentoUso típico
staticPadrão. Segue o fluxo da páginaQuase tudo
relativeDesloca a partir da posição original, sem tirar do fluxoServir de referência para um filho absoluto
absoluteSai do fluxo; posiciona-se pelo ancestral posicionado mais próximoSelo sobre uma imagem, badge no canto do card
fixedSai do fluxo; fica preso à janelaBotão flutuante de WhatsApp
stickyNormal até chegar no limite, depois grudaMenu que fica no topo ao rolar
css
/* menu que gruda no topo */
.menu {
  position: sticky;
  top: 0;
  z-index: 50;
}

/* botão flutuante do WhatsApp */
.whatsapp {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 40;
}

/* selo posicionado dentro de um card */
.card      { position: relative; }
.card__selo{ position: absolute; top: 12px; right: 12px; }
Duas armadilhas do z-index: ele só funciona em elemento posicionado (qualquer coisa diferente de static). E overflow: hidden em um pai corta o filho sticky — se o seu menu grudento parou de funcionar, procure um overflow acima dele.

Transições e animações

Revisão CSS

Transição suaviza a mudança entre dois estados. Animação (@keyframes) define uma sequência própria, que roda sozinha.

css
/* transição: do estado normal para o hover */
.botao {
  background: #7A1C30;
  transition: background .2s ease, transform .2s ease;
}
.botao:hover {
  background: #5A1827;
  transform: translateY(-2px);
}

/* animação própria */
@keyframes surgir {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: translateY(0); }
}
.card { animation: surgir .6s cubic-bezier(.22,.61,.36,1) both; }
Cubo 3D em CSS puro — arraste para girartransform-style: preserve-3d

Anime só duas propriedades

transform e opacity são processadas pela placa de vídeo e não forçam o navegador a recalcular o layout. Animar width, height, top ou margin trava em celular. Precisa mover algo? Use transform: translate(), não top.

Opções de animação

OpçãoPesoQuando usar
CSS puroZeroHover, fade, reveal simples. Resolve quase tudo
AOS~14KB via CDNReveal ao rolar, sem escrever JS
GSAP~70KBSequências complexas, timeline sincronizada
Lottie~250KB + JSONIlustração animada vinda do After Effects
Framer MotionSó com ReactNão se aplica ao seu fluxo
Obrigatório em qualquer entrega: respeitar quem desligou animação no sistema. São três linhas e evita enjoo real em pessoas com sensibilidade vestibular.
css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
}
PARTE 5

JavaScript na prática

O suficiente para fazer uma landing page interativa sem framework — que é exatamente o que os seus projetos pedem.

O DOM

Novo JS

Quando o navegador lê seu HTML, ele monta na memória uma árvore de objetos representando cada elemento. Essa árvore é o DOM (Document Object Model). O JavaScript não mexe no arquivo HTML — ele mexe nessa árvore, e o navegador redesenha a tela.

do arquivo à árvore
<body>                    document
  <header>                  └── html
    <h1>Art Bar</h1>             ├── head
  </header>                      └── body
  <main>                              ├── header
    <p>Drinks</p>                    │     └── h1 → "Art Bar"
  </main>                             └── main
</body>                                     └── p  → "Drinks"

Duas consequências práticas. Primeira: se o seu <script> roda antes do elemento existir na árvore, ele não acha nada — por isso o script vai no fim do <body>. Segunda: alterações no DOM são temporárias. Recarregou a página, voltou tudo ao HTML original.

Selecionar e manipular elementos

Novo JS
selecionar
// um elemento — aceita qualquer seletor CSS
const menu = document.querySelector('.menu');
const cta  = document.querySelector('#orcamento');

// vários — devolve uma lista
const cards = document.querySelectorAll('.card');
cards.forEach(card => console.log(card));
manipular
// texto (seguro — não interpreta HTML)
titulo.textContent = 'Novo título';

// HTML (só com conteúdo que VOCÊ escreveu)
box.innerHTML = '<strong>Atenção</strong>';

// classes — a forma correta de mudar aparência
menu.classList.add('is-solid');
menu.classList.remove('is-solid');
menu.classList.toggle('is-solid');
menu.classList.toggle('is-solid', window.scrollY > 80);  // condicional

// atributos
botao.setAttribute('aria-expanded', 'true');
link.href = 'https://wa.me/5567999999999';

// esconder / mostrar
painel.hidden = true;
Regra que economiza muita dor: o JavaScript troca classes, o CSS decide a aparência. Escrever elemento.style.background = 'red' espalha estilo pelo JS e cria estilo inline, que tem especificidade altíssima e é difícil de sobrescrever depois.
Sobre innerHTML: nunca use com texto vindo de fora — de um formulário, de uma URL, de uma API. Isso executa HTML e script arbitrário na sua página. Para texto de terceiro, use sempre textContent.

Eventos

Novo JS

Evento é “algo aconteceu”: um clique, uma tecla, uma rolagem, um formulário enviado. Você registra uma função que roda quando isso acontece.

javascript
botao.addEventListener('click', (e) => {
  console.log('clicaram em', e.target);
});
EventoDispara quando
clickClique ou toque
submitFormulário é enviado
inputCada caractere digitado
changeCampo perde o foco com valor alterado
keydownTecla pressionada — e.key === 'Escape'
scrollA página rola
DOMContentLoadedO HTML terminou de ser lido

Delegação de eventos

Em vez de registrar um ouvinte em cada card, registre um só no container e descubra quem foi clicado. Funciona inclusive para elementos criados depois.

javascript
galeria.addEventListener('click', (e) => {
  const card = e.target.closest('.card');
  if (!card) return;
  abrirLightbox(card.dataset.foto);
});

Scroll sem travar

O evento scroll dispara dezenas de vezes por segundo. Trabalho pesado direto nele engasga a rolagem — a solução é agrupar dentro de requestAnimationFrame.

javascript
let agendado = false;
window.addEventListener('scroll', () => {
  if (agendado) return;
  agendado = true;
  requestAnimationFrame(() => {
    menu.classList.toggle('is-solid', window.scrollY > 80);
    agendado = false;
  });
});

IntersectionObserver

Novo JS

Avisa quando um elemento entra ou sai da tela, sem você ficar calculando posição no scroll. É a ferramenta certa para animação de entrada, contador que começa ao aparecer e menu que destaca a seção atual.

javascript
const observador = new IntersectionObserver((entradas) => {
  entradas.forEach(entrada => {
    if (entrada.isIntersecting) {
      entrada.target.classList.add('is-in');
      observador.unobserve(entrada.target);   // anima uma vez só
    }
  });
}, { threshold: .15 });   // 15% visível já conta

document.querySelectorAll('[data-reveal]')
  .forEach(el => observador.observe(el));

O threshold é a fração do elemento que precisa estar visível para disparar. O unobserve é o que impede a animação de repetir a cada rolagem — e libera memória.

Armadilha de SEO e de primeira impressão: se o CSS deixa o elemento em opacity: 0 esperando o observer, e o JS falha ou está desligado, o conteúdo nunca aparece. Deixe o estado final como padrão e aplique o inicial só quando o JS assumir — ou aceite que a seção pode ficar invisível para alguém.

Fetch, Promises e async/await

Novo JS

Buscar algo na internet leva tempo, e o JavaScript não fica parado esperando. Uma Promise é a promessa de um resultado que chega depois. O async/await é a forma legível de escrever isso.

javascript
async function buscarCep(cep) {
  try {
    const resposta = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
    if (!resposta.ok) throw new Error('CEP não encontrado');

    const dados = await resposta.json();
    return dados.localidade;      // "Campo Grande"
  } catch (erro) {
    console.error(erro);
    return null;
  }
}

// usando
const cidade = await buscarCep('79000000');
PeçaPapel
asyncMarca a função como assíncrona; ela pode usar await
await“Espere o resultado antes de seguir” — sem travar a página
try / catchCaptura erro de rede e resposta inválida
resposta.okFalso em erro 404 ou 500. O fetch não joga erro sozinho nesses casos
No seu fluxo: raramente necessário — um formulário que abre o WhatsApp não precisa de fetch. Aparece quando você quer buscar CEP para preencher endereço, puxar horários de uma planilha publicada ou consultar uma API pública.

JSON

Novo JS

JSON é um formato de texto para representar dados. É o que quase toda API devolve e o que você usa para separar conteúdo de código.

javascript
const DADOS = {
  drinks: [
    { nome: "Negroni de Cerrado", alcool: true,
      ingredientes: ["gin", "campari", "vermute"] },
    { nome: "Pantanal Sem Álcool", alcool: false,
      ingredientes: ["maracujá", "hortelã", "tônica"] }
  ]
};

// gerar HTML a partir dos dados
const html = DADOS.drinks.map(d => `
  <article class="card">
    <h3>${d.nome}</h3>
    <p>${d.ingredientes.join(' · ')}</p>
  </article>
`).join('');

document.querySelector('.carta').innerHTML = html;
Padrão muito útil na sua entrega: colocar todo o conteúdo variável num const DADOS = {...} no topo do script. Quando o cliente pedir para trocar um drink ou um depoimento, você edita uma lista no começo do arquivo em vez de caçar HTML no meio da página.

localStorage

Novo JS

Guarda pequenos textos no navegador da pessoa, e eles sobrevivem ao fechar a aba. Serve para preferências: tema escolhido, aviso de cookie já dispensado, formulário parcialmente preenchido.

javascript
// sempre dentro de try/catch: pode estar bloqueado
try {
  localStorage.setItem('tema', 'escuro');
  const tema = localStorage.getItem('tema');   // "escuro" ou null
  localStorage.removeItem('tema');
} catch (e) {
  // aba anônima ou cookies bloqueados — a página segue funcionando
}

// guardando objeto: precisa virar texto
localStorage.setItem('rascunho', JSON.stringify({ nome: 'Ana' }));
const rascunho = JSON.parse(localStorage.getItem('rascunho') || '{}');
CaracterísticaDetalhe
CapacidadeCerca de 5MB por site
AlcanceUm navegador, um dispositivo. Não sincroniza
FormatoSó texto — objetos passam por JSON.stringify
SegurançaVisível e editável pelo visitante. Nunca guarde nada sensível
sessionStorageIgual, mas apaga ao fechar a aba
Não é banco de dados. O visitante limpa o histórico e os dados somem. Abriu no celular, não vê o que salvou no computador. Para qualquer coisa que precise durar de verdade, é back-end.
PARTE 6

CSS moderno

Recursos que chegaram nos últimos dois anos e que apagam JavaScript do seu arquivo. Todos funcionam hoje, sem build e sem biblioteca.

O seletor :has()

Novo CSS

Durante vinte anos o CSS só olhava para baixo: você estilizava um filho, nunca o pai. O :has() inverte isso — estiliza um elemento com base no que existe dentro dele.

css
/* card que TEM imagem ganha layout diferente */
.card:has(img) {
  grid-template-columns: 120px 1fr;
}

/* campo preenchido: esconde o rótulo flutuante */
.campo:has(input:not(:placeholder-shown)) .rotulo {
  opacity: 0;
}

/* o formulário inteiro reage a um checkbox marcado */
form:has(#outro:checked) .campo-extra {
  display: block;
}

/* menu aberto trava a rolagem — sem tocar no JS */
body:has(.menu-painel.is-aberto) {
  overflow: hidden;
}

Aquele último exemplo merece atenção: é um caso em que você provavelmente escreve document.body.style.overflow = 'hidden' no JavaScript. Com :has(), o CSS reage sozinho à classe que já existe.

Nome informal: chamam de “seletor pai”, mas ele é mais que isso — funciona com irmãos também. .titulo:has(+ .subtitulo) estiliza um título só quando existe um subtítulo logo depois dele.

Container queries

Novo CSS

Media query pergunta “qual o tamanho da tela?”. Container query pergunta “qual o tamanho do espaço onde eu estou?”. A diferença é grande: o mesmo card pode aparecer largo numa seção e estreito numa barra lateral, e se adaptar sozinho aos dois.

css
/* 1. declara que este elemento é um container mensurável */
.coluna {
  container-type: inline-size;
  container-name: coluna;
}

/* 2. o filho reage à largura DA COLUNA, não da tela */
@container coluna (min-width: 420px) {
  .card {
    display: grid;
    grid-template-columns: 140px 1fr;
    gap: 16px;
  }
}

Por que isso muda o jeito de pensar: com media query, um componente precisa saber em que ponto da página ele foi colocado. Com container query, ele se vira sozinho em qualquer lugar. É o que torna um componente de verdade reutilizável.

Onde te ajuda hoje: naquela grade de cards que você monta com auto-fit. Quando a coluna fica estreita, o card pode empilhar foto e texto; quando fica larga, coloca lado a lado — sem você escolher breakpoint de tela nenhum.

Aninhamento nativo e @layer

Novo CSS

O CSS agora aninha regras sem pré-processador. Era o principal motivo de usar Sass — e ele sumiu.

css
.card {
  padding: 20px;
  border: 1px solid var(--linha);

  &:hover { border-color: var(--destaque); }

  .titulo {
    font-size: 20px;
    margin-bottom: 8px;
  }

  @media (min-width: 700px) {
    padding: 32px;
  }
}

O & representa o próprio seletor pai. Dentro do aninhamento você também pode colocar media queries, o que mantém tudo sobre um componente no mesmo bloco.

@layer — resolvendo brigas de especificidade

Camadas definem a ordem de prioridade independentemente da especificidade. Uma regra numa camada posterior vence uma regra mais específica de uma camada anterior. É a solução estrutural para o problema que hoje se resolve com !important.

css
@layer base, componentes, utilidades;

@layer base {
  #conteudo p { color: gray; }   /* especificidade alta */
}

@layer utilidades {
  .texto-escuro { color: black; }  /* especificidade baixa... e vence */
}
Cuidado com aninhamento profundo. Aninhar quatro níveis gera seletores enormes e especificidade alta — o mesmo problema que o Sass criou em tanto projeto. Dois níveis costuma ser o limite saudável.

Cores modernas: oklch() e color-mix()

Novo CSS

Hexadecimal não tem lógica interna: #7A1C30 não te diz nada, e “clarear 10%” vira tentativa e erro. O oklch() descreve a cor em três números que fazem sentido — Luminosidade, Croma (saturação) e Hue (matiz, em graus).

css
:root {
  --marca-h: 345;   /* o matiz do marsala, em graus */

  --marca:       oklch(38% 0.13 var(--marca-h));
  --marca-clara: oklch(58% 0.13 var(--marca-h));  /* só a luz mudou */
  --marca-escura:oklch(26% 0.13 var(--marca-h));
}

A vantagem prática: a paleta inteira do cliente sai de um número. Mudou o matiz, mudou tudo de forma coerente — e a diferença de luminosidade é perceptualmente uniforme, coisa que o HSL não garante.

color-mix() — misturar sem calculadora

css
/* a cor da marca com 15% de transparência */
.aviso { background: color-mix(in srgb, var(--marca) 15%, transparent); }

/* a cor da marca puxada para o branco */
.suave  { background: color-mix(in srgb, var(--marca) 20%, white); }
Uso direto no seu trabalho: aquele fundo claro de seção que combina com a cor do cliente. Em vez de pedir o hexadecimal ou chutar no conta-gotas, você deriva do próprio token — e se o cliente trocar a cor, o fundo acompanha.

Modo escuro: color-scheme, light-dark() e o alternador sem flash

Novo CSS

Seguir o tema do sistema é fácil: um @media (prefers-color-scheme: dark) resolve quando o cliente não pede botão nenhum. O problema aparece quando ele pede — alternador manual, escolha lembrada na próxima visita, e a página sem piscar clara antes de escurecer.

Primeiro: declare que a página sabe lidar com os dois temas

html
<meta name="color-scheme" content="light dark">

Sem essa declaração, o navegador trata a página como só-clara: barra de rolagem, campo de formulário e seletor de data continuam com aparência clara mesmo que o seu fundo seja escuro. Com ela, esses elementos nativos acompanham o tema. A propriedade color-scheme no CSS faz o mesmo; a meta tag tem a vantagem de ser lida antes do CSS carregar, o que ajuda a evitar um clarão branco no início.

light-dark() — as duas cores num lugar só

Antes, cada cor que muda de tema pedia duas declarações: uma no :root e outra dentro do @media (prefers-color-scheme: dark), repetidas para cada variável. A função light-dark() junta as duas.

css
:root {
  color-scheme: light dark;

  --fundo: light-dark(#FFFFFF, #0D1514);
  --texto: light-dark(#1A1A1A, #E8E8E8);
  --marca: light-dark(oklch(38% 0.13 345), oklch(58% 0.13 345));
}

body {
  background: var(--fundo);
  color: var(--texto);
}

Arquitetura · Campo Grande

Reforma com cronograma fechado

Mesmo CSS, dois temas: só o color-scheme muda.

Pedir orçamento
light-dark(): as duas cores num lugar sócolor-scheme

Com color-scheme: light dark no :root, o navegador usa o primeiro valor de cada light-dark() quando o sistema está no claro e o segundo quando está no escuro — sem media query. Aproveite para escolher cada par com oklch(): mesma cor, só a luminosidade muda.

O alternador manual: vencer a escolha do sistema

Quando o cliente quer um botão de sol/lua, ele precisa sobrescrever a preferência do sistema. Um atributo no <html> troca o color-scheme dali, e o light-dark() acompanha sozinho.

css
:root {
  color-scheme: light dark; /* segue o sistema por padrão */
}

html[data-tema="claro"]  { color-scheme: light; }
html[data-tema="escuro"] { color-scheme: dark;  }
app.js
const botao = document.querySelector('.alternador-tema');
if (botao) {
  botao.addEventListener('click', () => {
    const raiz = document.documentElement;
    // sem escolha salva, o "atual" é o que o sistema está mostrando
    const escuroAgora = raiz.dataset.tema
      ? raiz.dataset.tema === 'escuro'
      : matchMedia('(prefers-color-scheme: dark)').matches;
    const novo = escuroAgora ? 'claro' : 'escuro';
    raiz.dataset.tema = novo;
    try { localStorage.setItem('tema', novo); } catch (e) {}
  });
}

A checagem com matchMedia não é enfeite: sem ela, quem está com o sistema no escuro clicaria no botão e nada mudaria na primeira vez, porque o "próximo" tema calculado seria o que já está na tela.

O flash de tema errado — e como evitar

Se você lê o localStorage só depois que a página carregou, o visitante vê a página abrir no tema padrão e trocar de repente para o salvo. A saída é aplicar o atributo antes do CSS renderizar, com um script inline no <head>, antes de qualquer <link rel="stylesheet">.

no <head>, antes do CSS
<script>
  try {
    const salvo = localStorage.getItem('tema');
    if (salvo) document.documentElement.dataset.tema = salvo;
  } catch (e) {}
</script>
Por que inline e não no app.js: um arquivo externo só roda depois de baixado, e nesse intervalo o navegador já pintou a página no tema errado. O script precisa ser pequeno, ficar no começo do <head> e ter try/catch — aba anônima ou localStorage bloqueado não pode quebrar a página.

A meta tag e o light-dark() funcionam sozinhos, seguindo o sistema. O alternador e o script inline só fazem sentido juntos, e só se o cliente pediu o botão — sem esse pedido, pare no light-dark().

Animação por rolagem sem JavaScript

Novo CSS

Aquele reveal ao rolar que hoje exige IntersectionObserver — ou a biblioteca AOS — virou CSS puro. E roda na thread de composição, então não trava nem em celular fraco.

css
@keyframes surgir {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}

.secao {
  animation: surgir linear both;
  animation-timeline: view();          /* atrelada à entrada na tela */
  animation-range: entry 10% cover 35%; /* começo e fim do movimento */
}

/* barra de progresso de leitura, no topo da página */
.progresso {
  animation: encher linear;
  animation-timeline: scroll();   /* atrelada à rolagem da página */
}
@keyframes encher { to { width: 100%; } }
Linha do tempoAtrelada aUso
scroll()A rolagem da página inteiraBarra de progresso, parallax
view()A entrada do elemento na telaReveal de seção, fade de imagem
Suporte ainda não é universal. Chrome e Edge sim; Safari e Firefox chegaram depois. Como é animação, a degradação é segura: onde não funciona, o elemento aparece estático. Mas nunca esconda conteúdo dependendo disso — vale a mesma regra do IntersectionObserver.

View Transitions

Novo CSS

Transição animada entre estados — ou entre páginas diferentes — com o navegador cuidando do trabalho pesado. É o efeito que antes só existia em aplicativo nativo ou em SPA com framework.

css
/* transição suave entre páginas do mesmo site */
@view-transition { navigation: auto; }

/* elemento que "viaja" de uma página para a outra */
.hero-foto { view-transition-name: foto-principal; }

Com esse nome igual nas duas páginas, o navegador entende que é o mesmo elemento e anima a transformação entre uma posição e a outra — a foto do card cresce e vira a foto do topo da página seguinte.

Onde isso te interessa: se um dia você entregar um site de várias páginas em HTML puro, duas linhas de CSS dão a ele a sensação de aplicativo. É exatamente o tipo de polimento que fazia as pessoas acharem que precisavam de framework.