Esta trilha cobre: arquiteturas computacionais para Web; criação de páginas web com HTML, CSS e JavaScript.
Objetivo geral: ao final desta trilha, você projeta e desenvolve o front-end de aplicações web funcionais, acessíveis e responsivas, dominando as três tecnologias fundamentais da plataforma (HTML, CSS e JavaScript) e entendendo a arquitetura em que elas operam.
A palavra-chave é front-end: tudo que roda no navegador do usuário. Você vai entender o sistema completo (servidor, banco de dados, API), mas vai construir a parte que o usuário vê e toca. O back-end é assunto do Nível 2.
Os três marcos incidem sobre o mesmo site: o Marco 1 constrói a estrutura em HTML, o Marco 2 aplica o design com CSS, o Marco 3 acrescenta o comportamento com JavaScript. Por isso é importante escolher hoje um tema que você aguente desenvolver até o fim da trilha.
Apresentação da trilha; tecnologias e arquitetura da Web
02
Introdução ao HTML: estrutura, textos, links, tabelas
03
Introdução ao formulário
04
Formulário, mídias e listas
05
Elementos HTML para layout e introdução ao CSS
06
CSS: sintaxe, seletores, classes, atributos e valores — Marco 1 do projeto
07
Formatando o layout de um website e o menu
08
Criando telas responsivas
09
Animações e efeitos em CSS
10
Introdução ao JavaScript — Marco 2 do projeto
11
Variáveis, operações aritméticas e estruturas de controle
12
Estruturas sequenciais, condicionais e de repetição
13
Funções e eventos
14
JavaScript para validação de formulários e consultas dinâmicas
15
Publicando seu website na internet — Marco 3 do projeto
O conteúdo abaixo é o mesmo em qualquer oferta: serve igualmente a quem estuda por conta própria, sem data alguma, e a quem cursa esta trilha em uma turma com professor e calendário próprios — nesse caso, é o professor quem define as datas.
Marco
Escopo
1
Site em HTML com os elementos da Unidade 1 (estrutura, textos, links, tabelas, formulários, mídias, listas).
2
O mesmo site estilizado com CSS: layout, menu, responsividade e animações.
3
O site dinâmico e interativo com JavaScript: eventos, validação de formulários e consultas dinâmicas.
Os blocos 📌 Vale gravar espalhados pelas aulas destacam os pontos que mais reaparecem mais adiante — vale mesmo memorizar.
Esta trilha soma aproximadamente 60 h de estudo: cerca de 45 h acompanhando as 15 aulas (teoria e prática guiada, em três blocos de 50 min cada) e 15 h nas atividades assíncronas — uma por aula, feita por conta própria depois do conteúdo principal.
⚠️ Atenção
As atividades assíncronas não são extras opcionais. Elas compõem a prática real da trilha e alimentam o Marco do projeto da unidade correspondente. Pule uma e você sente falta dela mais adiante.
O projeto fio-condutor é construído ao longo da trilha — o site de um evento acadêmico, a "Semana Acadêmica de Sistemas de Informação". São cinco páginas: início, programação, inscrição, palestrantes e contato. Na Unidade 1 elas nascem em HTML puro; na Unidade 2 ganham CSS; na Unidade 3 ganham JavaScript. Se você está em aula com um professor, é o projeto que ele constrói com a turma, passo a passo, e todo mundo digita junto; se está estudando sozinho, é o que você constrói acompanhando cada Mão na massa.
Cada pessoa desenvolve também um projeto autoral com a mesma arquitetura (cinco páginas, mesma sequência de tecnologias) e um domínio diferente. Os marcos do projeto acompanham o projeto autoral, não o site do evento.
Exemplos de temas que funcionam bem: catálogo de plantas do Pantanal, agenda de quadras esportivas, mural de estágios do curso, brechó de roupas, controle de pescarias no Teles Pires, cardápio de um restaurante, portfólio de um fotógrafo, site de uma ONG de proteção animal. O critério é: você consegue pensar em cinco páginas com conteúdo real para esse tema? Se sim, serve.
💡 Dica
Escolha um tema que você goste e sobre o qual tenha conteúdo (textos, dados para tabelas, fotos). O erro mais comum é escolher algo genérico demais ("site de uma empresa") e ficar sem o que escrever na página. Tema concreto gera site melhor.
Cada aula do WebLab tem quatro camadas de prática, sempre nesta ordem: 💻 Mão na massa (o passo a passo guiado, que você acompanha digitando junto — em aula ou sozinho), 🧪 Laboratório (exercícios em três níveis), 🏆 Desafios (extras, opcionais, com estrelas de dificuldade) e 🏠 Para praticar depois da aula (a tarefa de 1 h ligada ao seu projeto autoral).
Uma rotina que funciona:
Antes de estudar cada aula, leia os objetivos e passe os olhos no conteúdo. Você aproveita muito mais sabendo aonde ela vai chegar.
Ao acompanhar o conteúdo, digite o código junto. Copiar e colar não fixa nada; digitar e errar, sim.
Logo em seguida, faça o Laboratório Nível A. São perguntas curtas que consolidam o vocabulário.
Nos dias seguintes, faça o Nível B e a Atividade assíncrona. São eles que constroem habilidade de verdade.
Se sobrar tempo e vontade, encare o Nível C e os Desafios. São do tamanho de um item de portfólio — o tipo de coisa que impressiona em uma entrevista de emprego.
Programação não se aprende lendo. Se você fechar esta página achando que entendeu tudo e não tiver escrito código, não aprendeu. Abra o editor.
São coisas diferentes, e confundi-las é o primeiro erro conceitual da área.
A Internet é a infraestrutura física e lógica: cabos, fibras, roteadores, satélites e um conjunto de protocolos (principalmente TCP/IP) que permitem que máquinas em qualquer lugar do planeta troquem pacotes de dados. A Internet existe desde os anos 1970 (ARPANET) e transporta muito mais do que páginas: e-mail (SMTP), transferência de arquivos (FTP), streaming, chamadas de voz (VoIP), jogos online, o próprio DNS.
A World Wide Web é uma aplicação que roda sobre a Internet. Foi proposta por Tim Berners-Lee no CERN em 1989 e é composta por três invenções combinadas:
Invenção
Função
URL (Uniform Resource Locator)
Um endereço universal para identificar qualquer recurso
HTTP (HyperText Transfer Protocol)
Um protocolo para pedir e receber esses recursos
HTML (HyperText Markup Language)
Uma linguagem para descrever documentos com links entre si
💡 Dica
Analogia: a Internet é o sistema rodoviário (asfalto, placas, regras de trânsito). A Web é o serviço de entregas que usa essas estradas. WhatsApp, e-mail e jogos online são outros serviços que usam as mesmas estradas sem serem a Web.
Repare que as três invenções são exatamente o que você vai estudar: URLs (hoje), HTTP (hoje e no Nível 2) e HTML (a partir da próxima aula). A Web é, no fundo, uma ideia simples: documentos com endereço, que apontam uns para os outros.
Berners-Lee propõe e implementa a Web no CERN; o primeiro site vai ao ar
1993
O navegador Mosaic populariza a Web com imagens
1994
Fundação do W3C (World Wide Web Consortium)
1995
Nasce o JavaScript (Brendan Eich, Netscape, em cerca de 10 dias)
1996
Primeira recomendação do CSS
1999–2005
Ajax e o conceito de "Web 2.0": páginas que atualizam sem recarregar
2014–2015
HTML5 vira recomendação do W3C; ES2015 moderniza o JavaScript
Hoje
Padrões vivos (living standards) mantidos por WHATWG, W3C e TC39
🧠 Você sabia?
O primeiro site do mundo continua no ar, no endereço original: http://info.cern.ch/hypertext/WWW/TheProject.html. É só HTML, sem uma linha de CSS ou JavaScript — e abre em qualquer navegador moderno em milissegundos. Essa compatibilidade com o passado é uma decisão de projeto da Web: um navegador de hoje precisa continuar exibindo uma página de 1991. Poucas plataformas de software conseguem dizer o mesmo.
Praticamente toda a Web funciona sobre um modelo de requisição e resposta entre dois papéis:
Cliente: quem pede. Na Web, tipicamente o navegador (Chrome, Firefox, Safari, Edge). Também pode ser um aplicativo de celular, um script, outro servidor.
Servidor: quem responde. Um computador executando um software servidor web (Apache, Nginx, Node.js) que fica permanentemente à espera de requisições.
Texto
CLIENTE SERVIDOR
┌───────────┐ 1. Requisição HTTP ┌───────────┐
│ Navegador │ ────────────────────────────> │ Servidor │
│ │ GET /index.html │ Web │
│ │ │ │
│ │ <──────────────────────────── │ │
└───────────┘ 2. Resposta HTTP └───────────┘
200 OK + HTML
Três características definem esse modelo:
O cliente sempre inicia. O servidor nunca "manda" nada espontaneamente numa conexão HTTP tradicional — ele só responde ao que foi pedido.
HTTP é stateless (sem estado). Cada requisição é independente: o servidor não lembra, por si só, o que aconteceu na requisição anterior. Sessões, logins e carrinhos de compra são construídos por cima disso, com cookies e tokens.
Uma página não é um arquivo, são dezenas. Abrir uma página comum dispara de 30 a 200 requisições: o HTML, cada folha de estilo, cada script, cada imagem, cada fonte.
⚠️ Atenção
Consequência de segurança, importante desde já: todo código front-end é público. O usuário pode ler seu JavaScript, alterar valores e burlar validações. Validação no cliente é para conforto do usuário; validação no servidor é para segurança. Nunca confie apenas na validação client-side — você vai ver isso na prática na Aula 03 e voltaremos ao tema na Unidade 3.
Nesta trilha trabalhamos exclusivamente com front-end, mas com consciência do sistema completo.
Este é um dos assuntos mais cobrados em entrevistas técnicas. Aprenda a sequência.
Passo 1 — Análise da URL. O navegador separa o endereço em partes (§5) e descobre qual protocolo usar e qual servidor contatar.
Passo 2 — Resolução DNS. O nome www.exemplo.com.br não serve para roteamento; a rede trabalha com endereços IP. O navegador consulta o DNS (Domain Name System), um serviço distribuído de diretórios, para traduzir o nome em um IP. Antes de sair para a rede, ele consulta caches, nesta ordem: cache do navegador → cache do sistema operacional → arquivo hosts → servidor DNS do provedor.
Passo 3 — Conexão TCP. Com o IP em mãos, o navegador abre uma conexão TCP com o servidor, normalmente na porta 80 (HTTP) ou 443 (HTTPS), usando o three-way handshake (SYN → SYN-ACK → ACK).
Passo 4 — Handshake TLS (apenas em HTTPS). Cliente e servidor negociam algoritmos de criptografia, o servidor apresenta seu certificado digital e ambos combinam uma chave de sessão. É isso que produz o cadeado na barra de endereço.
Passo 5 — Requisição HTTP. O navegador envia um texto parecido com este:
HTTP
GET/cursos/sistemasHTTP/1.1Host:www.exemplo.com.brUser-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/139.0Accept:text/html,application/xhtml+xmlAccept-Language:pt-BR,pt;q=0.9Connection:keep-alive
A primeira linha diz o que se quer (GET) e onde (/cursos/sistemas). As demais são cabeçalhos (headers): metadados no formato Nome: valor. Repare que é texto puro, legível — HTTP foi projetado para ser simples.
Passo 6 — Processamento no servidor. O servidor identifica o recurso pedido. Se for um arquivo estático, apenas o lê do disco. Se for dinâmico, executa código (PHP, Node, Java), possivelmente consulta um banco de dados e gera o HTML na hora.
Passo 7 — Resposta HTTP. O servidor devolve outro texto, com uma linha de status, cabeçalhos e, depois de uma linha em branco, o conteúdo:
HTTP
HTTP/1.1200OKDate:Wed, 12 Aug 2030 22:14:05 GMTContent-Type:text/html; charset=UTF-8Content-Length:8420Cache-Control:max-age=3600<!DOCTYPE html><htmllang="pt-BR"><head><title>Sistemas de Informação</title></head>
Passo 8 — Renderização. O navegador processa a resposta (detalhado na §6) e dispara novas requisições para cada recurso referenciado no HTML: folhas de estilo, scripts, imagens, fontes. Cada uma repete os passos 5 a 7.
🔎 Por baixo do capô
Por que tanto cache de DNS? Porque cada consulta que sai para a rede custa dezenas de milissegundos — e uma página faz dezenas de requisições, muitas para domínios diferentes. Sem cache, só a tradução de nomes já deixaria a Web visivelmente lenta. O mesmo raciocínio (guardar o que já foi obtido para não pedir de novo) aparece no cabeçalho Cache-Control da resposta acima, e é o tema do desafio C1 de hoje.
📌 Vale gravar
A ordem das etapas (URL → DNS → TCP → TLS → requisição → processamento → resposta → renderização) e o que cada uma faz. Saber que o DNS traduz nome em IP e que HTTP é stateless são perguntas frequentes.
Qual serviço na máquina (80 = HTTP, 443 = HTTPS; omitida quando é a padrão)
4
/cursos/sistemas
Caminho (path)
Qual recurso dentro do servidor
5
?campus=sinop&turno=noite
Query string
Parâmetros no formato chave=valor, separados por &
6
#ementa
Fragmento / âncora
Posição dentro da página. Nunca é enviado ao servidor — é processado só pelo navegador
O fragmento (#) vai ser seu aliado a partir da Aula 02: é ele que faz um link "pular" para uma seção da mesma página. A query string (?) vai aparecer na Aula 03, quando um formulário enviado com GET grava os campos digitados na própria URL.
Você vai usar isto em toda aula a partir de agora, em links, imagens, folhas de estilo e scripts:
Notação
Significado
Exemplo
pagina.html
Mesma pasta do arquivo atual
contato.html
./pagina.html
Idem, de forma explícita
./contato.html
imagens/foto.jpg
Subpasta
img/logo.png
../pagina.html
Uma pasta acima
../index.html
/pagina.html
A partir da raiz do site
/sobre.html
https://site.com/x
Absoluto, outro servidor
link externo
O ponto de partida de um caminho relativo é sempre a pasta do arquivo onde o caminho está escrito, não a pasta do projeto. Se paginas/contato.html precisa da imagem img/logo.png que está na raiz, o caminho correto é ../img/logo.png: sobe um nível, entra em img.
⚠️ Atenção
Erro clássico: usar C:\Users\Ivan\Documents\site\logo.png no src de uma imagem. Funciona na sua máquina e quebra em todas as outras. Caminhos de sistema de arquivos não existem na Web — o servidor só conhece a pasta do site.
O navegador não "mostra o HTML". Ele executa um pipeline:
Texto
HTML ──parsing──> DOM ┐
├──> Render Tree ──> Layout ──> Paint ──> Composite
CSS ──parsing──> CSSOM ┘
▲
JavaScript ─────────────────┘ (pode alterar DOM e CSSOM a qualquer momento)
Parsing do HTML → DOM. O navegador lê o HTML caractere a caractere e monta uma árvore de objetos chamada DOM (Document Object Model). Cada tag vira um nó da árvore.
Parsing do CSS → CSSOM. As regras de estilo viram outra árvore, o CSSOM.
Render Tree. DOM + CSSOM são combinados, descartando o que não é visível (por exemplo, o que tem display: none).
Layout (reflow). Cálculo da posição e do tamanho exatos de cada caixa na tela.
Paint. Preenchimento de pixels: cores, textos, bordas, sombras.
Composite. Montagem das camadas na tela.
Cada navegador tem um motor de renderização: Blink (Chrome, Edge, Opera), Gecko (Firefox), WebKit (Safari). Diferenças entre motores são a razão de existirem os padrões — e a razão de os sites às vezes ficarem diferentes em cada navegador.
🔎 Por baixo do capô
Por que JavaScript bloqueia o parsing: quando o parser encontra um <script> sem atributos, ele para de montar o DOM, baixa e executa o script, e só então continua. Por isso a boa prática de colocar <script> antes de </body> ou usar o atributo defer. Você vai ver isso de perto na Aula 10, quando o JavaScript entrar em cena.
O DOM é uma das ideias mais importantes do semestre. Guarde desde já: o arquivo .html no disco é texto; o DOM é a árvore em memória que o navegador constrói a partir dele. O DevTools mostra o DOM, não o arquivo — e o JavaScript, na Unidade 3, vai manipular o DOM, não o arquivo.
A separação entre eles é chamada de separação de responsabilidades (separation of concerns) e é um princípio de engenharia, não capricho: permite trocar o visual sem tocar no conteúdo, reaproveitar estilos entre páginas e manter o código legível por equipes diferentes.
Um mesmo HTML com três CSS distintos vira três sites visualmente diferentes. Esse é o ponto — e é exatamente o que vai acontecer com o site do evento: o HTML que você escreve na Unidade 1 não muda quando o CSS chega na Unidade 2.
Especificações da Web, com destaque para CSS e acessibilidade
WHATWG
HTML e DOM como living standard
TC39 / Ecma International
ECMAScript, a especificação da linguagem JavaScript
IETF
Protocolos de rede (HTTP, TCP/IP), publicados como RFCs
Nenhuma empresa é dona da Web. Os navegadores implementam especificações públicas, escritas em processo aberto. É por isso que a documentação oficial (MDN, especificações do W3C e da WHATWG) vale mais do que qualquer tutorial — e é para ela que os links de "Para aprofundar" apontam.
Tudo em um único programa, em uma única máquina. Não é Web — é o modelo de um aplicativo desktop antigo, com interface, regras e dados no mesmo executável.
[ Cliente com lógica ] <──────> [ Servidor de Banco de Dados ]
O cliente é "gordo" (fat client): contém a interface e as regras de negócio, e conversa diretamente com o banco. Problema: qualquer mudança de regra exige reinstalar o programa em todas as máquinas, e o banco fica exposto na rede.
Regras de negócio, validação, autenticação, orquestração
Servidor
Dados
Armazenamento, integridade, consultas
Servidor de banco
Vantagens: cada camada pode ser escalada, atualizada e substituída de forma independente; o banco nunca fica exposto ao cliente; as regras existem em um único lugar.
Esta trilha inteira vive na camada de apresentação. O Nível 2 constrói a camada de aplicação (Node.js + Express) e o Nível 3 integra as três.
Sistemas grandes fragmentam ainda mais: camada de cache (Redis), fila de mensagens (RabbitMQ, Kafka), balanceador de carga (Nginx), serviços independentes por domínio de negócio. Ganha-se escalabilidade e autonomia de equipes; paga-se com complexidade operacional e latência de rede. Nada disso é assunto deste nível, mas você vai reconhecer os nomes quando ler uma vaga de emprego.
⚠️ Atenção
Confusão frequente: "dinâmico", no sentido de arquitetura, significa HTML gerado no servidor. Uma página estática com muito JavaScript e animações continua sendo estática do ponto de vista arquitetural. O SIGAA é dinâmico. Um portfólio com carrossel animado é estático. O site do evento que vamos construir é estático — e vai ser publicado de graça, por isso mesmo.
MPA — Multi Page Application. Cada navegação carrega um novo documento HTML completo do servidor. É o modelo tradicional. Vantagens: simples, ótimo para buscadores (SEO), funciona sem JavaScript. Desvantagem: a tela "pisca" a cada clique.
SPA — Single Page Application. O servidor entrega um HTML e um pacote JavaScript. A partir daí, o JavaScript intercepta a navegação, busca apenas os dados (via API, em JSON) e reescreve o DOM. Vantagens: sensação de aplicativo, menos tráfego após a carga inicial. Desvantagens: carga inicial pesada, complexidade, SEO exige trabalho extra, quebra sem JavaScript. Frameworks típicos: React, Vue, Angular, Svelte.
Texto
MPA: clique ──> servidor ──> HTML completo ──> recarrega a página
SPA: clique ──> JS intercepta ──> API ──> JSON ──> JS reescreve o DOM
O site do evento é uma MPA: cinco arquivos .html, um por página. É o ponto de partida certo — SPAs são o assunto do Nível 2 (Unidade 2) e do Nível 3 inteiro.
Uma API (Application Programming Interface) é um contrato pelo qual dois sistemas conversam. Na Web, o estilo dominante é o REST, que usa os próprios verbos do HTTP:
Verbo HTTP
Ação
Exemplo
GET
Ler
GET /api/alunos — lista alunos
POST
Criar
POST /api/alunos — cadastra aluno
PUT / PATCH
Atualizar
PUT /api/alunos/42
DELETE
Remover
DELETE /api/alunos/42
Perceba que essas quatro operações são exatamente o CRUD (Create, Read, Update, Delete). Neste nível você vai reconhecer o CRUD nas requisições que o navegador faz; a leitura de dados aparece nas consultas dinâmicas da Aula 14, e a escrita no servidor é assunto do Nível 2.
O formato de troca padrão é o JSON (JavaScript Object Notation) — texto, legível, com pares chave: valor:
JSON
{"id":42,"nome":"Maria Silva","curso":"Sistemas de Informação","fase":2,"ativo":true,"disciplinas":["Desenvolvimento Web","Banco de Dados"]}
400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found
5xx
Erro do servidor
500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable
💡 Dica
Mnemônico: 4xx é culpa de quem pediu; 5xx é culpa de quem respondeu. Um 404 diz "você pediu algo que não existe"; um 500 diz "eu quebrei ao tentar responder".
🔬 Investigue
Abra um site que você usa bastante (rede social, portal de notícias, e-commerce), pressione F12, vá à aba Network e recarregue com Ctrl+F5. Observe: (1) quantas linhas apareceram — cada uma é uma requisição; (2) a coluna Type: document, stylesheet, script, png, font; (3) clique na primeira linha, abra Headers e procure Content-Type e Server na resposta; (4) na barra inferior, leia o total de requisições e o peso transferido. Você acabou de ver o "uma página são dezenas de arquivos" acontecendo — e o cabeçalho Server conta qual software respondeu.
CDN (Content Delivery Network): rede de servidores espalhados geograficamente que guardam cópias dos arquivos. Um usuário em Sinop recebe o arquivo de um nó em São Paulo, não da Califórnia. Reduz a latência drasticamente. Exemplos: Cloudflare, Akamai.
Hospedagem estática: GitHub Pages, Netlify, Vercel, Cloudflare Pages — gratuitas e suficientes para tudo que faremos nesta trilha. O projeto do Marco 3 (Aula 15) é publicado em uma delas.
DNS e domínio: registro de domínios .br via Registro.br. Um domínio próprio custa por volta de R$ 40 por ano; a trilha Deploy (capítulo 04) mostra como apontar um para o seu site.
💻 Mão na massa — Pasta do projeto, primeiro HTML e DevTools¶
Hoje nasce a pasta que você vai usar até o fim da trilha e o primeiro arquivo do site do evento.
Crie, no seu computador (em Documentos, por exemplo), a seguinte estrutura. Por enquanto só as pastas e um arquivo:
Texto
introducao-web/
├── site-evento/ ← projeto fio-condutor (construído aula a aula)
│ ├── index.html
│ ├── css/
│ ├── img/
│ └── js/
├── meu-projeto/ ← projeto autoral (tema seu, mesma estrutura)
└── exercicios/
├── aula01/
├── aula02/
└── aula03/
⚠️ Atenção
Regra permanente: nomes de arquivos e pastas em minúsculas, sem espaços, sem acentos. Use hífen para separar palavras (sobre-nos.html, nunca Sobre Nós.html). Servidores Linux diferenciam maiúsculas de minúsculas — Index.html e index.html são arquivos distintos para eles, e o que funciona no Windows pode quebrar quando você publicar.
Abra o VS Code, vá em File → Open Folder e abra a pasta introducao-web. Sempre abra a pasta raiz, não um arquivo solto: o Live Server e os caminhos relativos dependem disso.
No painel esquerdo, clique com o botão direito em site-evento → New File → index.html. Digite (não cole):
site-evento/index.html
HTML
<!DOCTYPE html><htmllang="pt-BR"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Semana Acadêmica de Sistemas de Informação</title></head><body><h1>Semana Acadêmica de Sistemas de Informação</h1><p>Três dias de palestras, minicursos e oficinas.</p><p>Este site está sendo construído com o WebLab, na trilha <strong>Introdução ao Desenvolvimento Web</strong>.</p></body></html>
Cada linha será explicada na Aula 02. Por hoje, o essencial: <!DOCTYPE html> diz que é HTML5; <head> guarda informações sobre a página; <body> guarda o que aparece na tela; e <meta charset="UTF-8"> diz ao navegador como ler os acentos.
Clique com o botão direito no arquivo index.html → Open with Live Server (ou clique em Go Live no canto inferior direito). O navegador abre em um endereço parecido com:
Texto
http://127.0.0.1:5500/site-evento/index.html
Leia essa URL com a §5 na cabeça: esquema http, host 127.0.0.1, porta 5500, caminho /site-evento/index.html. Você acabou de subir um servidor web na sua máquina — 127.0.0.1 é o endereço que todo computador usa para se referir a si mesmo (também chamado de localhost). A partir de agora o navegador é o cliente e o Live Server é o servidor, exatamente como no diagrama da §3.
Altere o texto do <p>, salve com Ctrl+S e olhe o navegador: ele recarrega sozinho. É isso que o Live Server faz.
Passo 5 — Ferramentas do desenvolvedor (DevTools)¶
Pressione F12 (ou Ctrl+Shift+I). Você vai usar isto todos os dias do semestre.
Aba
Para que serve
Elements
Ver e editar o DOM e o CSS aplicado, ao vivo
Console
Mensagens de erro e execução de JavaScript
Network
Todas as requisições: método, status, tamanho, tempo
Sources
Ver arquivos e depurar JavaScript com breakpoints
Application
Cookies e localStorage (usaremos na Unidade 3)
Lighthouse
Auditoria de desempenho, acessibilidade e SEO
Roteiro de exploração — faça agora, na sua página:
Elements: dê duplo clique no texto do <h1> e altere. Note que muda na tela mas não no arquivo — você está editando o DOM em memória, não o HTML. Recarregue e a alteração some.
Console: digite document.title e pressione Enter. Aparece o texto do seu <title>. Digite document.body.children.length — quantos filhos diretos o <body> tem?
Network: marque Disable cache, recarregue com Ctrl+F5 e observe o status 200 ao lado de index.html. Agora renomeie o arquivo para inicio.html e recarregue: status 404. Renomeie de volta.
Abra em outra aba um site que você visita com frequência e, na aba Network, conte quantas requisições a página faz e qual é o maior arquivo (clique no cabeçalho da coluna Size para ordenar).
Copie site-evento/index.html para meu-projeto/index.html e troque o <title>, o <h1> e os parágrafos pelo tema do seu projeto. Abra também com o Live Server. Está feito o primeiro commit mental: a partir de agora, tudo que o site do evento ganha nesta trilha, o seu projeto ganha também.
O Live Server abre http://127.0.0.1:5500/site-evento/index.html e a página mostra o título e os parágrafos com acentos corretos.
Alterar e salvar o arquivo recarrega o navegador sozinho.
Na aba Network, index.html aparece com status 200.
No Console, document.title devolve "Semana Acadêmica de Sistemas de Informação".
meu-projeto/index.html abre com o seu tema.
Resultado esperado: duas páginas simples no ar, servidas pelo Live Server; o DevTools aberto e explorado; a estrutura de pastas pronta para o semestre.
A1. Explique, com suas palavras e em no máximo 4 linhas, a diferença entre Internet e World Wide Web.
A2. Classifique cada item como Internet ou Web: (a) protocolo TCP/IP; (b) HTML; (c) e-mail via SMTP; (d) HTTP; (e) cabos de fibra óptica submarinos; (f) URL.
A3. Decomponha a URL abaixo, nomeando cada uma das seis partes:
B1. Usando a aba Network do DevTools, acesse três sites diferentes (um portal de notícias, um e-commerce e o site da sua universidade ou escola) e registre, para cada um: número de requisições, peso total transferido (KB ou MB), tempo de carregamento (Load, na barra inferior) e qual foi o maior recurso. Escreva um parágrafo comparando os resultados e levantando hipóteses sobre as diferenças.
Resultado esperado: uma tabela com três linhas e quatro medidas cada, mais um parágrafo de análise (por exemplo: "o portal de notícias fez 3× mais requisições por causa de anúncios e rastreadores").
Dica
Marque Disable cache antes de medir, senão a segunda visita vem do cache e distorce a comparação. A barra de resumo na parte inferior da aba Network mostra "N requests | X MB transferred | Load: Y s".
B2. Crie exercicios/aula01/sobre-mim.html contendo: título da página no <title>, um <h1> com seu nome, um parágrafo de apresentação, um parágrafo com seus objetivos nesta trilha e um link para o site de uma universidade ou escola (a sua, se estiver cursando alguma) que abra em nova aba (pesquise o atributo target).
Resultado esperado: a página abre no Live Server com acentos corretos; o link abre o site escolhido em outra aba.
Dica
Parta do index.html que você criou no Mão na massa. O link é algo como <a href="https://www.wikipedia.org" target="_blank">Wikipédia</a>, com o href trocado pelo site escolhido. Na Aula 02 você vai descobrir por que esse link precisa de um atributo extra de segurança.
B3. Desenhe (no papel, no Draw.io ou no Excalidraw) um diagrama do ciclo requisição-resposta incluindo: usuário, navegador, DNS, Internet, servidor web e banco de dados. Indique com setas numeradas a ordem dos eventos.
Resultado esperado: um diagrama com pelo menos 8 setas numeradas, do "usuário digita a URL" ao "navegador renderiza".
Dica
Siga os oito passos da §4. O banco de dados só entra no Passo 6, e só se o site for dinâmico — represente isso.
B4. No Console do DevTools, execute os comandos abaixo em uma página qualquer e registre o resultado de cada um, explicando o que retornaram:
Resultado esperado: cinco linhas de saída com a sua explicação; você deve conseguir relacionar protocol e hostname com as partes 1 e 2 da URL da §5.
Dica
window.location é um objeto que representa a URL atual, já decomposta. Experimente também window.location.port e window.location.hash na sua página do Live Server.
B5. Pesquise e escreva um resumo de meia página sobre a diferença entre HTTP/1.1, HTTP/2 e HTTP/3, focando em: multiplexação, cabeçalhos e protocolo de transporte usado.
Resultado esperado: texto de 15 a 25 linhas, com pelo menos uma fonte citada (a MDN serve).
Dica
Comece por "HTTP/2 multiplexação" e "HTTP/3 QUIC" na MDN. Na aba Network, a coluna Protocol (ative-a clicando com o botão direito no cabeçalho das colunas) mostra h2 ou h3 para cada requisição — veja o que os sites que você visita usam.
B6. Abra um site com HTTPS, clique no cadeado e examine o certificado. Registre: quem emitiu, para qual domínio é válido, data de validade e qual o algoritmo de chave. Explique para que serve cada informação.
Resultado esperado: quatro dados anotados e uma explicação de 2 a 3 linhas para cada um.
Dica
No Chrome: cadeado → A conexão é segura → O certificado é válido. O "emissor" é a autoridade certificadora (Let's Encrypt, DigiCert); ela é quem garante ao navegador que o servidor é quem diz ser — é o Passo 4 da §4.
C1. Investigação de cache. Acesse um mesmo site duas vezes na aba Network: uma com Disable cache marcado e outra desmarcado. Compare o número de requisições e o peso transferido. Identifique quais recursos vieram do cache (procure por (disk cache), (memory cache) ou status 304). Produza um relatório de 1 página explicando o que é cache HTTP, o papel do cabeçalho Cache-Control e por que ele é essencial para a performance da Web.
Dica
Na segunda visita, a coluna Size mostra "(disk cache)" em vez de um tamanho — esses arquivos nem saíram para a rede. O status 304 Not Modified é diferente: a requisição saiu, mas o servidor respondeu "você já tem a versão atual". Clique em um recurso e leia Cache-Control e ETag nos cabeçalhos de resposta.
Toda resposta HTTP carrega cabeçalhos que contam quem a produziu — e muitos sites deixam pistas: qual servidor web, se passou por uma CDN, há quanto tempo o arquivo está em cache. Hoje você vira detetive: escolha três sites (o da sua universidade ou escola, um jornal e um e-commerce) e descubra, só pelos cabeçalhos, como cada um é entregue.
Critérios de pronto
Para cada site, uma tabela com os valores dos cabeçalhos de resposta do documento HTML principal: Server, Content-Type, Cache-Control e pelo menos um cabeçalho que revele CDN (por exemplo cf-ray, x-served-by, via, x-cache).
Uma classificação de cada site em "servido direto" ou "servido via CDN", com a evidência.
Uma linha explicando o que o valor de Cache-Control de cada site significa na prática.
Pistas
Aba Network → clique na primeira linha (o documento) → Headers → role até Response Headers.
Cloudflare deixa cf-ray e server: cloudflare; Fastly deixa x-served-by; Akamai costuma deixar x-akamai-* ou server: AkamaiGHost.
Se Server estiver ausente, o site está escondendo o software de propósito — isso também é uma resposta (e uma prática de segurança).
Leia a página da MDN sobre Cache-Control para traduzir max-age, no-cache e public.
A tabela da §2 resume meio século em dez linhas. Ela merece mais: crie uma página HTML com uma linha do tempo de pelo menos 10 marcos da história da Web (1969 até hoje), cada um com ano, título e um parágrafo de 2 a 3 linhas explicando sua importância. Use apenas HTML nesta etapa — o CSS entra a partir da Aula 05 e você vai reaproveitar exatamente esta página para estilizá-la.
Critérios de pronto
Arquivo exercicios/aula01/linha-do-tempo.html com a estrutura mínima completa (doctype, lang, charset, viewport, title).
Pelo menos 10 marcos, em ordem cronológica, cada um com ano, título e parágrafo.
Pelo menos 3 marcos que não estão na tabela da §2 (pesquise: o primeiro navegador gráfico, a criação do Google, o lançamento do iPhone e o efeito na Web móvel, o nascimento do Node.js).
Uma fonte citada ao final da página, com link.
Pistas
Você ainda não conhece os elementos de lista e de título — use <h2> para o ano + título e <p> para o texto; na Aula 02 você reescreve com <ol> e <time>.
A Wikipédia em português tem um artigo "História da World Wide Web" com datas confiáveis.
Pense na página como um documento que alguém vai ler de cima a baixo sem nenhum estilo: a ordem e a hierarquia dos títulos precisam contar a história sozinhas.
Escolha um domínio real (o site de uma universidade, um jornal, uma loja). Quantos computadores um pacote atravessa entre a sua máquina e o servidor dele? A resposta está em um comando que existe em todo sistema operacional. No terminal, execute tracert dominio-escolhido (Windows) ou traceroute dominio-escolhido (Linux/macOS). Registre a saída e responda: quantos saltos foram necessários? O que representa cada linha? Qual o tempo total? Relacione o resultado com o conceito de comutação de pacotes.
Critérios de pronto
A saída completa do comando, copiada em um bloco de texto.
O número de saltos e o tempo aproximado do último salto.
Uma explicação, por escrito, do que são as três medidas de tempo em cada linha e por que algumas linhas mostram * * *.
Uma comparação com nslookup dominio-escolhido (ou dig): qual IP foi resolvido, e ele bate com o destino do traceroute?
Um parágrafo relacionando o resultado com "a Internet é uma rede de comutação de pacotes".
Pistas
No Windows, abra o Prompt de Comando ou o PowerShell; no Linux/macOS, o Terminal. Se traceroute não existir no Linux, instale com sudo apt install traceroute.
Cada linha é um roteador no caminho; as três medidas são três tentativas de ida e volta (RTT). * * * significa que aquele roteador não respondeu ao pacote de teste — não que a rota parou.
Os primeiros saltos são a sua rede local e o seu provedor; os últimos, o data center do destino. Os nomes dos roteadores costumam entregar a cidade (procure siglas como spo, cgb, gru).
Rode duas vezes em horários diferentes e compare os tempos.
O navegador esconde tudo o que a §4 descreve. Hoje você faz o trabalho dele manualmente: dispara uma requisição HTTP sem navegador, lê a resposta crua e compara protocolos. A ferramenta é o curl, que já vem instalado no Windows 10+, no macOS e na maioria das distribuições Linux. Escolha um site HTTPS (pode ser o mesmo domínio do desafio anterior). Ao final, você vai conseguir explicar para outra pessoa, com evidências, cada etapa do "o que acontece quando digito uma URL".
Critérios de pronto
A saída de curl -v https://dominio-escolhido salva em um arquivo, com anotações marcando: a resolução DNS, a conexão TCP, o handshake TLS (algoritmo negociado e emissor do certificado), a requisição enviada (linhas com >) e a resposta recebida (linhas com <).
A saída de curl -I https://dominio-escolhido (só cabeçalhos) com uma explicação, em uma linha cada, de pelo menos 5 cabeçalhos de resposta.
Uma requisição a um caminho inexistente (curl -I https://dominio-escolhido/nao-existe) e a interpretação do status recebido.
Uma comparação entre curl --http1.1 -I e curl --http2 -I no mesmo site: qual versão o servidor aceitou, e como você sabe?
Um texto de 10 linhas, escrito como se fosse explicar a outra pessoa, ligando cada evidência ao passo correspondente da §4.
Pistas
curl --version confirma que o comando existe e lista os protocolos suportados (procure HTTP2 e HTTP3 na linha Features).
Em -v, as linhas que começam com * são informações da conexão (DNS, TCP, TLS); > é o que foi enviado; < é o que voltou.
Se o site redirecionar (status 301 ou 302), adicione -L para seguir o redirecionamento e observe as duas respostas.
A primeira linha da resposta (HTTP/2 200 ou HTTP/1.1 200 OK) já denuncia a versão do protocolo negociada.
Parte 1 — Leitura (20 min). SILVA, M. S. Criando sites com HTML, capítulo introdutório. MILETTO, E. M.; BERTAGNOLLI, S. C. Desenvolvimento de software II, capítulo sobre arquiteturas para a Web. Anote duas ideias de cada texto que não apareceram nesta aula.
Parte 2 — Produção (30 min). Produza:
O exercício B2 (sobre-mim.html) e o exercício B5 (resumo sobre HTTP/1.1, 2 e 3).
Uma captura de tela do VS Code com a pasta introducao-web aberta e outra do Live Server exibindo site-evento/index.html, com a URL 127.0.0.1:5500 visível.
O tema do seu projeto autoral, em 3 linhas: o que é, para quem é, e o nome das cinco páginas (a versão "início, programação, inscrição, palestrantes, contato" do seu domínio).
Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —, escreva 10 a 15 linhas sobre por que a Web se tornou a plataforma dominante de aplicações — pense em distribuição (não precisa instalar nada), padrões abertos e compatibilidade entre dispositivos. Se puder, compare sua resposta com a de outra pessoa que esteja estudando o mesmo conteúdo.
Critério de pronto: os dois arquivos abrem sem erro de codificação; as duas capturas mostram o ambiente funcionando; o tema tem cinco páginas nomeadas.
Guarde: os arquivos .html e as capturas na pasta exercicios/aula01/ — a partir da Aula 15 tudo isso passa a viver em um repositório Git.
SILVA, Maurício Samy. Criando sites com HTML: sites de alta qualidade com HTML e CSS. Novatec, 2008 — capítulo 1.
TERUEL, Evandro C. HTML 5 — Guia Prático. Saraiva, 2014 — introdução (disponível na Minha Biblioteca).
MILETTO, Evandro M.; BERTAGNOLLI, Silvia C. Desenvolvimento de software II. Bookman, 2014 — capítulo sobre arquiteturas para a Web.
Na próxima aula, o HTML entra de verdade: você vai entender cada linha do index.html que criou hoje, aprender os elementos de texto, listas, links e tabelas, e construir a página inicial, a programação e a página de palestrantes do site do evento com HTML semântico e validado no W3C.
[ ] Pasta introducao-web/ da Aula 01 com site-evento/index.html abrindo no Live Server.
[ ] VS Code com Live Server e Prettier instalados e format on save ativado.
[ ] Navegador com DevTools — você vai usar a aba Elements o tempo todo hoje.
[ ] Tema do projeto autoral definido (entregue na atividade assíncrona da Aula 01).
Na aula passada você viu como a Web funciona por fora: cliente e servidor, URL, HTTP, o caminho de uma requisição até a renderização — e criou um index.html mínimo sem entender cada linha. Hoje você entende cada linha, aprende os elementos de texto, listas, links e tabelas, e constrói três das cinco páginas do site do evento com HTML semântico e validado.
HTML é uma linguagem de marcação, não de programação: ela não tem variáveis, condicionais ou laços. Sua função é descrever a estrutura e o significado do conteúdo por meio de marcas (tags). Você não diz ao navegador "desenhe um retângulo azul aqui"; você diz "isto é um título", "isto é um parágrafo", "isto é uma lista de três itens" — e o navegador (ou o leitor de tela, ou o buscador) decide o que fazer com essa informação.
Essa é a diferença entre HTML e um editor de texto: no Word você formata a aparência; no HTML você declara o que cada coisa é. A aparência é assunto do CSS, a partir da Aula 05.
<a href="https://github.io" title="Site oficial">GitHub</a>
└┬┘└──────────┬───────────┘ └───────┬───────┘└──┬──┘└─┬─┘
│ │ │ │ │
nome atributo 1 atributo 2 conteúdo tag
da tag de fechamento
└───────────────── tag de abertura ─────────────┘
Elemento = tag de abertura + conteúdo + tag de fechamento.
Atributo = informação adicional, sempre na tag de abertura, no formato nome="valor". Use sempre aspas duplas.
Elementos vazios não têm conteúdo nem fechamento: <br>, <hr>, <img>, <input>, <meta>, <link>.
Os nomes de tags e atributos não diferenciam maiúsculas de minúsculas, mas a convenção universal é tudo em minúsculas. Siga-a.
Elementos podem conter outros, mas precisam ser fechados na ordem inversa da abertura — como parênteses em matemática:
HTML
<!-- CORRETO --><p>Texto com <strong>destaque <em>duplo</em></strong> aqui.</p><!-- ERRADO: fechamento cruzado --><p>Texto com <strong>destaque <em>duplo</strong></em> aqui.</p>
O aninhamento é o que transforma o HTML em uma árvore — a mesma árvore que você viu na Aula 01 com o nome de DOM. <p> é pai de <strong>, que é pai de <em>. Todo elemento tem exatamente um pai (exceto <html>, a raiz) e pode ter vários filhos.
🧠 Você sabia?
O navegador nunca se recusa a exibir um HTML "errado". A especificação da WHATWG descreve, passo a passo, como o parser deve se recuperar de cada tipo de erro: uma tag cruzada é reorganizada, um </li> esquecido é inserido, um <p> aberto dentro de outro <p> fecha o anterior. É por isso que páginas quebradas "funcionam" — e por isso que o resultado às vezes não é o que você escreveu. Aliás, pela especificação, fechar <li>, <p>, <td> e <tr> é opcional; nesta trilha você fecha tudo, sempre, porque código previsível é mais fácil de ler, depurar e manter.
<!DOCTYPE html><htmllang="pt-BR"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metaname="description"content="Descrição da página em até 160 caracteres."><metaname="author"content="Seu Nome"><title>Título que aparece na aba do navegador</title><linkrel="stylesheet"href="css/estilo.css"></head><body><!-- Todo conteúdo visível fica aqui --><scriptsrc="js/script.js"></script></body></html>
Linha
Função
<!DOCTYPE html>
Declara HTML5. Sem ela, o navegador entra em quirks mode e emula bugs de navegadores antigos
lang="pt-BR"
Idioma do conteúdo. Essencial para leitores de tela (pronúncia), tradução automática e buscadores
<meta charset="UTF-8">
Codificação de caracteres. Sempre a primeira linha do <head> — você viu na Aula 01 o que acontece sem ela
viewport
Faz a página se adaptar a telas de celular. Sem ela, não há responsividade (Aula 08)
description
Texto que aparece nos resultados de busca, abaixo do título
<title>
Título da aba, dos favoritos e do resultado de busca. Obrigatório
<link rel="stylesheet">
Liga uma folha de estilo externa (Aula 05). Pode omitir enquanto não houver CSS
<script src>
Liga um arquivo JavaScript (Aula 10). Fica no fim do <body> para não bloquear a renderização
O <head> é invisível: nada dele aparece na tela. Ele carrega metadados — informações sobre a página para o navegador, os buscadores e as redes sociais. Tudo que o usuário vê fica dentro de <body>.
🔎 Por baixo do capô
O quirks mode existe por causa da história: nos anos 1990, cada navegador interpretava CSS de um jeito, e milhões de páginas foram escritas em cima desses bugs. Quando os padrões foram corrigidos, os navegadores precisaram de um jeito de saber se a página era "antiga" (renderiza com os bugs) ou "moderna" (renderiza pelo padrão). O sinal é o <!DOCTYPE>. Sem ele, o navegador assume o pior — e o seu CSS da Unidade 2 vai se comportar de forma estranha sem motivo aparente.
<h1>Título principal — apenas um por página</h1><h2>Seção</h2><h3>Subseção</h3><h4>Sub-subseção</h4>
⚠️ Atenção
Hierarquia de títulos não é escolha de tamanho de fonte — é estrutura de documento. Leitores de tela permitem navegar pulando de título em título, e o Google usa essa hierarquia para entender a página. Nunca pule níveis (h1 direto para h4) e nunca escolha <h3> só porque "ficou menor". Tamanho é assunto do CSS.
Pense nos títulos como o sumário de um livro: <h1> é o título do livro (um só), <h2> são os capítulos, <h3> as seções dentro de cada capítulo. Se você conseguir extrair só os títulos da página e eles formarem um sumário coerente, a hierarquia está certa.
<p>Um parágrafo comum de texto.</p><p>
Este é <strong>importante</strong> e este tem <em>ênfase</em>.
Este é apenas <b>negrito visual</b> e este é <i>itálico visual</i>.
<mark>Texto realçado</mark>, <small>letra miúda</small>,
<del>removido</del> e <ins>inserido</ins>.
Fórmula: H<sub>2</sub>O e potência: x<sup>2</sup>.
Código: <code>let x = 10;</code>.
Atalho: <kbd>Ctrl</kbd> + <kbd>S</kbd>.
</p>
Par de tags
Diferença
<strong> × <b>
strong = importância semântica (o leitor de tela pode enfatizar). b = negrito puramente visual (palavras-chave, nome de produto)
<em> × <i>
em = ênfase semântica (muda o sentido da frase). i = itálico visual (termo estrangeiro, nome científico, pensamento)
Na prática: use strong e em; deixe b e i para casos específicos. Uma regra simples: se você leria a palavra em voz alta com outro tom, é em; se a informação é importante mesmo lida em tom neutro, é strong.
📌 Vale gravar
A diferença entre <strong>/<b> e <em>/<i> é pergunta clássica. Resposta curta: os dois pares têm a mesma aparência padrão, mas só strong e em carregam significado — e significado é o que leitores de tela e buscadores usam.
<blockquotecite="https://www.w3.org/"><p>A força da Web está em sua universalidade.</p><footer>— <cite>Tim Berners-Lee</cite></footer></blockquote><p>Endereço:<br>Av. dos Ingás, 3001<br>Sinop — MT</p><hr><pre>
Texto pré-formatado:
preserva espaços
e quebras de linha.
</pre>
<blockquote> é uma citação em bloco; o atributo cite guarda a URL da fonte (não aparece na tela). <cite> marca o título da obra ou autor citado.
<q> é a versão inline: <p>Ele disse <q>volto já</q> e sumiu.</p> — o navegador coloca as aspas.
<br> quebra a linha dentro de um bloco; <hr> é uma quebra temática (mudança de assunto), não uma "linha decorativa".
<pre> preserva espaços e quebras exatamente como digitados — útil para código e diagramas.
⚠️ Atenção<br> serve para quebras dentro de um bloco de conteúdo (endereços, poemas, letras de música). Usar <br><br> para separar parágrafos é erro — use <p>. O espaçamento entre parágrafos é assunto do CSS.
<!-- Lista não ordenada: a ordem dos itens não importa --><ul><li>HTML</li><li>CSS</li><li>JavaScript</li></ul><!-- Lista ordenada: a ordem importa (passos, ranking) --><ol><li>Analisar a URL</li><li>Resolver o DNS</li><li>Abrir conexão TCP</li></ol><!-- Com atributos: numeração romana, começando em 3, decrescente --><oltype="I"start="3"reversed><li>Terceiro item</li><li>Segundo item</li><li>Primeiro item</li></ol><!-- Lista de definições: pares termo → descrição --><dl><dt>HTML</dt><dd>Linguagem de marcação que estrutura o conteúdo.</dd><dt>CSS</dt><dd>Linguagem de estilo que define a apresentação.</dd></dl><!-- Listas aninhadas: o <ul> filho vai DENTRO do <li> pai --><ul><li>Front-end
<ul><li>HTML</li><li>CSS</li></ul></li><li>Back-end</li></ul>
A pergunta para escolher entre <ul> e <ol> é: se eu embaralhar os itens, a informação muda? Passos de uma receita, sim → <ol>. Ingredientes, não → <ul>. A lista de definições (<dl>) serve para glossários, metadados ("Autor: Fulano", "Duração: 2 h") e perguntas frequentes.
Um <ul> ou <ol> só pode ter <li> como filhos diretos. Qualquer outra coisa — um <h3>, um <p>, outra lista — vai dentro de um <li>. O validador do W3C avisa quando você erra isso.
O link é a invenção que dá o "hipertexto" ao HTML. Todos os tipos usam o mesmo elemento, <a>, e mudam só o href:
HTML
<ahref="https://www.wikipedia.org">Link externo</a><ahref="contato.html">Link interno relativo</a><ahref="/sobre.html">Link a partir da raiz</a><ahref="#secao3">Âncora interna da própria página</a><ahref="programacao.html#dia-2">Âncora em outra página</a><ahref="documentos/edital.pdf"download>Baixar edital</a><ahref="mailto:contato@exemplo.com.br">Enviar e-mail</a><ahref="tel:+556635111000">Ligar</a><ahref="https://exemplo.com"target="_blank"rel="noopener noreferrer">
Abrir em nova aba
</a>
Os caminhos relativos seguem exatamente a tabela da §5 da Aula 01: contato.html é "na mesma pasta", ../index.html é "uma pasta acima", img/logo.png é "na subpasta img".
⚠️ Atenção
Segurança: ao usar target="_blank", sempre inclua rel="noopener noreferrer". Sem isso, a página aberta ganha acesso parcial à sua janela original (window.opener) e pode redirecioná-la — um vetor de phishing conhecido como tabnabbing. Navegadores modernos já aplicam noopener por padrão, mas o atributo explícito garante o comportamento em todos.
Para a âncora funcionar, o destino precisa de um id — e id deve ser único na página:
HTML
<ahref="#metodologia">Ir para Metodologia</a><h2id="metodologia">Metodologia</h2><p>O evento combina palestras e oficinas práticas.</p>
Ao clicar, o navegador rola até o elemento e acrescenta #metodologia à URL — lembre-se da Aula 01: o fragmento nunca é enviado ao servidor. Um link href="#a02-topo" para um id="a02-topo" no cabeçalho dá o clássico "voltar ao topo".
Textos de link ruins: "clique aqui", "saiba mais", "link". Um usuário de leitor de tela pode navegar por uma lista só de links — "clique aqui" ali não significa nada. Buscadores também usam o texto do link para entender o destino. Use texto descritivo: "Baixe o edital em PDF", "Veja a programação completa".
Tabelas são para dados tabulares — informação que faz sentido em linhas e colunas, como notas, horários, preços. Jamais para layout: isso quebra em celular, confunde leitores de tela e é assunto do CSS (Aula 07).
HTML
<table><caption>Notas da turma</caption><thead><tr><thscope="col">Aluno</th><thscope="col">A1</th><thscope="col">A2</th><thscope="col">A3</th><thscope="col">Média</th></tr></thead><tbody><tr><thscope="row">Maria Silva</th><td>8,0</td><td>7,5</td><td>9,0</td><td>8,2</td></tr><tr><thscope="row">João Souza</th><td>6,0</td><td>5,5</td><td>7,0</td><td>6,2</td></tr></tbody><tfoot><tr><thscope="row">Média da turma</th><td>7,0</td><td>6,5</td><td>8,0</td><td>7,2</td></tr></tfoot></table>
Elemento
Função
<caption>
Título/legenda da tabela — o leitor de tela anuncia antes de entrar nela
<thead>, <tbody>, <tfoot>
Agrupamento semântico de linhas: cabeçalho, corpo, rodapé (totais)
<tr>
Linha (table row)
<th>
Célula de cabeçalho
<td>
Célula de dado
scope="col" / scope="row"
Informa a leitores de tela a qual eixo o cabeçalho pertence
colspan estende a célula para a direita; rowspan, para baixo. Cuidado com a contagem: se uma linha tem colspan="3", as linhas seguintes precisam continuar somando o mesmo número de colunas, senão a tabela fica torta e o validador reclama.
Sem CSS, a tabela aparece sem bordas — parece "só texto alinhado". É normal. A Aula 06 coloca as bordas; hoje o que importa é a estrutura estar certa.
Antes do HTML5, tudo era <div>. O resultado era a chamada div soup: código ilegível, inacessível e ruim para buscadores. O HTML5 trouxe elementos que dizem o que a região é:
Só quando não existe elemento semântico adequado — agrupamento puramente visual
Repare que <header> e <footer> podem aparecer mais de uma vez: um para o site, outro dentro de cada <article>. Só <main> é único.
💡 Dica
Teste da semântica: se você trocar todo o seu CSS por um arquivo vazio, a página ainda faz sentido lida de cima a baixo? Se sim, sua semântica está boa. É exatamente o que um leitor de tela e um buscador "veem".
Para decidir entre <section> e <article>: pergunte se o trecho faria sentido publicado sozinho, em outro site. Um palestrante com nome, bio e foto, sim → <article>. A seção "Sobre o evento" da página inicial só faz sentido dentro dela → <section>.
🔬 Investigue
Crie exercicios/aula02/minimo.html contendo só a linha <p>Oi — sem doctype, sem <html>, sem <body>, sem fechar o <p>. Abra no Live Server e vá à aba Elements do DevTools. O navegador construiu <html>, <head>, <body> e fechou o <p> sozinho: você está vendo o DOM, não o arquivo. Agora acrescente uma <table> com um <tr> direto dentro (sem <tbody>) e olhe de novo: o <tbody> aparece no DOM sem você ter escrito. Quando o JavaScript da Unidade 3 percorrer a árvore, é essa árvore que ele vai encontrar.
O validador (https://validator.w3.org/) é o corretor ortográfico do HTML. Ele compara o seu código com a especificação e lista cada erro com a linha exata. Três formas de usar:
Validate by URI — para sites já publicados.
Validate by File Upload — envie o arquivo .html.
Validate by Direct Input — cole o código. É a forma mais rápida durante a aula.
A meta é sempre a mesma mensagem: "Document checking completed. No errors or warnings to show."
Mensagem do validador
O que significa
Stray end tag “p”.
Um </p> sem <p> correspondente — geralmente um fechamento duplicado ou cruzado
Unclosed element “ul”.
Faltou </ul>
End tag “strong” violates nesting rules.
Fechamento cruzado (§1)
Element “ul” not allowed as child of element “ol” in this context.
Lista aninhada fora do <li> (§4)
Element “h2” not allowed as child of element “ul”.
Só <li> pode ser filho direto de lista
Duplicate ID “topo”.
Dois elementos com o mesmo id — âncoras vão quebrar
Consider adding a “lang” attribute to the “html” start tag
Faltou lang="pt-BR" (aviso, mas corrija)
A table row was N columns wide and exceeded the column count
colspan/rowspan desbalanceado (§6)
Erros são listados na ordem em que ocorrem, e um erro costuma causar vários: uma tag não fechada no início gera dezenas de mensagens depois. Corrija o primeiro, revalide, repita.
💻 Mão na massa — Início, programação e palestrantes do site do evento¶
Hoje o index.html da Aula 01 vira uma página de verdade, e nascem programacao.html e palestrantes.html. As páginas inscricao.html e contato.html ganham só o esqueleto — elas são o assunto da Aula 03.
<!DOCTYPE html><htmllang="pt-BR"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metaname="description"content="Semana Acadêmica de Sistemas de Informação: três dias de palestras, minicursos e oficinas para estudantes e profissionais de tecnologia."><metaname="author"content="Semana Acadêmica de Sistemas de Informação"><title>Início — Semana Acadêmica de Sistemas de Informação</title></head>
O <title> segue o padrão "Página — Nome do site": é assim que ele aparece na aba e no histórico.
O cabeçalho é idêntico nas cinco páginas — é o que faz o site parecer um site. Escreva uma vez, com capricho, e copie nas outras.
site-evento/index.html (trecho: início do <body>)
HTML
<body><headerid="topo"><h1>Semana Acadêmica de Sistemas de Informação</h1><p>Três noites de outubro · Auditório Central</p><nav><ul><li><ahref="index.html">Início</a></li><li><ahref="programacao.html">Programação</a></li><li><ahref="inscricao.html">Inscrição</a></li><li><ahref="palestrantes.html">Palestrantes</a></li><li><ahref="contato.html">Contato</a></li></ul></nav></header>
Os cinco href são caminhos relativos: todos os arquivos estão na mesma pasta. O id="a02-topo" vai servir para os links "voltar ao topo".
<main><p>Nesta página:</p><ul><li><ahref="#sobre">Sobre o evento</a></li><li><ahref="#como-participar">Como participar</a></li><li><ahref="#glossario">Glossário</a></li></ul><sectionid="sobre"><h2>Sobre o evento</h2><p>
A Semana Acadêmica de Sistemas de Informação reúne estudantes, professores e
profissionais de tecnologia da região norte de Mato Grosso em três dias de
<strong>palestras, minicursos e oficinas práticas</strong>. O evento é
<em>gratuito</em> e aberto à comunidade.
</p><p>
Nesta edição, o tema central é <q>Desenvolvimento web para problemas reais</q>:
cada atividade parte de um caso concreto — do agronegócio à saúde pública —
e mostra como HTML, CSS e JavaScript resolvem parte dele.
</p><p>Emitimos certificado de <strong>20 horas</strong> para quem participar de pelo menos 75% das atividades.</p><p><ahref="#topo">Voltar ao topo</a></p></section><sectionid="como-participar"><h2>Como participar</h2><ol><li>Leia a <ahref="programacao.html">programação</a> e escolha as atividades.</li><li>Preencha o <ahref="inscricao.html">formulário de inscrição</a>.</li><li>Confira o e-mail de confirmação (verifique a caixa de spam).</li><li>No primeiro dia, faça o credenciamento no saguão do auditório a partir das 18h30.</li><li>Participe, assine a lista de presença e receba o certificado por e-mail.</li></ol><p><ahref="#topo">Voltar ao topo</a></p></section><sectionid="glossario"><h2>Glossário</h2><dl><dt>Palestra</dt><dd>Apresentação expositiva de 50 minutos, com perguntas ao final.</dd><dt>Minicurso</dt><dd>Atividade prática de 3 horas, com computador, para até 30 participantes.</dd><dt>Oficina</dt><dd>Atividade curta (90 minutos) em que os participantes constroem algo juntos.</dd><dt>Mesa-redonda</dt><dd>Conversa entre três ou quatro convidados mediada por um professor do curso.</dd><dt>Credenciamento</dt><dd>Confirmação de presença e retirada do crachá no primeiro dia.</dd></dl><p><ahref="#topo">Voltar ao topo</a></p></section><blockquotecite="https://www.w3.org/"><p>A força da Web está em sua universalidade. O acesso por todos, independentemente de deficiência, é um aspecto essencial.</p><footer>— <cite>Tim Berners-Lee</cite>, criador da Web</footer></blockquote></main>
Cada <section> tem um <h2> — regra de ouro: seção sem título provavelmente deveria ser um <div>. Os id dos <section> alimentam a lista "Nesta página" e as âncoras funcionam sem uma linha de CSS ou JavaScript.
Um <tbody>por dia, cada um com id. Isso permite links diretos como programacao.html#dia-2 a partir de outras páginas.
A linha de título de cada dia usa <th colspan="4">: uma célula ocupa as quatro colunas. Note que a linha da confraternização tem <th> + <td colspan="3"> — somando 4 de novo.
O <tfoot> carrega a observação final. Sem CSS ele aparece no fim, onde deve.
Cada palestrante é um <article>: faria sentido sozinho em outro site. Os títulos são <h3> porque estão abaixo do <h2> "Palestrantes e ministrantes" — hierarquia respeitada. Os links para programacao.html#dia-2 combinam caminho relativo com fragmento.
Abra https://validator.w3.org/#validate_by_input, cole o código de cada página e corrija até ver "No errors or warnings to show". Faça isso nas cinco. É a última etapa de toda aula da Unidade 1.
As cinco páginas abrem no Live Server e o menu leva de uma para outra sem 404.
Em index.html, os links "Nesta página" rolam até a seção e a URL ganha #sobre, #como-participar, #glossario.
Em palestrantes.html, clicar em "Minicurso: primeiros passos com redes neurais" abre programacao.html já posicionada no Dia 2.
Na aba Elements, a tabela mostra três <tbody> e um <tfoot>; cada <tr> soma 4 colunas.
O validador não acusa nenhum erro em nenhuma das cinco páginas.
Resultado esperado: um site de cinco páginas, sem estilo, navegável, com hierarquia de títulos correta, tabela de programação estruturada e zero erros no W3C. Ele parece "feio" — e vai continuar assim até a Aula 05. O que importa agora é a estrutura.
A8. Qual a diferença semântica entre <strong> e <b>? E entre <em> e <i>?
A9. Escreva o HTML de uma lista não ordenada com 3 itens, sendo que o segundo contém uma sublista ordenada com 2 itens.
A10. Escreva um link que abra https://www.wikipedia.org em nova aba, com os atributos de segurança corretos.
A11. O que faz o atributo scope em um <th>?
A12. Escreva o HTML de uma célula que ocupe 3 colunas e outra que ocupe 2 linhas.
A13. Para cada situação, indique o elemento semântico mais adequado: (a) menu principal; (b) uma notícia completa; (c) barra lateral de links relacionados; (d) rodapé com copyright; (e) imagem com legenda; (f) conteúdo principal da página.
A14. Por que <main> deve aparecer apenas uma vez por página?
B1. Crie exercicios/aula02/curriculo.html — um currículo pessoal completo usando apenas HTML semântico, sem nenhum CSS. Deve conter: dados pessoais no <header>; seções de formação, experiência, habilidades e projetos; listas apropriadas em cada uma (ordenada onde a cronologia importa); uma tabela de idiomas com nível; links para e-mail, telefone, LinkedIn e GitHub; <footer>.
Resultado esperado: validador do W3C com zero erros; a hierarquia h1 → h2 sem pulos; os links mailto: e tel: funcionando.
Dica
Use a página de palestrantes como modelo: <header> com seu nome no <h1>, <main> com uma <section> por bloco do currículo, <dl> para pares como "Período / Instituição". Para a tabela de idiomas, <th scope="row"> no nome do idioma.
B2. Escreva o HTML (sem CSS) de uma página de notícia com: cabeçalho do site, navegação, artigo com título, subtítulo, data em <time>, autor, cinco parágrafos, uma citação em destaque, uma seção de "leia também" em <aside> e rodapé do artigo com as tags do assunto.
Resultado esperado: um <article> com <header> e <footer> próprios, além do <header> e <footer> do site; o <aside> fora do <article>; zero erros no validador.
Dica
O exemplo da §7 é quase esse esqueleto. O subtítulo da notícia é um <p> dentro do <header> do artigo, não um <h2> — subtítulo jornalístico é descrição, não seção. As tags do assunto cabem em uma <ul> dentro do <footer> do artigo.
B3. Reproduza em HTML a tabela com a sequência das 15 aulas desta trilha (está na Aula 01), usando <caption>, <thead>, <tbody> e <th scope> corretamente. Mescle células nas linhas que fecham unidade (06, 10, 15) para indicar "Marco do projeto".
Resultado esperado: uma tabela de 15 linhas de dados, três <th scope="row"> de marco mesclados com colspan, e cada linha somando o mesmo número de colunas.
Dica
Comece pela tabela de programação do Mão na massa: um <tbody> por unidade deixa a estrutura mais clara. Nas linhas de marco, se a tabela tem 3 colunas e você quer "Aula" + "Marco do projeto", use <th scope="row"> + <td colspan="2">.
B4. Pegue o trecho abaixo (div soup) e reescreva-o inteiramente com HTML5 semântico:
HTML
<divid="topo"><divclass="titulo">Blog do Curso</div><divclass="menu"><div><ahref="#">Home</a></div><div><ahref="#">Posts</a></div></div></div><divid="conteudo"><divclass="post"><divclass="titulo-post">Como estudar programação</div><divclass="data">14 de outubro</div><divclass="texto">Estudar programação exige prática diária e projetos pequenos que cresçam aos poucos.</div></div></div><divid="rodape">Copyright Semana Acadêmica de Sistemas de Informação</div>
Cada class do original está gritando o nome do elemento semântico: titulo → <h1>, menu → <nav>, post → <article>, data → <time>. Só o # dos links deve virar um caminho real.
B5. Construa um índice navegável: uma página longa com 6 seções, cada uma com id, e um menu no topo com links de âncora para cada seção. Ao final de cada seção, inclua um link "voltar ao topo".
Resultado esperado: clicar em cada item do menu rola até a seção certa e a URL muda para #id-da-secao; "voltar ao topo" funciona em todas.
Dica
Cada seção precisa de conteúdo suficiente para a página rolar — copie três parágrafos da §1 desta aula em cada uma. Os id não podem ter espaços nem acentos e não podem se repetir.
B6. Compare arquiteturas: escolha três sistemas que você usa (por exemplo SIGAA, Instagram e um blog pessoal) e produza uma tabela HTML classificando cada um quanto a: estático/dinâmico, MPA/SPA e quantas camadas você supõe existirem. Justifique cada classificação com uma evidência observável (comportamento ao navegar, aba Network).
Resultado esperado: uma <table> com <caption>, <thead> e três linhas de dados, seguida de um parágrafo de justificativa por sistema.
Dica
Na aba Network, filtre por Doc: uma MPA carrega um novo documento HTML a cada clique; uma SPA carrega um documento no início e depois só Fetch/XHR com JSON.
B7. Use o validador do W3C em três sites reais e registre quantos erros e avisos cada um apresenta. Escolha um erro de cada site, explique o que significa e como corrigir.
Resultado esperado: uma lista com site, número de erros, número de avisos, o texto literal de um erro e a correção proposta.
Dica
Use Validate by URI. Sites grandes costumam ter dezenas de erros — isso não os impede de funcionar (lembre-se do "Você sabia?" da §1), mas mostra por que o validador é um padrão de qualidade e não uma obrigação técnica.
C1. Site institucional de 4 páginas. Construa, em exercicios/aula02/curso/, um site sobre um curso técnico ou de graduação — o seu, ou o curso de Sistemas de Informação de uma universidade pública à sua escolha (a grade curricular costuma estar disponível no site da instituição) — com quatro páginas interligadas: index.html (apresentação), grade.html (tabela com a grade curricular completa), corpo-docente.html (lista de professores com formação) e contato.html (endereço, telefone, e-mail e mapa em link). Requisitos: navegação idêntica em todas as páginas; caminhos relativos corretos; HTML5 semântico; zero erros no validador; nomes de arquivos em minúsculas sem acentos. Sem CSS ainda — este site será estilizado nas Aulas 05 e 06.
Dica
Faça primeiro o <header> com o <nav> de 4 links e só depois as páginas — copie o cabeçalho pronto em cada uma. Para a grade curricular, um <tbody> por fase, como na programação do evento. O "mapa em link" é um <a> para o Google Maps com target="_blank" e rel.
O trecho abaixo "funciona" — abre no navegador e mostra tudo. Mesmo assim, ele tem pelo menos oito problemas: alguns o validador do W3C encontra, outros só um olho humano treinado percebe. Encontre todos, classifique-os e corrija.
HTML
<html><head><title>Semana de SI</title><metacharset="UTF-8"></head><body><h1>Semana Acadêmica</h1><h1>Programação</h1><h4>Dia 1</h4><p>Palestra de abertura com <strong>convidada <em>especial</strong></em>.</p><p>Para se inscrever, <ahref="inscricao.html"target="_blank">clique aqui</a>.<br><br>
As vagas são limitadas.</p><ul><li>Minicursos</li><ul><li>HTML e CSS</li></ul><li>Oficinas</li></ul><table><tr><td>Horário</td><td>Atividade</td></tr><tr><td>19h</td><tdcolspan="3">Abertura</td></tr></table></body></html>
Critérios de pronto
Uma lista numerada com cada problema, a linha em que está e uma classificação: "o validador encontra" ou "só um humano encontra".
Pelo menos oito problemas listados (há mais — quem achar dez merece destaque especial).
A versão corrigida, validada com zero erros e zero avisos.
Uma frase explicando por que os problemas que o validador não encontra são, ainda assim, problemas.
Pistas
Cole o código como está no validador e leia as mensagens — elas resolvem a primeira metade.
Releia a tabela de "Erros comuns" desta aula: cada linha dela é um candidato.
Pense em: quantos <h1>? Que nível vem depois de <h1>? O <ul> filho está dentro de um <li>? O target="_blank" está acompanhado? O texto do link diz para onde vai? A tabela tem cabeçalhos de verdade? E o que falta na primeira linha do arquivo?
Boa parte das pessoas que usam a Web não usa mouse: quem navega por teclado, quem usa leitor de tela, quem tem lesão por esforço repetitivo. E buscadores não "veem" CSS nenhum. Hoje você vai usar o site do evento (e um site real) do jeito que eles usam — e descobrir se a estrutura que você escreveu se sustenta sozinha.
Critérios de pronto
No index.html do site do evento, navegue só com o teclado (Tab, Shift+Tab, Enter): registre a ordem em que os links recebem foco e se algum ficou inalcançável.
No Firefox, abra um site real (o da sua universidade ou escola, ou um jornal), desligue os estilos (Exibir → Estilo da página → Sem estilo) e responda: a página ainda faz sentido lida de cima a baixo? Onde a ordem do conteúdo surpreendeu você?
Um parágrafo comparando as duas experiências e apontando uma melhoria concreta que você faria no site real, com o elemento HTML que usaria.
No site do evento, uma melhoria implementada a partir do que você observou. Sugestão: acrescente id="a02-conteudo" ao <main> das cinco páginas (ele ainda não tem) e, como primeiro elemento do <body>, um link <a href="#a02-conteudo">Pular para o conteúdo</a> — sem CSS ele fica visível o tempo todo, e está tudo bem: escondê-lo até receber foco é assunto da Aula 07.
Pistas
A ordem do foco segue a ordem do HTML — não há como mudá-la sem reordenar o código (e é assim que deve ser).
No Chrome não há menu "sem estilo", mas a extensão Web Developer (Chris Pederick) tem CSS → Disable All Styles.
Leitores de tela oferecem uma lista só de títulos e outra só de links. Imagine essa lista para a sua página: os textos de link fazem sentido fora de contexto?
O link "pular para o conteúdo" é o primeiro elemento do <body> em quase todo site bem feito — procure-o com Tab em .
A tabela de status HTTP da Aula 01 tem cinco linhas. Um desenvolvedor consulta esses códigos toda semana — e a maioria dos sites de referência é em inglês. Produza http-referencia.html: uma página que documente os códigos de status HTTP com uma tabela por faixa (1xx a 5xx), com pelo menos 4 códigos por faixa, contendo código, nome em inglês, descrição em português e um exemplo de quando ocorre. Inclua um índice com âncoras internas para cada faixa.
Critérios de pronto
Cinco <section>, uma por faixa, cada uma com <h2> e id, e um índice no topo com âncoras para as cinco.
Cinco tabelas com <caption>, <thead>, <th scope="col"> e pelo menos 4 linhas de dados cada (20 códigos no total).
Pelo menos um código por faixa que não aparece na Aula 01 (por exemplo 418, 429, 451, 307, 206).
Você aprendeu camadas, MPA/SPA e APIs em teoria. Agora aplique em um sistema que você usa todo dia: escolha um (SIGAA, um e-commerce, uma rede social, um app de banco no navegador) e produza um relatório de 2 páginas com: diagrama da arquitetura provável (camadas e componentes), evidências obtidas na aba Network (formatos de resposta, chamadas de API visíveis, uso de CDN identificável pelos domínios), classificação MPA/SPA justificada, e uma proposta fundamentada de como você reorganizaria a arquitetura se precisasse suportar 10× mais usuários.
Critérios de pronto
Um diagrama (papel, Draw.io ou Excalidraw) com pelo menos três camadas e os componentes que você identificou (CDN, API, servidor de aplicação, banco).
Pelo menos cinco evidências da aba Network, cada uma com o que foi observado (URL, Content-Type, domínio) e o que ela indica.
A classificação MPA ou SPA com a evidência decisiva (documento novo a cada clique, ou JSON via Fetch/XHR).
A proposta para 10× mais usuários citando pelo menos duas técnicas da §8 e da §12 da Aula 01 (CDN, cache, separação de camadas, balanceamento).
Pistas
Na aba Network, o filtro Fetch/XHR mostra as chamadas de API; o filtro Doc mostra os documentos HTML. A proporção entre eles já classifica MPA/SPA.
Clique com o botão direito no cabeçalho das colunas e ative Domain: domínios como cdn., static., cloudfront.net, akamaihd.net denunciam CDN.
Respostas com Content-Type: application/json são a camada de aplicação falando; text/html é a camada de apresentação sendo entregue.
Para "10× mais usuários", pense no que é caro (gerar HTML dinâmico, consultar banco) e no que é barato (servir arquivo estático do cache).
A Wikipédia é um dos sites mais bem estruturados da Web: sumário com âncoras, infobox em tabela, seções hierárquicas, notas de rodapé com links de ida e volta. Escolha um artigo em português (sugestão: "HTML" ou "Tim Berners-Lee") e reproduza a sua estrutura integralmente em HTML semântico, sem uma linha de CSS. Ao final, compare a sua versão com o artigo real de estilos desligados: se ficaram parecidos, você entendeu HTML.
Critérios de pronto
Arquivo único exercicios/aula02/wikipedia.html com título, introdução, sumário (<nav> com <ol> de âncoras, incluindo subitens aninhados), pelo menos 6 seções (<h2>) e 4 subseções (<h3>).
A infobox reproduzida como <table> com <caption> e <th scope="row"> em cada linha.
Pelo menos 5 referências: no texto, um link <a href="#a02-ref-1"><sup>[1]</sup></a>; na seção "Referências", uma <ol> com id="a02-ref-1" em cada item e um link de volta ao ponto de citação.
Um <blockquote> com cite e pelo menos uma <figure> com <figcaption> (pode usar uma imagem de https://commons.wikimedia.org/ com alt — a Aula 04 aprofunda).
Zero erros no validador; navegação completa por teclado; o sumário funcionando.
Uma comparação escrita (10 linhas) entre a sua página e o artigo real com estilos desligados no Firefox.
Pistas
Abra o artigo real, pressione Ctrl+U e estude o código-fonte — ele é enorme, mas procure <h2>, <table class="infobox" e <ol class="references".
Para o link de volta na referência, o ponto de citação precisa de um id também: <sup id="a02-cite-1"> e, na referência, <a href="#a02-cite-1">↑</a>.
Sumário aninhado: o <ol> das subseções vai dentro do <li> da seção — o mesmo padrão da §4.
Faça em etapas: estrutura de títulos → sumário → tabela → referências. Valide a cada etapa; um erro de aninhamento no início gera dezenas de mensagens depois.
Parte 1 — Leitura (20 min). SILVA, M. S. Criando sites com HTML, capítulos sobre estrutura do documento e elementos de texto. TERUEL, E. C. HTML5 — Guia Prático, capítulo introdutório. Anote um elemento HTML que apareceu na leitura e não nesta aula, e pesquise o que ele faz na MDN.
Parte 2 — Produção (30 min). Produza o exercício B1 (currículo em HTML semântico), em arquivo .html, acompanhado de captura de tela do validador W3C mostrando "Document checking completed. No errors or warnings to show."
No projeto autoral: replique no seu meu-projeto/ o que o site do evento ganhou hoje — index.html completo (cabeçalho, navegação com as cinco páginas, três seções com <h2>, uma lista ordenada, uma lista de definições, um <blockquote>, rodapé), mais uma página com uma tabela de dados do seu domínio e uma página com <article> por item (o equivalente aos palestrantes).
Parte 3 — Registro (10 min). Escreva, no seu repositório ou no fórum da turma (se houver), uma nota "Semântica importa": encontre um site real com problemas de semântica (uso excessivo de div, títulos fora de ordem, links "clique aqui") e descreva três problemas encontrados com a correção proposta para cada. Se puder, compare com o que outra pessoa encontrou.
Critério de pronto: o currículo e as três páginas do projeto autoral passam no validador com zero erros; a navegação do projeto autoral não leva a nenhum 404.
Guarde:curriculo.html, a captura do validador e a pasta meu-projeto/ — a partir da Aula 15 tudo isso vai para um repositório Git.
web.dev — Learn HTML (em inglês): https://web.dev/learn/html — os capítulos "Document structure", "Semantic HTML", "Headings and sections", "Links" e "Tables".
SILVA, Maurício Samy. Criando sites com HTML: sites de alta qualidade com HTML e CSS. Novatec, 2008 — capítulos 2 a 5.
TERUEL, Evandro C. HTML 5 — Guia Prático. Saraiva, 2014 — capítulos sobre estrutura e texto (Minha Biblioteca).
MILETTO, Evandro M.; BERTAGNOLLI, Silvia C. Desenvolvimento de software II. Bookman, 2014 — capítulo sobre arquitetura de aplicações web (para os itens A1–A6).
W3C — HTML 5.2 Recommendation: https://www.w3.org/TR/html52/ — seções de elementos de seccionamento e conteúdo de texto.
Na próxima aula, a página de inscrição deixa de ser um esqueleto: você vai aprender o elemento <form>, os tipos de <input>, rótulos, agrupamento, validação nativa e a diferença entre GET e POST — e vai ver, na aba Network, os dados do formulário viajando até o servidor.
[ ] Pasta introducao-web/site-evento/ com as cinco páginas da Aula 02 (index.html, programacao.html, inscricao.html, palestrantes.html, contato.html), todas validadas no W3C.
[ ] VS Code com Live Server e Prettier, e o navegador com DevTools (F12). Hoje você vai passar boa parte da aula na aba Network.
[ ] Revisar da Aula 02: caminhos relativos, hierarquia de títulos, <nav> como lista e o hábito de validar cada página no W3C.
[ ] Pasta meu-projeto/ com o tema autoral definido e as páginas equivalentes já criadas.
Na aula passada você aprendeu a marcar conteúdo: textos, listas, links e tabelas, e construiu três das cinco páginas do site do evento com HTML semântico e validado. As páginas inscricao.html e contato.html ficaram só com o esqueleto, esperando esta aula. Hoje elas ganham o elemento que transforma um site em sistema: o formulário — a porta por onde os dados das pessoas entram no seu programa.
Pense em qualquer sistema web que você usa: SIGAA, internet banking, loja on-line, rede social, portal da prefeitura. Todos têm uma coisa em comum — em algum momento alguém digitou alguma coisa. Login, matrícula, transferência, pedido, comentário, denúncia: tudo entra por um formulário.
Isso faz do formulário o elemento com maior consequência de todo o HTML. Uma tabela mal marcada deixa o site feio para um leitor de tela. Um formulário mal construído:
Gera dado sujo. Um campo de telefone sem restrição recebe (66) 9 9999-9999, 66999999999, 9999-9999 e me liga no zap. Quem sofre é o banco de dados, o relatório e a pessoa que vai tentar ligar.
Afasta usuários. Cada campo desnecessário, cada teclado errado no celular, cada erro sem explicação é um motivo para desistir. Formulário longo demais é a principal causa de carrinho abandonado no comércio eletrônico.
Exclui pessoas. Um campo sem <label> é, para quem usa leitor de tela, apenas "campo de edição, em branco". Sem rótulo, não há como saber o que digitar.
Cria retrabalho. Todo dado que não foi validado na entrada vira, depois, uma planilha de correção manual.
Nesta aula você constrói a estrutura. Na Aula 04 fecha o ciclo com campos avançados e upload de arquivos. Na Aula 06 estiliza o formulário com CSS. Na Aula 14 acrescenta validação em JavaScript, com expressões regulares e mensagens acessíveis. O formulário de inscrição do evento vai atravessar toda a trilha.
Você já viu na Aula 01 o caminho de uma requisição HTTP. O formulário usa exatamente esse caminho, com um detalhe novo: ele monta a requisição para você.
Texto
1. Você preenche os campos e clica no botão de envio.
2. O navegador percorre o formulário e monta uma lista de pares nome=valor,
usando o atributo name de cada campo preenchido.
3. Se method="get": a lista vira a query string da URL (depois do ?).
Se method="post": a lista vira o corpo (body) da requisição.
4. O navegador envia a requisição para o endereço do atributo action.
5. O servidor lê os pares, faz o que tem de fazer e devolve uma resposta.
6. O navegador exibe a resposta — normalmente uma página de confirmação.
Repare no passo 2: é o name que vira a chave. Não é o id, não é o texto do <label>, não é o placeholder. Um campo sem name simplesmente não existe do ponto de vista do servidor. Guarde isso; é o erro número um da lista de Erros comuns desta aula.
🧠 Você sabia?
Formulários existem na Web desde 1993 — são mais antigos que o CSS, que o JavaScript e que a maioria das pessoas que os usam. Eles apareceram no HTML 2.0 e praticamente não mudaram de sintaxe desde então: um <form> escrito em 1995 ainda funciona hoje, em qualquer navegador. Essa estabilidade tem um nome na Web: compatibilidade retroativa. É por isso que uma página antiga não "para de funcionar" — a plataforma quase nunca remove nada, ela só acrescenta. O preço dessa escolha é conviver com atributos estranhos como enctype, cujo nome só faz sentido quando você conhece a história. O benefício é que o que você aprende hoje continua valendo daqui a dez anos.
Todo formulário começa e termina no elemento <form>. Ele não desenha nada na tela: é o envelope que agrupa os campos, define para onde os dados vão e como viajam.
URL que vai receber os dados. Ausente ou vazio: a própria página
method
get (padrão) ou post
enctype
Como os dados são codificados; use multipart/form-data para upload (Aula 04)
novalidate
Desliga a validação nativa do navegador (útil ao testar validação em JS)
autocomplete
on (padrão) ou off — permite ou bloqueia o preenchimento automático
target
Onde exibir a resposta: _self (padrão) ou _blank (nova aba)
name
Nome do formulário; usado por JavaScript e por scripts antigos
Nesta trilha você ainda não tem um servidor para receber os dados — isso é assunto do Nível 2. Por enquanto, o action aponta para um endereço fictício (/inscrever), e o que interessa é observar a requisição sendo montada no DevTools. O servidor do Live Server vai responder 404 Not Found, e está tudo bem: os dados foram enviados, e é isso que vamos inspecionar.
Essa é a decisão mais importante do <form> — e a que mais confunde quem está começando.
Aspecto
GET
POST
Onde vão os dados
Na URL, como query string
No corpo da requisição
Visível na barra de endereço
Sim
Não
Limite prático de tamanho
Sim (cerca de 2.000 caracteres)
Praticamente ilimitado
Fica no histórico e nos favoritos
Sim
Não
Pode ser recarregado sem efeito colateral
Sim
Não (o navegador pergunta antes)
Uso típico
Busca, filtros, paginação
Cadastro, login, envio de dados
A regra prática: GET para ler, POST para alterar.
Se a operação apenas consulta informação e poderia ser repetida cem vezes sem consequência, use GET. É por isso que a URL de uma busca é compartilhável: programacao.html?dia=2&trilha=web descreve exatamente o que você está vendo, e mandar esse link para outra pessoa mostra a ela a mesma tela. Essa propriedade — repetir sem mudar nada — chama-se idempotência.
Se a operação cria, altera ou apaga alguma coisa, use POST. Uma inscrição feita duas vezes por engano vira duas inscrições. Por isso o navegador avisa antes de reenviar um POST quando você aperta F5.
Como fica a URL de um GET. Um formulário com method="get", action="busca.html" e um campo name="q" preenchido com html leva o navegador para:
Texto
busca.html?q=html
Com mais campos, os pares são separados por &:
Texto
programacao.html?dia=2&trilha=web&ordem=horario
Caracteres especiais são codificados: um espaço vira + ou %20, o @ vira %40, o acento de programação vira programa%C3%A7%C3%A3o. Esse processo é a codificação de URL (percent-encoding), e o navegador faz tudo sozinho.
⚠️ AtençãoNunca envie senha por GET. Ela ficaria visível na barra de endereço, no histórico do navegador, no cache, nos registros (logs) do servidor e no cabeçalho Referer enviado a sites de terceiros. Já houve vazamento de milhões de senhas exatamente assim. Formulário de login é sempre method="post".
🔎 Por baixo do capôPOST não é "seguro" no sentido de escondido: os dados vão no corpo da requisição, em texto puro, e qualquer pessoa com acesso à rede pode lê-los se a conexão for http://. O que protege de verdade é o HTTPS, que cifra a requisição inteira — URL, cabeçalhos e corpo. POST protege dos olhos de quem está do seu lado vendo a tela e do histórico do navegador; o HTTPS protege do resto do mundo. Você vai ver os detalhes do certificado e do cadeado na trilha Deploy; por ora, guarde a distinção entre não aparecer na URL e estar cifrado.
Este é o experimento central da aula. Faça uma vez acompanhando o texto e depois refaça sozinho, sem consultar.
🔬 Investigue
Crie um arquivo exemplos/get-vs-post.html com dois formulários idênticos — um com method="get" e outro com method="post", ambos com action="recebe.html" e dois campos (name="usuario" e name="mensagem"). Abra com o Live Server, abra o DevTools na aba Network e marque "Preserve log". Preencha e envie o primeiro: na lista de requisições aparece recebe.html?usuario=maria&mensagem=oi — os dados estão na própria URL, e você os vê também na barra de endereço. Agora envie o segundo: a URL fica limpa; clique na requisição, vá em Payload (ou Requisição, no Firefox) e veja usuario: maria e mensagem: oi. Repita o POST e aperte F5: o navegador pergunta se você quer reenviar os dados. Agora apague o atributo name de um dos campos e envie de novo: o campo some da requisição, mesmo estando preenchido na tela. Esse é o bug mais silencioso dos formulários.
Enquanto não existe um back-end, três truques deixam o experimento útil:
action vazio (action=""): o formulário envia para a própria página. Com method="get", os dados aparecem na URL e a página recarrega — ótimo para ver a query string sem erro 404.
action="recebe.html": crie uma página simples de "recebido com sucesso". Com GET, ela abre com os dados na URL.
Aba Network: funciona sempre, mesmo com resposta 404. A requisição saiu; é ela que interessa.
O <input> é o campo mais versátil do HTML. Ele não tem tag de fechamento (é um elemento vazio) e muda completamente de comportamento conforme o atributo type.
Nenhum desses tipos exige uma linha de JavaScript. O navegador entrega calendário, seletor de cor, controle deslizante e validação de formato de graça.
type
Ganho prático imediato
email
Teclado com @ no celular e validação de formato
tel
Teclado numérico no celular (sem validar formato)
number
Setas de incremento e validação numérica com min, max e step
date
Seletor de calendário nativo, no idioma do sistema
url
Validação de endereço (exige o esquema, como https://)
search
Botão de limpar (×) e integração com o histórico de buscas
color
Seletor de cores do sistema operacional
range
Controle deslizante — bom para valores aproximados
file
Botão de escolher arquivo, filtrado por accept
hidden
Dado enviado sem aparecer na tela (origem, identificador, etapa)
💡 Dica
Escolher o type certo não é detalhe estético: no celular, é ele que decide qual teclado aparece. Um campo de telefone com type="text" obriga a pessoa a trocar de teclado antes de digitar — uma fricção pequena que, multiplicada por milhares de usuários, vira abandono. Teste o seu formulário no celular pelo IP do Live Server na mesma rede Wi-Fi: é o teste mais barato de usabilidade que existe.
⚠️ Atençãotype="number" serve para quantidades, não para "sequências de dígitos". CPF, CEP, telefone, matrícula e número de cartão não são números: eles têm zeros à esquerda que importam, podem ter pontuação e nunca são somados. Em type="number", o navegador remove zeros à esquerda, mostra setas de incremento sem sentido e permite notação como 1e5. Para esses campos, use type="text" com pattern — e, a partir da Aula 04, com inputmode="numeric" para o teclado certo.
<labelfor="nome">Nome completo (obrigatório)</label><inputtype="text"id="nome"name="nome"value="Maria"placeholder="Ex.: Maria da Silva"requiredminlength="3"maxlength="80"autocomplete="name"autofocus>
Atributo
O que faz
id
Liga o campo ao <label for="…">; precisa ser único na página
name
Chave enviada ao servidor; sem ele o campo não é enviado
value
Valor inicial, já preenchido e já enviável
placeholder
Dica dentro do campo; some ao digitar e não é enviada
required
Bloqueia o envio enquanto o campo estiver vazio
minlength / maxlength
Mínimo e máximo de caracteres
autocomplete
Diz ao navegador que dado é este, para preenchimento automático
autofocus
Coloca o foco neste campo ao abrir a página; use no máximo um por página
readonly
Não editável, mas é enviado
disabled
Não editável, não é enviado e sai da ordem do Tab
Quatro pares que costumam ser confundidos:
id × name. O id é para a página (ligar o <label>, servir de âncora, ser encontrado pelo JavaScript na Aula 13) e precisa ser único no documento. O name é para o servidor e pode se repetir — é assim que um grupo de caixas de seleção manda vários valores com a mesma chave.
placeholder × value. O placeholder é um texto cinza que some quando você digita; ele não é enviado. O value é conteúdo de verdade: aparece preenchido e é enviado se não for alterado.
readonly × disabled. Os dois impedem edição. readonlyenvia o valor; disablednão envia e também tira o campo da navegação por Tab. Se você precisa que um valor calculado chegue ao servidor, use readonly (ou um hidden), nunca disabled.
minlength × min.minlength conta caracteres (texto); min compara valores (números e datas). Trocar um pelo outro é um erro clássico: minlength="16" em um campo de idade exige um número de dezesseis dígitos.
🔎 Por baixo do capô
O atributo autocomplete não é um simples liga/desliga. Ele tem um vocabulário padronizado de mais de cinquenta valores — name, given-name, family-name, email, tel, street-address, postal-code, cc-number, one-time-code — e é isso que permite ao navegador (e ao gerenciador de senhas do celular) preencher um cadastro inteiro em um toque. Preencher autocomplete corretamente é acessibilidade: consta na WCAG 2.1 como critério 1.3.5 ("identificar a finalidade da entrada"), justamente porque reduz a carga de digitação para pessoas com deficiência motora ou cognitiva. Um formulário com autocomplete="off" em tudo, "por segurança", só torna a vida mais difícil — e leva as pessoas a escolherem senhas piores.
Todo campo precisa de um rótulo associado. Não é recomendação de estilo: é requisito de acessibilidade e de usabilidade — e o checklist de qualidade do Marco do projeto cobra isso.
<!-- Forma 1: atributo for apontando para o id do campo (preferida) --><labelfor="email">E-mail institucional</label><inputtype="email"id="email"name="email"><!-- Forma 2: o <label> envolve o campo --><label>
E-mail institucional
<inputtype="email"name="email"></label>
A forma 1 é a preferida porque separa rótulo e campo no HTML — o que dá liberdade total ao CSS para colocá-los lado a lado, um acima do outro ou em colunas, sem lutar contra a estrutura. Ela também é a única que funciona quando o rótulo está em outra célula de uma tabela ou em outro contêiner.
A forma 2 dispensa id e é útil em casos pequenos, especialmente caixas de seleção dentro de uma lista. Cuidado: se você usar as duas ao mesmo tempo (<label for="x"> envolvendo o campo de id="a03-x"), o for precisa apontar para esse mesmo campo, senão o validador acusa erro.
Acessibilidade. Ao focar um campo, o leitor de tela anuncia o rótulo: "E-mail institucional, campo de edição". Sem <label>, a pessoa ouve apenas "campo de edição" e não tem como saber o que digitar.
Usabilidade para todo mundo. Clicar no rótulo foca o campo. Em uma caixa de seleção de 16 × 16 pixels, o rótulo multiplica por dez a área clicável — a diferença entre acertar e errar o toque no celular.
Clareza permanente. O rótulo continua visível depois que a pessoa digitou. O placeholder, não.
⚠️ Atençãoplaceholder não é <label>. Ele desaparece assim que a pessoa começa a digitar (e quem se distraiu no meio do preenchimento não sabe mais o que aquele campo pedia), costuma ter contraste baixo demais para a WCAG, não é lido de forma confiável por todas as tecnologias assistivas e, em campos preenchidos automaticamente, some sem nunca ter sido lido. Use placeholder só para exemplo de formato ("Ex.: 78550-000") — e sempre além do rótulo, nunca no lugar dele.
Marcar obrigatoriedade só com um asterisco vermelho falha duas vezes: quem não enxerga a cor não percebe, e quem usa leitor de tela ouve "asterisco". Faça assim:
O texto "(obrigatório)" no rótulo é lido por todos. O atributo required faz o navegador barrar o envio e, de quebra, é anunciado pelos leitores de tela como "obrigatório". E, no rodapé do formulário, uma frase explicando a convenção resolve o resto: "Os campos marcados como obrigatórios precisam ser preenchidos".
<labelfor="mensagem">Mensagem</label><textareaid="mensagem"name="mensagem"rows="6"cols="40"maxlength="500"placeholder="Escreva sua mensagem"></textarea>
rows e cols definem o tamanho inicial em linhas e colunas de caractere; o CSS pode sobrescrever (Aula 06).
maxlength limita os caracteres, exatamente como no <input>.
⚠️ Atenção<textarea>não usa o atributo value: o conteúdo inicial vai entre as tags. E tudo que estiver ali dentro — inclusive espaços e quebras de linha de indentação — vira conteúdo do campo. Escreva sempre <textarea></textarea> colado quando o campo deve começar vazio. Um <textarea> "indentado bonitinho" nasce com espaços em branco dentro, e um required passa a aceitar um campo que parece vazio.
<labelfor="curso">Curso</label><selectid="curso"name="curso"required><optionvalue="">Selecione o seu curso</option><optgrouplabel="Computação"><optionvalue="si">Sistemas de Informação</option><optionvalue="ads">Análise e Desenvolvimento de Sistemas</option></optgroup><optgrouplabel="Engenharias"><optionvalue="civil"selected>Engenharia Civil</option><optionvalue="agro">Engenharia Agronômica</option></optgroup><optgrouplabel="Outros"><optionvalue="outro">Outro curso</option></optgroup></select>
Pontos que fazem diferença:
O value é o que vai para o servidor; o texto entre as tags é o que a pessoa lê. Quando o value é omitido, o próprio texto é enviado.
A primeira <option> com value="" é o truque que faz required funcionar em um <select>: enquanto ela estiver escolhida, o campo é considerado vazio. Sem ela, o primeiro item já conta como resposta válida — e você recebe "Sistemas de Informação" de quem nunca olhou a lista.
<optgroup label="…"> agrupa opções. O label é obrigatório e é anunciado pelo leitor de tela.
selected define a opção inicial. Use com parcimônia: uma resposta pré-marcada é uma resposta que muita gente vai deixar como está.
Seleção múltipla:
HTML
<labelfor="linguagens">Linguagens que você já usou</label><selectid="linguagens"name="linguagens"multiplesize="4"><optionvalue="html">HTML</option><optionvalue="css">CSS</option><optionvalue="js">JavaScript</option><optionvalue="python">Python</option><optionvalue="java">Java</option></select>
multiple permite escolher vários (com Ctrl ou Shift) e size define quantas opções ficam visíveis. Na prática, um <select multiple> é pouco descoberto pelos usuários: em telas de toque, quase ninguém adivinha como selecionar mais de um. Prefira caixas de seleção quando as opções forem poucas.
<fieldset><legend>Turno de preferência</legend><inputtype="radio"id="matutino"name="turno"value="matutino"checked><labelfor="matutino">Matutino</label><inputtype="radio"id="vespertino"name="turno"value="vespertino"><labelfor="vespertino">Vespertino</label><inputtype="radio"id="noturno"name="turno"value="noturno"><labelfor="noturno">Noturno</label></fieldset><fieldset><legend>Oficinas de interesse</legend><inputtype="checkbox"id="of-git"name="oficinas"value="git"><labelfor="of-git">Git e GitHub do zero</label><inputtype="checkbox"id="of-api"name="oficinas"value="api"><labelfor="of-api">Introdução a APIs REST</label><inputtype="checkbox"id="of-acess"name="oficinas"value="acessibilidade"><labelfor="of-acess">Acessibilidade na prática</label></fieldset>
A regra decisiva: botões de rádio do mesmo grupo compartilham o mesmo name e têm value diferentes. É o name igual que faz um desmarcar o outro. Dar um name diferente para cada rádio é o erro mais comum da aula — e o sintoma é fácil de reconhecer: todos ficam marcáveis ao mesmo tempo.
Nas caixas de seleção, o name repetido tem outro efeito: o servidor recebe uma lista de valores sob a mesma chave (oficinas=git&oficinas=api).
Aspecto
Radio
Checkbox
Escolha
Uma entre várias
Várias independentes, ou nenhuma
Atributo name
Igual em todo o grupo
Igual (lista) ou diferente (sim/não)
Dá para desmarcar clicando
Não
Sim
Valor enviado quando não marcado
Nada
Nada
💡 Dica
Uma caixa de seleção não marcada não é enviada — o servidor não recebe termos=false, ele simplesmente não recebe termos. Se você precisa distinguir "respondeu não" de "não respondeu", use dois botões de rádio ("Sim" e "Não") em vez de uma caixa de seleção.
<labelfor="instituicao">Instituição de ensino</label><inputtype="text"id="instituicao"name="instituicao"list="instituicoes"autocomplete="organization"><datalistid="instituicoes"><optionvalue="Universidade Estadual — Campus Sinop"></option><optionvalue="UFMT — Campus Sinop"></option><optionvalue="Instituto Federal — Campus Sorriso"></option><optionvalue="UNIC"></option><optionvalue="Outra instituição"></option></datalist>
O <input> aponta para o <datalist> pelo atributo list, que recebe o id da lista. À medida que a pessoa digita, o navegador filtra as sugestões — mas qualquer valor fora da lista continua sendo aceito. É a diferença essencial em relação ao <select>.
Situação
Use
O conjunto de respostas válidas é fechado e conhecido
<select>
A lista é longa e a digitação acelera a busca, mas há exceções
<input list> + <datalist>
Você precisa garantir que o valor esteja na lista
<select> (e revalide no servidor)
Poucas opções (até cinco) e todas devem ficar visíveis
<fieldset><legend>Dados pessoais</legend><!-- campos do grupo --></fieldset>
<fieldset> agrupa campos relacionados; <legend> (que precisa ser o primeiro filho) dá título ao grupo. Por padrão o navegador desenha uma borda em volta — na Aula 06 você troca isso por algo mais bonito.
O valor real está na acessibilidade: para um leitor de tela, a legenda é anunciada junto com cada campo do grupo. Sem <fieldset>, três botões de rádio rotulados "Matutino", "Vespertino" e "Noturno" são apenas três opções soltas; com ele, viram "Turno de preferência, Matutino, botão de opção, 1 de 3". É o que torna um grupo de rádios compreensível sem visão.
Use <fieldset> sempre em grupos de rádio e de caixas de seleção, e em blocos temáticos de formulários longos (dados pessoais, endereço, pagamento). Não use para envolver um campo só.
Valida e envia o formulário. É o padrão dentro de um <form>
reset
Devolve todos os campos aos valores iniciais
button
Não faz nada sozinho; existe para receber uma ação em JavaScript (Aula 13)
⚠️ Atenção
Dentro de um <form>, um <button>sem type age como submit. Isso causa envios acidentais em botões criados para outra finalidade ("mostrar mais", "adicionar linha", "calcular"). Declare sempre o type — é uma linha que evita um bug difícil de perceber, porque a página parece só "recarregar sozinha".
<button> ou <input type="submit">? Os dois enviam. Prefira <button>: ele aceita conteúdo HTML dentro (um ícone SVG ao lado do texto, por exemplo), enquanto o <input> só aceita o texto do atributo value. E nunca use uma imagem com um manipulador de clique no lugar de um botão: <img onclick="enviar()"> não é focável pelo teclado, não é anunciado como botão e não funciona com Enter.
Evite type="reset". Ele fica ao lado do botão de envio, tem o mesmo tamanho e apaga meia hora de preenchimento com um clique errado. Praticamente nenhum formulário profissional oferece "Limpar" hoje. Se você incluir um, coloque-o longe do botão principal e com aparência secundária.
O navegador valida os campos antes de enviar, sem uma linha de JavaScript. Você só precisa declarar as regras.
exemplos/validacao-nativa.html (trecho)
HTML
<formaction="/cadastrar"method="post"><labelfor="v-nome">Nome completo (obrigatório)</label><inputtype="text"id="v-nome"name="nome"requiredminlength="5"maxlength="100"autocomplete="name"><labelfor="v-email">E-mail (obrigatório)</label><inputtype="email"id="v-email"name="email"requiredautocomplete="email"><labelfor="v-idade">Idade</label><inputtype="number"id="v-idade"name="idade"min="16"max="120"step="1"><labelfor="v-cep">CEP</label><inputtype="text"id="v-cep"name="cep"pattern="[0-9]{5}-[0-9]{3}"placeholder="Ex.: 78550-000"title="Use o formato 00000-000, com o hífen"autocomplete="postal-code"><labelfor="v-senha">Senha (obrigatório)</label><inputtype="password"id="v-senha"name="senha"requiredminlength="8"autocomplete="new-password"><buttontype="submit">Cadastrar</button></form>
pattern recebe uma expressão regular — um assunto que a Aula 14 aprofunda. Por enquanto, três padrões resolvem quase tudo:
HTML
<!-- CEP: cinco dígitos, hífen, três dígitos --><inputtype="text"name="cep"pattern="[0-9]{5}-[0-9]{3}"title="Use o formato 00000-000, com o hífen"><!-- CPF: 000.000.000-00 --><inputtype="text"name="cpf"pattern="[0-9]{3}\.[0-9]{3}\.[0-9]{3}-[0-9]{2}"title="Use o formato 000.000.000-00, com pontos e hífen"><!-- Matrícula: exatamente oito dígitos --><inputtype="text"name="matricula"pattern="[0-9]{8}"title="A matrícula tem 8 dígitos, sem pontos ou traços">
Como ler [0-9]{5}-[0-9]{3}: "um dígito de 0 a 9, repetido cinco vezes; depois um hífen; depois um dígito repetido três vezes". O ponto precisa de barra invertida (\.) porque, sozinho, . significa "qualquer caractere".
Sem title, o navegador mostra uma mensagem inútil como "Corresponda ao formato solicitado." — a pessoa fica sabendo que errou, mas não o que fazer. Com title, a mensagem inclui o seu texto. pattern sem title é considerado formulário incompleto neste material.
Quando a validação falha, quem escreve a mensagem é o navegador, no idioma do sistema operacional:
Texto
Preencha este campo.
Inclua um "@" no endereço de e-mail.
O valor precisa ser maior ou igual a 16.
Aumente o texto para 5 caracteres ou mais.
Selecione um item na lista.
Você não controla esse texto no HTML puro (na Aula 14 vai controlar, com a Constraint Validation API do JavaScript). O que você controla é o title do pattern e, principalmente, o rótulo: um campo bem rotulado gera menos erro do que qualquer mensagem bem escrita.
📌 Vale gravar
Três perguntas voltam sempre: (1) o que acontece com um campo sem name ao enviar — ele não é enviado; (2) por que os rádios de um grupo precisam do mesmo name — é o que os torna mutuamente exclusivos; (3) a validação nativa garante que o dado chega correto ao servidor? Não. Ela é conveniência para o usuário, não segurança.
⚠️ Atenção
Toda a validação desta seção pode ser desligada em cinco segundos: basta abrir o DevTools e apagar o atributo required, ou acrescentar novalidate ao <form>, ou enviar a requisição por fora do navegador com uma ferramenta de linha de comando. Validação no cliente é usabilidade; validação no servidor é segurança. O servidor precisa revalidar absolutamente tudo — e isso vale para o resto da sua carreira, não só para esta trilha. Você vai implementar essa segunda camada no Nível 2, com Express.
O atributo novalidate no <form> desliga a validação nativa da página inteira. Ele parece um tiro no pé, mas tem uso legítimo: quando o formulário passa a ter validação em JavaScript, com mensagens próprias e acessíveis, você desliga a nativa para não ter duas mensagens competindo. É exatamente o que a Aula 14 faz.
Passe esta lista em todo formulário que você construir nesta trilha. Ela também é a base do desafio ⭐⭐ de hoje.
[ ] Todo campo tem <label> associado por for/id (ou envolvente). Nenhum rótulo é só placeholder.
[ ] Grupos de rádio e de caixas de seleção estão dentro de <fieldset> com <legend>.
[ ] A obrigatoriedade está no texto do rótulo, não apenas em uma cor ou em um asterisco.
[ ] A ordem de tabulação é a ordem visual: siga a ordem do HTML e não use tabindex positivo.
[ ] O foco é visível em todos os campos e botões (não remova o contorno sem colocar outro indicador).
[ ] autocomplete está preenchido com o valor semântico correto (name, email, tel, postal-code, organization, bday).
[ ] Cada <input type="file"> diz, no rótulo, os formatos e o tamanho aceitos.
[ ] O formulário inteiro é utilizável só com o teclado: Tab para avançar, Espaço para marcar, Enter para enviar.
[ ] Não há campo pedindo informação que você não vai usar. O campo mais acessível é o que não existe.
🔬 Investigue
Abra qualquer formulário que você já tenha escrito e guarde o mouse. Navegue só com Tab, Shift+Tab, setas (nos rádios) e Espaço. Anote: você sempre sabe onde está o foco? A ordem faz sentido? Consegue marcar todas as opções? Consegue enviar com Enter? Depois repita clicando no texto de cada rótulo: se o campo correspondente não recebe o foco, aquele <label> não está associado — e o problema é for sem id correspondente, ou id duplicado na página.
💻 Mão na massa — a página de inscrição da Semana Acadêmica¶
A inscricao.html que você criou na Aula 02 tem apenas a frase "O formulário de inscrição será construído na próxima aula". Chegou a hora. Ao final destes passos, a página terá um formulário de inscrição completo, acessível e validado, com quatro grupos de campos.
Trabalhe em introducao-web/site-evento/inscricao.html.
Passo 1 — Cabeçalho, <head> e o esqueleto do <main>¶
Abra inscricao.html e substitua o conteúdo inteiro por este esqueleto. O <head>, o <header> e o <footer> são os mesmos das outras páginas — só mudam <title> e description.
method="post": a inscrição cria um registro. Não é uma consulta, não deve ficar no histórico e não pode ser repetida por engano com F5.
action="/inscrever": o endereço que vai receber os dados. Ele ainda não existe — o Live Server vai responder 404, e nós vamos observar a requisição na aba Network mesmo assim.
<input type="hidden" name="origem">: um dado que a pessoa não digita, mas que o servidor precisa. Aqui, de onde veio a inscrição — útil quando o mesmo endereço também recebe inscrições feitas no balcão do credenciamento.
site-evento/inscricao.html (trecho: primeiro <fieldset>)
HTML
<fieldset><legend>Dados pessoais</legend><p><labelfor="nome">Nome completo (obrigatório)</label><inputtype="text"id="nome"name="nome"requiredminlength="5"maxlength="100"placeholder="Ex.: Maria Aparecida da Silva"autocomplete="name"></p><p><labelfor="cpf">CPF (obrigatório)</label><inputtype="text"id="cpf"name="cpf"requiredpattern="[0-9]{3}\.[0-9]{3}\.[0-9]{3}-[0-9]{2}"placeholder="Ex.: 000.000.000-00"title="Use o formato 000.000.000-00, com pontos e hífen"></p><p><labelfor="nascimento">Data de nascimento</label><inputtype="date"id="nascimento"name="nascimento"min="1930-01-01"autocomplete="bday"></p><p><labelfor="email">E-mail (obrigatório)</label><inputtype="email"id="email"name="email"requiredplaceholder="Ex.: maria@exemplo.br"autocomplete="email"></p><p><labelfor="telefone">Telefone com DDD</label><inputtype="tel"id="telefone"name="telefone"placeholder="Ex.: (66) 99999-0000"autocomplete="tel"></p></fieldset>
Cada par rótulo + campo está dentro de um <p>. Isso não é decoração: sem CSS, elementos de formulário são todos inline e ficariam todos na mesma linha, um grudado no outro. O <p> dá a quebra e o espaçamento mínimos até a Aula 06 assumir o controle.
Repare no type="date" com min="1930-01-01": o formato do atributo é sempre AAAA-MM-DD, independentemente de como o navegador mostra a data para o usuário (no Brasil, DD/MM/AAAA). Essa separação entre formato de máquina e formato humano é a mesma do atributo datetime que você usou nas tabelas da Aula 02.
site-evento/inscricao.html (trecho: segundo <fieldset>)
HTML
<fieldset><legend>Dados acadêmicos</legend><p><labelfor="instituicao">Instituição de ensino</label><inputtype="text"id="instituicao"name="instituicao"list="instituicoes"placeholder="Comece a digitar para ver sugestões"autocomplete="organization"><datalistid="instituicoes"><optionvalue="Universidade Estadual — Campus Sinop"></option><optionvalue="UFMT — Campus Sinop"></option><optionvalue="Instituto Federal — Campus Sorriso"></option><optionvalue="UNIC — Sinop"></option><optionvalue="Escola de ensino médio"></option></datalist></p><p><labelfor="curso">Curso (obrigatório)</label><selectid="curso"name="curso"required><optionvalue="">Selecione o seu curso</option><optgrouplabel="Computação"><optionvalue="si">Sistemas de Informação</option><optionvalue="ads">Análise e Desenvolvimento de Sistemas</option><optionvalue="cc">Ciência da Computação</option></optgroup><optgrouplabel="Engenharias"><optionvalue="civil">Engenharia Civil</option><optionvalue="agro">Engenharia Agronômica</option></optgroup><optgrouplabel="Outros"><optionvalue="outro">Outro curso</option><optionvalue="nenhum">Ainda não estou na graduação</option></optgroup></select></p><p><labelfor="fase">Fase ou semestre</label><inputtype="number"id="fase"name="fase"min="1"max="8"step="1"placeholder="Ex.: 3"></p><p><labelfor="matricula">Matrícula</label><inputtype="text"id="matricula"name="matricula"pattern="[0-9]{8}"placeholder="Ex.: 20240001"title="A matrícula tem 8 dígitos, sem pontos ou traços"></p></fieldset>
Compare de propósito os dois campos de escolha deste grupo:
Instituição é um <input list> com <datalist>: as cinco instituições da região cobrem quase todos os casos, mas alguém de fora precisa poder digitar a sua.
Curso é um <select> com <optgroup>: o conjunto é fechado, existe a opção "Outro curso" e a primeira <option> tem value="" para que o required funcione de verdade.
<fieldset><legend>Participação</legend><fieldset><legend>Turno de preferência (obrigatório)</legend><inputtype="radio"id="turno-matutino"name="turno"value="matutino"required><labelfor="turno-matutino">Matutino</label><inputtype="radio"id="turno-vespertino"name="turno"value="vespertino"><labelfor="turno-vespertino">Vespertino</label><inputtype="radio"id="turno-noturno"name="turno"value="noturno"><labelfor="turno-noturno">Noturno</label></fieldset><fieldset><legend>Oficinas de interesse</legend><inputtype="checkbox"id="of-git"name="oficinas"value="git"><labelfor="of-git">Git e GitHub do zero (dia 1, 40 vagas)</label><br><inputtype="checkbox"id="of-html"name="oficinas"value="html-css"><labelfor="of-html">Do zero ao primeiro site com HTML e CSS (dia 2, 30 vagas)</label><br><inputtype="checkbox"id="of-api"name="oficinas"value="api-rest"><labelfor="of-api">Introdução a APIs REST (dia 2, 30 vagas)</label><br><inputtype="checkbox"id="of-acessibilidade"name="oficinas"value="acessibilidade"><labelfor="of-acessibilidade">Acessibilidade na prática (dia 3, 25 vagas)</label></fieldset><p><labelfor="camiseta">Tamanho da camiseta</label><selectid="camiseta"name="camiseta"><optionvalue="">Não quero camiseta</option><optionvalue="p">P</option><optionvalue="m">M</option><optionvalue="g">G</option><optionvalue="gg">GG</option></select></p></fieldset>
Dois detalhes que valem nota:
required em um grupo de rádios só precisa aparecer uma vez, em qualquer um dos botões do grupo. O navegador entende que o grupo inteiro é obrigatório, porque todos compartilham o mesmo name.
<fieldset> dentro de <fieldset> é válido e é a marcação correta aqui: "Participação" é o bloco temático, e dentro dele há dois grupos de escolha que precisam, cada um, da sua própria legenda.
Os <br> entre as caixas de seleção são um paliativo até a Aula 06: sem CSS, todas ficariam na mesma linha. Não use <br> para espaçamento em conteúdo normal — aqui ele marca uma quebra real entre opções de uma escolha.
site-evento/inscricao.html (trecho: quarto <fieldset>)
HTML
<fieldset><legend>Informações complementares</legend><p><labelfor="acessibilidade">Você precisa de algum recurso de acessibilidade?</label><textareaid="acessibilidade"name="acessibilidade"rows="4"cols="50"maxlength="500"placeholder="Ex.: intérprete de Libras, lugar reservado, material ampliado"></textarea></p><p><labelfor="restricao">Restrição alimentar (para o coffee break)</label><inputtype="text"id="restricao"name="restricao"maxlength="120"placeholder="Ex.: sem lactose"></p><p><labelfor="como-soube">Como você ficou sabendo do evento?</label><selectid="como-soube"name="como_soube"><optionvalue="">Prefiro não responder</option><optionvalue="professor">Um professor divulgou em aula</option><optionvalue="redes">Redes sociais do curso</option><optionvalue="colega">Um colega me contou</option><optionvalue="cartaz">Cartaz no campus</option></select></p><p><inputtype="checkbox"id="termos"name="termos"value="aceito"required><labelfor="termos">
Li e aceito o regulamento do evento e autorizo o uso da minha imagem
nas fotos de divulgação (obrigatório).
</label></p><p><buttontype="submit">Enviar inscrição</button></p></fieldset></form>
O aceite dos termos é uma caixa de seleção com required: o formulário não é enviado enquanto ela não for marcada. Repare que o rótulo vem depois do campo — a convenção para caixas de seleção e rádios é rótulo à direita, ao contrário dos campos de texto.
O campo name="como_soube" usa sublinhado em vez de hífen. Não é obrigatório, mas é hábito: chaves com hífen dão trabalho em algumas linguagens do lado do servidor, onde dados.como-soube seria interpretado como uma subtração.
Abra o DevTools (F12), vá à aba Network e marque Preserve log.
Preencha o formulário inteiro, marcando duas oficinas.
Envie. Vai aparecer uma requisição inscrever com status 404 — esperado, porque não existe servidor nesse endereço.
Clique nela e abra Payload (Chrome) ou Requisição (Firefox). Confira: cada campo aparece com o seu name; oficinas aparece duas vezes, uma para cada caixa marcada; origem aparece com site-evento, mesmo você nunca tendo digitado nada; a camiseta, se você deixou "Não quero camiseta", aparece com valor vazio.
Agora troque method="post" por method="get", recarregue e envie de novo. Os mesmos dados aparecem na URL, separados por &. Repare na codificação: o espaço do nome virou +, o @ do e-mail virou %40 e os parênteses do telefone viraram %28 e %29.
Volte para method="post". Inscrição não é consulta.
A página abre no Live Server com o mesmo cabeçalho e rodapé das outras quatro.
Enviar o formulário vazio mostra a mensagem do navegador no primeiro campo obrigatório, sem sair da página.
Clicar no texto de cada rótulo coloca o foco (ou marca a opção) no campo correspondente — teste em todos, inclusive nos rádios e nas caixas de seleção.
Marcar um turno desmarca automaticamente o anterior; marcar duas oficinas mantém as duas marcadas.
Só com o teclado, você consegue percorrer o formulário inteiro na ordem visual e enviar com Enter.
Na aba Network, a requisição POST traz um par nome=valor para cada campo preenchido — inclusive o hidden.
O validador do W3C não acusa nenhum erro.
Resultado esperado: uma página de inscrição feia e completamente funcional, com quatro blocos de campos, mais de vinte controles, validação nativa em mais de dez deles e zero erros no W3C. A aparência é assunto da Unidade 2; hoje o que importa é que o dado sai certo.
A1. Qual type de <input> é o mais adequado para cada dado, e por quê: (a) CPF; (b) data de nascimento; (c) senha; (d) avaliação de 0 a 10 com controle deslizante; (e) upload de currículo; (f) cor favorita; (g) e-mail; (h) site pessoal?
A2. Cite três diferenças entre GET e POST e dê um exemplo de formulário real para cada método.
A3. Por que placeholder não substitui <label>? Dê três motivos diferentes.
A4. Qual é a diferença prática entre readonly e disabled? Em qual situação a escolha errada faz um dado desaparecer do servidor?
A5. O que acontece com um campo preenchido que não tem o atributo name quando o formulário é enviado? Como você comprovaria isso em 30 segundos?
A6. Escreva um grupo de três botões de rádio para escolha de turno, com <fieldset>, <legend> e <label> corretos, e com o grupo marcado como obrigatório.
A7. Por que os botões de rádio de um grupo precisam ter o mesmo name? O que acontece na tela se cada um tiver um name diferente?
A8. O que faz o atributo required? Ele garante que o dado chegará preenchido ao servidor? Justifique em duas frases.
A9. Escreva um <select> de estados do Centro-Oeste com um <optgroup> chamado "Centro-Oeste" contendo MT, MS, GO e DF, e uma primeira opção que faça o required funcionar.
A10. Qual é a diferença entre <select> e <datalist>? Dê um exemplo de dado do site do evento em que cada um seria a escolha certa.
A11. Explique o que este pattern aceita, caractere por caractere: [0-9]{2}/[0-9]{2}/[0-9]{4}. Escreva um title adequado para ele.
A12. Um <button> sem atributo type dentro de um <form>: o que ele faz ao ser clicado? Que bug isso costuma causar?
B1. Construa exercicios/aula03/login.html: um formulário de login com e-mail, senha (mínimo 8 caracteres), caixa de seleção "manter conectado", botão "Entrar" e um link "Esqueci minha senha". Todos os campos com <label>, autocomplete correto (email e current-password) e validação nativa. O método precisa ser post.
Resultado esperado: enviar com os campos vazios mostra a mensagem do navegador no e-mail; digitar uma senha de 5 caracteres mostra a mensagem de tamanho mínimo; clicar no texto "Manter conectado" marca a caixa; a aba Network mostra a senha no corpo da requisição, e nunca na URL.
Dica
O autocomplete de senha tem dois valores diferentes: current-password em telas de login e new-password em cadastro e troca de senha. Usar o valor certo é o que faz o gerenciador de senhas do celular oferecer a senha salva em vez de sugerir uma nova.
B2. Construa exercicios/aula03/matricula.html: um formulário de matrícula em disciplinas com dados do aluno (nome, matrícula, curso), seleção múltipla de disciplinas (caixas de seleção, no mínimo seis opções, com a carga horária no texto do rótulo), turno preferencial (rádios), observações (<textarea> de 500 caracteres) e aceite do regulamento (obrigatório). Use um <fieldset> por grupo.
Resultado esperado: quatro <fieldset> com <legend>; marcar três disciplinas gera três pares disciplinas=… na aba Network; o envio é bloqueado enquanto o aceite não estiver marcado; zero erros no validador do W3C.
Dica
Todas as caixas de seleção de disciplinas usam o mesmoname (disciplinas) e value diferentes — é assim que o servidor recebe uma lista. Cada uma precisa, ainda assim, de um id único para o seu <label>.
B3. Construa exercicios/aula03/busca.html: um formulário de busca com method="get", um campo name="q", um <select> de categoria (palestras, minicursos, oficinas) e um <select> de ordenação (por horário, por título). Envie e transcreva a URL gerada, explicando em uma frase cada parte da query string e por que este formulário usa GET e não POST.
Resultado esperado: a URL final tem a forma busca.html?q=acessibilidade&categoria=oficinas&ordem=titulo; o texto explica os três pares, o papel do ? e do &, e argumenta que a busca é idempotente e compartilhável.
Dica
Use action="busca.html" (a própria página) para não cair em 404. Depois de enviar, copie a URL da barra de endereço e cole em uma aba nova: se a mesma busca aparece, você acabou de demonstrar por que buscas usam GET.
B4. Caça aos problemas. O formulário abaixo tem pelo menos oito problemas de acessibilidade, semântica e funcionamento. Encontre todos, reescreva o formulário corrigido e entregue um documento listando cada problema, a correção e o motivo.
Resultado esperado: uma versão corrigida com method declarado, <label> associado a cada campo, name em todos, type adequado a cada dado, um único name para o grupo de rádios (dentro de <fieldset> com <legend>), um <button type="submit"> no lugar da imagem clicável e validação nativa nos campos aplicáveis; e uma lista com pelo menos oito itens justificados.
Dica
Comece pelos problemas que impedem o formulário de funcionar (campos sem name, rádios com name diferente, botão que não é botão) e depois passe aos de acessibilidade (rótulos, agrupamento, foco). O campo de senha com type="text" e o CEP com type="number" são dois erros de tipo, cada um com uma consequência diferente.
B5. Estenda a página de contato. Em site-evento/contato.html, construa um formulário de mensagem com: nome, e-mail, assunto (<select> com quatro opções), mensagem (<textarea> com maxlength="600") e uma escolha entre "quero receber resposta por e-mail" e "por telefone" (rádios), com o telefone aparecendo como campo opcional. Método post, action="/contato".
Resultado esperado: o formulário completo dentro do <main> da contato.html, com <fieldset> no grupo de rádios, autocomplete correto em nome, e-mail e telefone, e zero erros no W3C.
Dica
Um <select> de assunto com value="" na primeira opção mais required garante uma escolha consciente. Para o telefone opcional, não use required: em HTML puro você ainda não consegue tornar um campo obrigatório em função de outro — isso é JavaScript, e você fará na Aula 14.
C1. Formulário de matrícula institucional. Reproduza, em HTML puro, um formulário de matrícula acadêmica com pelo menos 25 campos organizados em cinco<fieldset>: identificação, documentos, endereço, dados acadêmicos e informações complementares. Requisitos: todo campo com <label> associado e autocomplete semântico; validação nativa em todos os campos aplicáveis (required, pattern, min/max, minlength); ordem de tabulação lógica sem tabindex positivo; zero erros no validador do W3C. Ao final, escreva meia página justificando a escolha do type de cada campo não trivial (CPF, CEP, data, matrícula, renda, telefone).
Dica
Comece pela lista de dados em papel, agrupando-os antes de escrever uma linha de HTML: os cinco <fieldset> saem naturalmente do agrupamento. Para o endereço, use os valores de autocomplete específicos (postal-code, address-line1, address-level2, address-level1) — é o que faz o navegador preencher tudo depois do CEP. Teste com o teclado antes de validar no W3C.
C2. Formulário de inscrição do projeto autoral. No seu meu-projeto/, construa a página de formulário equivalente à inscricao.html — pedido, reserva, cadastro, matrícula, agendamento: o que fizer sentido no seu tema — com no mínimo 12 campos, 3 <fieldset>, um <select> com <optgroup>, um <datalist>, um grupo de rádios, um grupo de caixas de seleção, um <textarea> e validação nativa em pelo menos 5 campos. Este formulário é um dos requisitos do Marco 1 do projeto (Aula 06).
Dica
Não copie os campos do site do evento trocando os textos: pergunte-se que dados o seu domínio realmente precisa coletar. Um brechó precisa de tamanho e forma de pagamento; uma agenda de quadras precisa de data, horário e número de jogadores. Cada campo que você não usar depois é um campo que não deveria existir.
Todo mundo já desistiu de preencher um cadastro. Qual é o menor formulário capaz de inscrever alguém em um evento? Pegue a inscricao.html que você acabou de construir e faça o caminho inverso: corte tudo que não é indispensável e chegue a uma versão de três campos. Depois defenda cada corte. A pergunta que guia o exercício é sempre a mesma: "o que a organização do evento faria de diferente se soubesse este dado?" — se a resposta for "nada", o campo sai.
Critérios de pronto
Um arquivo inscricao-minima.html com exatamente três campos visíveis, todos com <label>, autocomplete e validação nativa.
Um arquivo cortes.md listando cada campo removido da versão completa e a justificativa em uma linha.
Pelo menos dois campos removidos com a justificativa "pode ser perguntado depois" e a explicação de quando seria o momento certo de perguntar.
Uma medição honesta: cronometre o preenchimento das duas versões (a completa e a mínima) por outra pessoa e registre os dois tempos.
Pistas
Comece pelos campos que a organização consegue deduzir de outro campo (a matrícula diz o curso; o CPF diz a data de nascimento em outro sistema).
Procure "progressive disclosure" e pense em quais dados podem ser pedidos só no credenciamento, presencialmente.
Um campo obrigatório custa mais que um opcional: o opcional que quase ninguém preenche talvez devesse simplesmente não existir.
Nome, e-mail e mais um. Qual é o terceiro, e por quê?
Escolha um formulário público de verdade: matrícula de uma universidade, agendamento de uma prefeitura, cadastro de uma loja, abertura de conta de um banco. Abra o DevTools e leia o HTML dele. Quantos campos têm <label> de verdade? Quantos usam placeholder como rótulo? O grupo de rádios está em <fieldset>? Dá para preencher tudo só com o teclado? Você vai descobrir que sites enormes, com equipes enormes, erram exatamente as coisas desta aula — e vai aprender mais lendo o erro dos outros do que acertando sozinho.
Critérios de pronto
Um relatório de duas páginas com o endereço do formulário auditado e a data da auditoria.
Cada problema encontrado classificado por severidade (impede o uso / atrapalha / incomoda), com o trecho de HTML original copiado do DevTools e o trecho corrigido ao lado.
O resultado de uma passagem completa só com o teclado, descrevendo onde você travou (ou registrando que não travou).
Uma reescrita completa do formulário em HTML acessível, com todos os problemas corrigidos, que valide sem erros no W3C.
Pelo menos um problema encontrado com o checklist da seção 8 que uma ferramenta automática não apontaria.
Pistas
No DevTools, com o campo selecionado, procure o painel Accessibility: ele mostra o "nome acessível" calculado do elemento. Nome vazio significa rótulo ausente.
Rode também o Lighthouse (aba do DevTools) na categoria Accessibility, ou a extensão axe DevTools — mas lembre-se: as ferramentas pegam cerca de um terço dos problemas.
O teste do teclado é o mais revelador e não precisa de ferramenta nenhuma: Tab, Shift+Tab, setas, Espaço, Enter.
Compare a ordem do Tab com a ordem visual: quando o CSS move um campo de lugar, elas divergem — e o formulário fica confuso para quem não usa mouse.
Um mesmo campo "telefone", sem restrição nenhuma, recebeu na vida real: 66999990000, (66) 99999-0000, 66 9 9999 0000, +55 66 99999-0000, 9999-0000 (falar com a Maria) e não tenho. Nenhum está errado do ponto de vista de quem digitou — todos estão errados do ponto de vista de quem vai usar o dado. Sua missão: descobrir quanto de sujeira um formulário mal projetado gera e provar que dá para reduzir isso só com HTML.
Critérios de pronto
Uma página coleta-suja.html com cinco campos sem nenhuma restrição (telefone, CEP, data, valor em reais e nome) e uma página coleta-limpa.html com os mesmos cinco dados, agora com type, pattern, min/max, maxlength e title adequados.
Dez pessoas (colegas, familiares) preenchendo as duas versões, com as respostas registradas em uma tabela.
Uma contagem de quantos valores de cada versão poderiam entrar direto em um banco de dados sem tratamento manual.
Uma conclusão de dez linhas explicando qual restrição trouxe o maior ganho por linha de HTML escrita.
Um campo em que você não conseguiu impedir a sujeira só com HTML, com a explicação do porquê e do que faltaria (adiante o que a Aula 14 vai resolver).
Pistas
Para registrar as respostas sem servidor, use method="get" e copie a URL gerada a cada envio — a query string já é a sua tabela de coleta.
pattern resolve formato, mas não resolve conteúdo: 000.000.000-00 passa no padrão de CPF e não é um CPF válido. Guarde esse caso para a conclusão.
Compare type="date" com um campo de texto pedindo "DD/MM/AAAA": a diferença na sujeira costuma ser a maior de todas.
Para valores em reais, teste type="number" com step="0.01" e observe o que acontece quando alguém digita vírgula.
Formulários longos assustam. A solução clássica é dividir em etapas ("Etapa 2 de 4") — e quase todo mundo faz isso com JavaScript. Mas dá para construir um formulário de várias etapas usando apenas HTML, com uma página por etapa e os dados sendo carregados de uma para a próxima em campos ocultos. O resultado funciona sem JavaScript, funciona com o botão Voltar do navegador e é totalmente acessível. Descubra como e construa.
Critérios de pronto
Quatro páginas (etapa-1.html a etapa-4.html), cada uma com o seu <form method="get"> apontando para a etapa seguinte e um <progress> ou um texto "Etapa N de 4".
Os dados das etapas anteriores viajam até a última página e aparecem lá em campos ocultos, prontos para o envio final.
Uma página revisao.html que mostra, em texto, tudo o que foi preenchido antes do envio definitivo.
O botão Voltar do navegador funciona em todas as etapas e mantém as respostas já dadas.
Validação nativa em cada etapa: não é possível avançar com um campo obrigatório vazio.
Um texto de meia página comparando essa abordagem com a de "abas em JavaScript", listando duas vantagens e duas desvantagens concretas de cada uma.
Pistas
Com method="get", tudo que foi preenchido está na URL da próxima etapa — e uma URL pode ser lida, copiada e retomada depois.
<input type="hidden"> é o elemento que carrega um dado de uma página para a outra sem exibi-lo. Você precisará de um por campo herdado.
Sem servidor, os campos ocultos da etapa seguinte não se preenchem sozinhos: para o exercício, preencha os value à mão a partir da URL e explique no texto o que um servidor faria automaticamente.
Cuidado com dados sensíveis: uma senha nunca poderia atravessar etapas assim. Diga isso no seu texto final — vale ponto.
Parte 1 — Leitura (20 min). SILVA, Maurício Samy. Criando sites com HTML, capítulo de formulários. TERUEL, Evandro C. HTML 5 — Guia Prático, capítulo de formulários HTML5. Na MDN em pt-BR, o guia "Seu primeiro formulário" e a referência de <input>. Anote dois atributos que aparecem nas leituras e não apareceram nesta aula.
Parte 2 — Produção (30 min). Exercícios B2 (matrícula em disciplinas) e B4 (caça aos problemas), em arquivos .html comentados dentro de exercicios/aula03/. Para o B4, produza também o documento listando cada problema identificado, a correção aplicada e a justificativa.
Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: em 10 a 15 linhas, explique como a escolha do tipo de campo e das restrições de validação influencia a qualidade do que chega ao banco de dados. Traga um exemplo concreto de dado sujo que você já viu (um telefone impossível, um nome em caixa alta, uma data no futuro) e diga qual atributo desta aula teria evitado.
Critério de pronto: os dois arquivos abrem no navegador, validam sem erros no W3C, permitem envio apenas com os campos obrigatórios preenchidos e podem ser percorridos inteiramente pelo teclado, com o rótulo de cada campo funcionando ao clique.
Guarde: a pasta do projeto (ou, se você já usa Git — assunto do capítulo 02 da trilha Deploy e da Aula 15 —, faça commit e push).
Ao fim desta aula, em site-evento/ e em meu-projeto/:
[ ] inscricao.html com um <form method="post" action="/inscrever"> e um <input type="hidden"> de origem.
[ ] Quatro <fieldset> de primeiro nível com <legend> (dados pessoais, dados acadêmicos, participação e informações complementares), com os grupos de escolha em <fieldset> aninhados.
[ ] Pelo menos oito tipos diferentes de <input> em uso (text, email, tel, date, number, radio, checkbox, hidden).
[ ] Um <select> com <optgroup> e primeira opção value="", um <datalist> e um <textarea> com maxlength.
[ ] <label> associado a todos os campos, com a obrigatoriedade indicada no texto do rótulo.
[ ] Validação nativa em pelo menos seis campos: required, minlength, min/max e dois pattern acompanhados de title.
[ ] Um grupo de rádios com o mesmo name e um grupo de caixas de seleção com o mesmo name.
[ ] autocomplete semântico em nome, e-mail, telefone, instituição e data de nascimento.
[ ] O formulário inteiro percorrível e enviável apenas com o teclado.
[ ] contato.html com o formulário de mensagem do exercício B5.
[ ] Zero erros no validador do W3C nas cinco páginas.
[ ] Em meu-projeto/: o formulário equivalente do seu tema (exercício C2), com no mínimo 12 campos.
SILVA, Maurício Samy. Criando sites com HTML: sites de alta qualidade com HTML e CSS. Novatec, 2008 — capítulo de formulários.
TERUEL, Evandro C. HTML 5 — Guia Prático. Saraiva, 2014 — capítulo de formulários HTML5 (Minha Biblioteca).
MILETTO, Evandro M.; BERTAGNOLLI, Silvia C. Desenvolvimento de software II. Bookman, 2014 — capítulo sobre entrada de dados em aplicações web.
Na próxima aula você fecha o capítulo de formulários com os campos que faltaram — upload de arquivos com enctype, <output>, <progress>, <meter>, inputmode e campos ligados a um formulário à distância — e passa para o que dá vida às páginas: listas bem estruturadas, imagens responsivas com srcset e <picture>, vídeo com legendas e conteúdo externo em <iframe>.
Completar um formulário com campos avançados (<output>, <progress>, <meter>, upload múltiplo, inputmode, atributos form e formaction) e justificar a escolha de cada um.
Aplicar listas ordenadas, não ordenadas e de definição para organizar conteúdo — e explicar por que um menu de navegação é uma lista.
Inserir imagens com alt adequado a cada situação (informativa, decorativa, dentro de link, logotipo) e escolher o formato de arquivo certo.
Usar <figure>, <figcaption>, srcset, sizes e <picture> para imagens responsivas e com formatos modernos.
Incorporar áudio, vídeo (com legendas em WebVTT) e conteúdo externo em <iframe> de forma acessível e segura.
Medir o peso de uma página na aba Network do DevTools e reduzi-lo otimizando as mídias.
[ ] VS Code com a extensão Live Server e o navegador com DevTools (F12) funcionando.
[ ] A pasta do projeto do evento com as cinco páginas validadas no W3C: index.html, programacao.html, palestrantes.html (Aula 02), inscricao.html com o formulário (Aula 03) e contato.html (esqueleto da Aula 02).
[ ] Três fotos suas (ou livres de direitos) e um vídeo curto em .mp4 — usaremos para medir peso de página. Se não tiver, baixe amostras do Unsplash ou do Pexels.
Na Aula 03 você construiu a página de inscrição da Semana Acadêmica de Sistemas de Informação: <form>, tipos de <input>, <label>, <select>, <datalist>, <fieldset> e validação nativa. Hoje você fecha o capítulo de formulários com os campos que faltavam e passa para o que dá vida a uma página: listas bem estruturadas, imagens que carregam rápido, vídeo com legenda e conteúdo externo incorporado. Ao final, as cinco páginas do site do evento — todas já existentes — ganham logo, banner responsivo, listas de dois níveis, vídeo legendado, áudio, mapa e os campos de formulário que faltavam.
Na Aula 03 você viu os tipos de <input>, <textarea>, <select>, <datalist>, <fieldset> e a validação nativa. Faltam alguns elementos e atributos que aparecem em formulários reais — inscrição em evento, cadastro com anexo, pesquisa de satisfação — e que resolvem problemas específicos.
Um campo type="file" só funciona de fato quando o formulário sabe empacotar bytes, não apenas texto. Isso é responsabilidade do atributo enctype:
inscricao.html (trecho)
HTML
<formaction="/inscrever"method="post"enctype="multipart/form-data"><fieldset><legend>Comprovante de matrícula</legend><labelfor="comprovante">Arquivo (PDF ou imagem, até 2 MB)</label><inputtype="file"id="comprovante"name="comprovante"accept=".pdf,image/png,image/jpeg"required><labelfor="certificados">Certificados anteriores (opcional, vários)</label><inputtype="file"id="certificados"name="certificados"accept="application/pdf"multiple></fieldset><buttontype="submit">Enviar inscrição</button></form>
Atributo
Efeito
enctype="multipart/form-data"
Sem ele, o navegador envia só o nome do arquivo, não o conteúdo
accept
Filtra o seletor de arquivos por extensão ou tipo MIME (image/* aceita qualquer imagem)
multiple
Permite selecionar vários arquivos; o name vira uma lista no servidor
capture="environment"
No celular, abre a câmera traseira direto em vez da galeria
⚠️ Atençãoaccept é conveniência, não segurança. O usuário pode trocar o filtro para "Todos os arquivos" e enviar o que quiser. Como toda validação no navegador, o servidor precisa checar tipo e tamanho de novo — esse lembrete vai se repetir até a última aula.
multiple também vale para type="email": o campo aceita vários endereços separados por vírgula e valida cada um.
HTML
<labelfor="coautores">E-mails dos coautores (separe por vírgula)</label><inputtype="email"id="coautores"name="coautores"multipleplaceholder="ana@exemplo.br, joao@exemplo.br">
1.3 <output>, <progress> e <meter> — mostrar, não coletar¶
Nem tudo dentro de um formulário é um campo de entrada. Três elementos existem para exibir valores com significado:
HTML
<!-- <output>: resultado de um cálculo. O atributo for aponta os campos envolvidos --><labelfor="lote">Lote</label><selectid="lote"name="lote"><optionvalue="30">1º lote — R$ 30</option><optionvalue="45">2º lote — R$ 45</option></select><labelfor="qtd">Quantidade de ingressos</label><inputtype="number"id="qtd"name="qtd"min="1"max="5"value="1"><p>Total: <outputid="total"for="lote qtd">R$ 30</output></p><!-- <progress>: andamento de uma tarefa (etapa 2 de 4 da inscrição) --><labelfor="etapa">Andamento da inscrição</label><progressid="etapa"value="2"max="4">2 de 4</progress><!-- <meter>: medida dentro de uma faixa conhecida (vagas ocupadas) --><labelfor="vagas">Vagas ocupadas no minicurso de Git</label><meterid="vagas"value="38"min="0"max="40"low="20"high="35"optimum="0">38 de 40</meter>
Elemento
Serve para
Não serve para
<output>
Resultado calculado a partir de outros campos (total, IMC, prazo)
Texto estático
<progress>
Quanto de uma tarefa já foi feito (upload, passo a passo)
Medida que pode subir e descer
<meter>
Valor escalar em uma faixa: espaço em disco, vagas, força da senha
Andamento de tarefa
O texto entre as tags de <progress> e <meter> é o fallback: aparece em navegadores muito antigos e é o que um leitor de tela lê quando não consegue interpretar a barra. Sem JavaScript o <output> fica parado no valor inicial — na Aula 13 você o atualizará em tempo real.
1.4 Campos fora do <form> e botões que mudam o destino¶
Dois atributos pouco conhecidos resolvem layouts que parecem impossíveis:
HTML
<formid="form-inscricao"action="/inscrever"method="post"><labelfor="nome">Nome completo</label><inputtype="text"id="nome"name="nome"required></form><!-- Este campo está FORA do form no HTML, mas pertence a ele pelo atributo form --><labelfor="newsletter">Quero receber novidades do evento</label><inputtype="checkbox"id="newsletter"name="newsletter"form="form-inscricao"><!-- Botões podem sobrescrever action, method e a validação do form --><buttontype="submit"form="form-inscricao">Enviar inscrição</button><buttontype="submit"form="form-inscricao"formaction="/rascunho"formnovalidate>Salvar rascunho</button>
form="id" liga um campo ou botão a um formulário, mesmo estando em outra região da página (um botão fixo no rodapé, por exemplo).
formaction e formmethod mudam o destino só daquele botão.
formnovalidate envia sem passar pela validação nativa — útil para "salvar rascunho", em que campos obrigatórios ainda podem estar vazios.
Valores úteis: numeric (só dígitos), decimal (dígitos e vírgula/ponto), tel, email, url, search. O type continua controlando a validação; o inputmode controla o teclado.
🔬 Investigue
Abra o DevTools, ative o modo dispositivo (Ctrl+Shift+M) e escolha um celular. Crie três campos: type="text", type="text" inputmode="numeric" e type="number". Foque cada um. O emulador não mostra o teclado virtual, mas na aba Elements você verá que a validação de type="number" rejeita "12abc" e a de type="text" inputmode="numeric" aceita — a diferença entre validar e sugerir teclado fica visível. Se tiver um celular na mão, abra a página pelo IP do Live Server na mesma rede Wi-Fi e veja o teclado de verdade.
Uma lista é uma sequência de itens relacionados. Parece óbvio, mas o HTML oferece três tipos com semânticas diferentes, e escolher o certo muda como o leitor de tela anuncia o conteúdo ("lista com 5 itens") e como você o estilizará na Unidade 2.
<!-- Lista não ordenada: a ordem NÃO importa --><ul><li>HTML</li><li>CSS</li><li>JavaScript</li></ul><!-- Lista ordenada: a ordem IMPORTA (passos, ranking, cronograma) --><ol><li>Analisar a URL</li><li>Resolver o DNS</li><li>Abrir a conexão TCP</li><li>Enviar a requisição HTTP</li></ol><!-- Lista de definições: pares termo → descrição (glossário, ficha técnica) --><dl><dt>HTML</dt><dd>Linguagem de marcação que estrutura o conteúdo.</dd><dt>CSS</dt><dd>Linguagem de estilo que define a apresentação.</dd><dt>JavaScript</dt><dd>Linguagem de programação que adiciona comportamento.</dd></dl>
Elemento
Quando usar
Exemplo real
<ul>
Itens sem ordem significativa
Menu de navegação, lista de requisitos, tags
<ol>
A sequência carrega informação
Passo a passo, programação do dia, ranking
<dl>
Pares nome/valor
Glossário, ficha técnica de produto, perguntas e respostas
Um <dt> pode ter vários <dd> (um termo com duas definições) e vários <dt> podem compartilhar um <dd> (sinônimos). Isso torna a <dl> a escolha certa para FAQ e para a ficha de um palestrante (Nome → valor, Instituição → valor, Tema → valor).
<!-- Numeração romana, começando em 3, decrescente --><oltype="I"start="3"reversed><li>Terceiro colocado</li><li>Segundo colocado</li><li>Campeão</li></ol><!-- Pular a numeração em um item --><ol><li>Abertura</li><livalue="5">Encerramento (os itens 2 a 4 estão em outra página)</li></ol>
Atributo
Valores
Uso
type
1, a, A, i, I
Tipo do marcador (numérico, alfabético, romano)
start
inteiro
Primeiro número da sequência
reversed
booleano
Conta para baixo (top 10, contagem regressiva)
value (no <li>)
inteiro
Força o número de um item específico
Na Unidade 2 você verá que a aparência do marcador (bolinha, traço, ícone) é assunto de CSS (list-style). Os atributos acima mudam o significado — por isso ficam no HTML.
2.3 Listas aninhadas: o erro que o validador sempre pega¶
O <ul> filho vai dentro do <li> pai, nunca solto entre dois <li>:
HTML
<!-- CERTO: o <ul> das sessões fica dentro do <li> da trilha --><ul><li>Trilha Front-end
<ul><li>HTML semântico na prática</li><li>CSS moderno sem framework</li></ul></li><li>Trilha Back-end
<ul><li>APIs REST com Node</li><li>Bancos de dados na nuvem</li></ul></li></ul>
HTML
<!-- ERRADO: <ul> diretamente dentro de <ul>. O W3C acusa "Element ul not allowed as child of element ul" --><ul><li>Trilha Front-end</li><ul><li>HTML semântico na prática</li></ul></ul>
Os únicos filhos permitidos de <ul> e <ol> são <li> (e <script>/<template>, que não renderizam). Qualquer outra coisa — um <p>, um <div>, outro <ul> — precisa estar dentro de um <li>.
Um menu de navegação é um conjunto de links relacionados. Marcá-lo como <ul> dentro de <nav> entrega três coisas de graça:
O leitor de tela anuncia "navegação, lista com 5 itens" — o usuário sabe o tamanho do menu antes de percorrê-lo.
A estrutura se mantém mesmo sem CSS: os itens ficam um embaixo do outro, legíveis.
Na Aula 07 você transformará essa mesma lista em um menu horizontal só com CSS, sem tocar no HTML.
programacao.html (trecho do cabeçalho)
HTML
<headerid="topo"><imgsrc="img/logo-sasi.svg"alt=""width="160"height="48"><h1>Semana Acadêmica de Sistemas de Informação</h1><p>Três noites de outubro · Auditório Central</p><navaria-label="Principal"><ul><li><ahref="index.html">Início</a></li><li><ahref="programacao.html"aria-current="page">Programação</a></li><li><ahref="inscricao.html">Inscrição</a></li><li><ahref="palestrantes.html">Palestrantes</a></li><li><ahref="contato.html">Contato</a></li></ul></nav></header>
aria-current="page" marca o link da página atual — o leitor de tela anuncia "página atual" e, na Unidade 2, você o usará como gancho para destacar o item ativo.
🧠 Você sabia?
O Safari (e, com ele, o VoiceOver do iPhone) remove a semântica de lista quando o CSS aplica list-style: none a uma <ul> fora de <nav>. O leitor de tela deixa de anunciar "lista com 5 itens" e lê os links soltos. Foi uma decisão deliberada da Apple, porque muitos desenvolvedores usavam listas para tudo. A solução é justamente a que você aprendeu hoje: manter o menu dentro de <nav>, onde a semântica é preservada, ou acrescentar role="list" na <ul> quando a lista precisa ser anunciada e vive fora da navegação.
Imagem é, disparado, o maior peso da maioria dos sites — e também a maior fonte de barreiras de acessibilidade. Uma <img> bem escrita resolve os dois problemas.
<imgsrc="img/campus-sinop.jpg"alt="Vista aérea do campus universitário em Sinop ao entardecer"width="800"height="600"loading="lazy">
Atributo
Função
src
Caminho da imagem (relativo à página, como nos links da Aula 02)
alt
Texto alternativo — obrigatório em toda <img>
width / height
Dimensões intrínsecas. O navegador reserva o espaço antes de baixar o arquivo e evita o "pulo" do layout
loading="lazy"
Só baixa quando a imagem estiver perto de entrar na tela. Ganho grande em páginas longas
title
Tooltip ao passar o mouse. Não substitui o alt
Sobre width e height: declare sempre os valores reais do arquivo. Eles não impedem o CSS de redimensionar depois (você fará max-width: 100% na Aula 08); servem para o navegador calcular a proporção e reservar a área. Sem eles, o texto abaixo da imagem "pula" quando ela chega — o que o Google mede como Cumulative Layout Shift e penaliza no ranqueamento.
O alt é o que uma pessoa cega ouve, o que aparece quando a imagem não carrega e o que o buscador indexa. A regra não é "descreva a imagem"; é "entregue a mesma informação que a imagem entrega para quem vê".
Situação
Regra
Exemplo
Imagem informativa
Descreva a informação, não a aparência
alt="Gráfico de barras: inscrições subiram de 120 na primeira edição para 180 na terceira"
Imagem decorativa
alt="" (vazio, mas presente) — o leitor de tela pula
Linha divisória, textura de fundo
Imagem dentro de link
Descreva o destino, não a imagem
alt="Página inicial da Wikipédia"
Logotipo
O nome, sem a palavra "logo"
alt="Wikipédia"
Nunca escreva alt="imagem", alt="foto.jpg", alt="clique aqui" ou omita o atributo. Sem alt, o leitor de tela lê o nome do arquivo — "DSC underline zero zero quatro dois ponto jpg".
Um teste simples: feche os olhos e peça para alguém ler só o alt. Se você entende o que a imagem comunica, está bom. Se ouve "foto bonita do campus", não está.
Quando a imagem tem uma legenda visível — figura numerada, crédito, explicação — envolva-a em <figure>:
HTML
<figure><imgsrc="img/modelo-cliente-servidor.png"alt="Diagrama: o navegador envia uma requisição HTTP e o servidor devolve uma resposta"width="640"height="360"><figcaption>
Figura 1 — Modelo cliente-servidor. Fonte: organização do evento.
</figcaption></figure>
alt e figcaption não são redundantes: o alt é para quem não vê a imagem; a figcaption é a legenda para todos. Um leitor de tela lê os dois — por isso a legenda não deve repetir o alt palavra por palavra. <figure> também serve para trechos de código, tabelas, citações e vídeos com legenda — qualquer conteúdo autocontido ao qual o texto se refere.
Compressão com perdas; ajuste a qualidade (75–85 costuma bastar)
PNG
Logos, ícones, capturas de tela
Sim
Sem perdas; arquivos maiores em fotos
WebP
Substituto moderno de JPG e PNG
Sim
Cerca de 30% menor; suporte universal nos navegadores atuais
AVIF
Compressão máxima
Sim
Ainda menor que WebP; suporte amplo, com exceções em navegadores antigos
SVG
Logos, ícones, diagramas
Sim
Vetorial: escala sem perder qualidade; arquivo minúsculo; é texto (XML)
GIF
Animações simples
Sim (binária)
Obsoleto; prefira vídeo em MP4/WebM (10× menor)
💡 Dica
Regra de otimização: nunca coloque na web uma foto de 4000×3000 px e 6 MB para exibi-la em um espaço de 400 px. Redimensione e comprima antes de subir — Squoosh (no navegador), TinyPNG ou GIMP. Um site de evento com dez fotos de celular sem tratamento pesa 40 MB; o mesmo site, tratado, pesa 1 MB. No 4G da zona rural de Sinop, isso é a diferença entre carregar em 2 segundos e em 1 minuto.
Um celular de 360 px de largura não precisa baixar a mesma foto de 1600 px que o monitor do laboratório. Com srcset você oferece a mesma imagem em vários tamanhos e o navegador escolhe:
HTML
<imgsrc="img/banner-800.jpg"srcset="img/banner-400.jpg 400w, img/banner-800.jpg 800w, img/banner-1600.jpg 1600w"sizes="(max-width: 600px) 100vw, 50vw"alt="Banner da Semana Acadêmica de Sistemas de Informação"width="800"height="450">
Como o navegador decide:
Lê sizes: "se a janela tem até 600 px, a imagem ocupará 100% da largura (100vw); senão, 50%".
Calcula a largura em pixels que a imagem terá na tela, multiplicada pela densidade da tela (um celular com tela 2× precisa do dobro).
Escolhe em srcset o menor arquivo que ainda cobre essa largura. Os números com w são as larguras reais de cada arquivo.
O src continua sendo o fallback para navegadores que não entendem srcset.
⚠️ Atençãosrcset sem sizes faz o navegador supor que a imagem ocupa 100% da janela — e ele baixará o arquivo grande mesmo para uma miniatura. Os dois atributos andam juntos.
srcset resolve tamanho; <picture> resolve formato (e também troca de enquadramento entre telas, a chamada "direção de arte"):
HTML
<picture><sourcesrcset="img/banner.avif"type="image/avif"><sourcesrcset="img/banner.webp"type="image/webp"><imgsrc="img/banner.jpg"alt="Banner da Semana Acadêmica de Sistemas de Informação"width="1600"height="900"loading="lazy"></picture>
O navegador lê os <source> de cima para baixo e usa o primeiro cujo type ele suporta. O <img> final é obrigatório: é ele que carrega alt, width, height, loading — e serve de garantia para navegadores antigos. Os <source> também aceitam srcset com vários tamanhos e sizes, combinando as duas técnicas.
🔬 Investigue
Coloque o <picture> acima em uma página, abra o DevTools na aba Network, filtre por Img e recarregue. Qual arquivo foi baixado — .avif, .webp ou .jpg? Agora, na coluna Type, confira o tipo real. Depois teste uma <img> com srcset de três larguras: redimensione a janela de 400 px até 1400 px recarregando a cada vez e anote qual arquivo veio em cada largura. Marque a opção Disable cache antes, senão o navegador reaproveita o arquivo anterior e o experimento mente.
Nem toda imagem deve ser lazy. A imagem principal do topo da página (o banner, o "herói") precisa chegar primeiro: ela é o que o usuário vê antes de qualquer rolagem e o que as ferramentas de performance medem como Largest Contentful Paint.
HTML
<!-- Banner do topo: prioridade alta, sem lazy --><imgsrc="img/banner-800.jpg"alt="Banner do evento"width="800"height="450"fetchpriority="high"decoding="async"><!-- Fotos da galeria, abaixo da dobra: lazy --><imgsrc="img/palestrante-01.jpg"alt="Ana Lúcia Ferreira"width="400"height="400"loading="lazy"decoding="async">
decoding="async" permite que o navegador decodifique a imagem sem travar o desenho do texto. Regra prática: as duas ou três primeiras imagens visíveis sem rolar não levam lazy; todas as outras levam.
Antes do HTML5, vídeo na web exigia o plugin Flash. Hoje <audio> e <video> são nativos, controláveis por teclado e legendáveis — desde que você use os atributos certos.
<audiocontrolspreload="metadata"><sourcesrc="midia/depoimento.mp3"type="audio/mpeg"><sourcesrc="midia/depoimento.ogg"type="audio/ogg">
Seu navegador não suporta áudio HTML5.
<ahref="midia/depoimento.mp3">Baixe o arquivo</a>.
</audio>
Assim como em <picture>, o navegador usa o primeiro <source> que consegue tocar. O texto e o link no final são o fallback — aparecem só onde o elemento não é suportado. MP3 é aceito em todo lugar; OGG é opcional.
<videocontrolswidth="640"height="360"poster="img/capa-abertura.jpg"preload="metadata"><sourcesrc="midia/abertura.mp4"type="video/mp4"><sourcesrc="midia/abertura.webm"type="video/webm"><tracksrc="legendas/abertura-pt.vtt"kind="captions"srclang="pt"label="Português"default>
Seu navegador não suporta vídeo HTML5.
<ahref="midia/abertura.mp4">Baixe o vídeo</a>.
</video>
Atributo
Efeito
controls
Exibe os controles nativos (play, volume, tempo, tela cheia, legendas)
autoplay
Inicia sozinho — os navegadores bloqueiam se houver som
muted
Sem som. Necessário para autoplay funcionar
loop
Repete indefinidamente
poster
Imagem exibida antes do play (a "capa")
preload
none (nada), metadata (só duração e dimensões), auto (o navegador decide)
playsinline
No iPhone, toca dentro da página em vez de abrir em tela cheia
Sobre preload: o padrão de cada navegador varia, e auto pode baixar o vídeo inteiro mesmo que o usuário nunca aperte play. Para vídeos que não são o foco da página, metadata é a escolha segura. MP4 (H.264) toca em todos os navegadores; WebM é menor, mas opcional.
Vídeo sem legenda é conteúdo inacessível para pessoas surdas — e, na prática, para qualquer pessoa no ônibus sem fone. Legenda não é extra: é requisito do critério 1.2.2 da WCAG, e volta no checklist de qualidade do Marco do projeto.
O <track> aponta para um arquivo WebVTT, um texto simples com marcações de tempo:
legendas/abertura-pt.vtt
Texto
WEBVTT
00:00:00.000 --> 00:00:03.500
Bem-vindos à Semana Acadêmica de Sistemas de Informação.
00:00:04.000 --> 00:00:08.000
Nesta edição, teremos três dias de palestras, minicursos e maratona de programação.
00:00:08.500 --> 00:00:12.000
As inscrições estão abertas no site do evento.
00:00:12.500 --> 00:00:15.000
[música de abertura]
Regras do formato: a primeira linha é WEBVTT; cada bloco tem um intervalo início --> fim no formato hh:mm:ss.mmm (as horas podem ser omitidas) seguido do texto; blocos separados por uma linha em branco. Sons relevantes vão entre colchetes — é isso que diferencia captions (para quem não ouve) de subtitles (tradução).
kind
Para que serve
captions
Legenda completa com falas e sons relevantes (acessibilidade)
subtitles
Tradução das falas (o usuário ouve, mas não entende o idioma)
descriptions
Descrição textual do que aparece na tela, para pessoas cegas
chapters
Marcadores de capítulo para navegar no vídeo
Um <video> pode ter vários <track> — um por idioma ou por tipo. default marca o que liga sozinho. O Live Server serve arquivos .vtt sem configuração; em servidores próprios, o tipo MIME text/vtt precisa estar registrado (você verá isso na trilha Deploy).
🔎 Por baixo do capô
Por que os navegadores bloqueiam autoplay com som? Há alguns anos, Chrome e Safari adotaram políticas que só permitem reprodução automática sem som ou depois de uma interação do usuário com a página (clique, toque, tecla). O motivo foi o abuso de anúncios em vídeo que começavam a gritar assim que a página abria. Hoje, autoplay sem muted simplesmente não toca — e o console avisa: play() failed because the user didn't interact with the document first. Se a interface depende de vídeo de fundo, ele será silencioso.
Um <iframe> abre uma outra página dentro da sua: um vídeo do YouTube, um mapa, um formulário do Google, um widget de clima.
HTML
<iframesrc="https://www.youtube.com/embed/ID_DO_VIDEO"title="Vídeo: abertura da edição anterior da Semana Acadêmica"width="560"height="315"loading="lazy"allow="fullscreen; picture-in-picture"referrerpolicy="strict-origin-when-cross-origin"></iframe>
Atributo
Função
src
Endereço da página incorporada (use a URL de embed que o serviço fornece, não a do site)
title
Obrigatório para acessibilidade: é o que o leitor de tela anuncia ao entrar no quadro
loading="lazy"
Só carrega quando estiver perto da tela — iframes pesam mais que imagens
allow
Lista de permissões concedidas ao conteúdo (tela cheia, câmera, geolocalização)
sandbox
Restringe o que o conteúdo pode fazer (scripts, formulários, pop-ups); vazio = bloqueia tudo
Mapa incorporado do Google Maps (o serviço gera este código em "Compartilhar → Incorporar um mapa"):
HTML
<iframesrc="https://www.google.com/maps/embed?pb=CODIGO_GERADO_PELO_MAPS"title="Mapa: localização do Auditório Central, em Sinop"width="600"height="450"loading="lazy"referrerpolicy="no-referrer-when-downgrade"></iframe>
⚠️ Atenção
Um <iframe> executa código de terceiros dentro da sua página: ele pode rastrear o visitante, mostrar anúncios e, se o serviço for comprometido, servir conteúdo malicioso. Só incorpore fontes confiáveis. Para conteúdo que você não controla (um widget de fórum, por exemplo), use sandbox="allow-scripts allow-same-origin" e libere apenas o necessário. E nunca coloque um <iframe> de um site que você não tem permissão para incorporar: muitos enviam o cabeçalho X-Frame-Options: DENY e o quadro fica em branco — esse é um dos erros mais comuns da aula.
Também existem <embed> e <object>, dos tempos dos plugins. Hoje só têm um uso corrente: exibir um PDF dentro da página (<object data="edital.pdf" type="application/pdf">). Para todo o resto, <iframe>.
Tudo o que você aprendeu hoje tem um custo em bytes. A aba Network do DevTools mostra esse custo com precisão:
Abra o DevTools (F12) e vá à aba Network.
Marque Disable cache — senão a segunda visita esconde o peso real.
Recarregue a página (Ctrl+R).
Leia a barra de status no rodapé: N requests · X MB transferred · Y MB resources · Z s.
Clique no cabeçalho da coluna Size para ordenar: os maiores arquivos ficam no topo. Quase sempre são imagens ou vídeo.
No menu Throttling (ao lado de "Disable cache"), escolha Slow 4G e recarregue: é assim que metade dos seus usuários vê o site.
Faixa de peso total
Avaliação
até 1 MB
Bom para uma página com fotos
1 a 3 MB
Aceitável; procure a imagem mais pesada e otimize
acima de 3 MB
Problema. Alguém subiu foto de celular sem tratar
A coluna Size mostra dois números: o de cima é o transferido (comprimido pela rede); o de baixo, o tamanho real do recurso. Para imagens e vídeo eles são quase iguais — esses formatos já são comprimidos e a rede não ganha nada extra. Para HTML e CSS, o transferido costuma ser 3 a 5× menor (compressão gzip/brotli do servidor).
Fluxo de otimização de uma imagem no Squoosh (squoosh.app, roda no navegador, sem instalar nada):
Arraste a foto original.
Em Resize, reduza para o dobro da largura em que ela será exibida (uma foto mostrada em 400 px sai com 800 px, para telas de alta densidade).
Em Compress, escolha WebP com qualidade 75 e compare visualmente com o original arrastando a linha divisória.
Repita em AVIF e MozJPEG para gerar os três formatos do <picture>.
Anote o peso antes e depois — você fará isso na Mão na massa.
💻 Mão na massa — Mídias, listas e campos avançados nas páginas do evento¶
As cinco páginas do site da Semana Acadêmica de Sistemas de Informação já existem: index.html, programacao.html e palestrantes.html nasceram na Aula 02, e inscricao.html recebeu o formulário completo na Aula 03. Hoje nenhuma página é criada do zero — você amplia o que já está lá.
⚠️ Atenção
Nada do que as Aulas 02 e 03 entregaram é jogado fora. O <header id="a04-topo"> com o <h1> do site, o <h2> que titula cada página, o rodapé de três parágrafos e a tabela da programação continuam exatamente onde estão. Todo o trabalho de hoje é acrescentar — se em algum passo você se pegar apagando conteúdo antigo, pare e releia o passo.
Passo 1 — Logo e navegação marcada nas cinco páginas¶
O <head> de cada página não muda hoje: charset, viewport, description, meta name="author" e title continuam como você os escreveu na Aula 02. O que muda é o cabeçalho: entram o logo e dois atributos de acessibilidade do <nav>.
programacao.html (trecho: <header>, depois da mudança)
HTML
<headerid="topo"><imgsrc="img/logo-sasi.svg"alt=""width="160"height="48"><h1>Semana Acadêmica de Sistemas de Informação</h1><p>Três noites de outubro · Auditório Central</p><navaria-label="Principal"><ul><li><ahref="index.html">Início</a></li><li><ahref="programacao.html"aria-current="page">Programação</a></li><li><ahref="inscricao.html">Inscrição</a></li><li><ahref="palestrantes.html">Palestrantes</a></li><li><ahref="contato.html">Contato</a></li></ul></nav></header>
Três decisões deste passo:
alt="" no logo. Pela tabela da seção 3.2, um logotipo leva o nome da organização no alt — mas aqui o <h1> logo abaixo já diz exatamente esse nome. Repetir faria o leitor de tela anunciar o mesmo texto duas vezes seguidas; então a imagem é decorativa e recebe alt vazio (presente, e vazio). Na Aula 07, quando o logo virar um link para a página inicial, o alt passa a descrever o destino.
aria-label="Principal" distingue este <nav> de qualquer outro que a página venha a ter.
aria-current="page" muda em cada página: em index.html ele vai no link "Início", em palestrantes.html no link "Palestrantes", e assim por diante. Repita o cabeçalho nas cinco páginas, trocando só onde o atributo está.
Em index.html, o banner entra como primeiro elemento do <main>, antes da lista "Nesta página". Ele combina as duas técnicas da seção 3: <picture> escolhe o formato, srcset/sizes escolhem o tamanho. Como é a primeira imagem visível, ela não leva lazy.
index.html (trecho: início do <main>)
HTML
<main><picture><sourcesrcset="img/banner.avif"type="image/avif"><sourcesrcset="img/banner.webp"type="image/webp"><imgsrc="img/banner-800.jpg"srcset="img/banner-400.jpg 400w, img/banner-800.jpg 800w, img/banner-1600.jpg 1600w"sizes="(max-width: 700px) 100vw, 1100px"alt="Auditório lotado na abertura da edição anterior da Semana Acadêmica"width="1600"height="900"fetchpriority="high"decoding="async"></picture><p>Nesta página:</p>
Gere os seis arquivos no Squoosh a partir da mesma foto: três larguras em JPG e a versão de 1600 px também em WebP e AVIF. Anote o peso de cada um em um comentário HTML acima do <picture> — você vai comparar no Passo 10.
A tabela da programação continua sendo a fonte de horários. O que falta é uma visão por assunto, e ela é uma lista de dois níveis. Em programacao.html, logo depois do </table> e antes do parágrafo "Voltar ao topo", acrescente:
programacao.html (trecho: novo <section> dentro do <main>)
HTML
<sectionid="trilhas"><h3>Trilhas do evento</h3><p>As mesmas atividades da tabela acima, agrupadas por assunto.</p><ul><li>Desenvolvimento Web
<ul><li>Abertura e palestra magna: o futuro do desenvolvimento web</li><li>Minicurso: Git e GitHub do zero</li><li>Minicurso: acessibilidade na prática</li><li>Maratona de programação</li></ul></li><li>Ciência de Dados
<ul><li>Dashboards que os produtores realmente usam</li><li>Dados abertos e cidades inteligentes</li></ul></li><li>Inteligência Artificial
<ul><li>Minicurso: primeiros passos com redes neurais</li><li>Visão computacional no controle de pragas</li></ul></li><li>Segurança
<ul><li>Segurança em aplicações web: dez erros comuns</li><li>Minicurso: phishing e engenharia social</li></ul></li></ul></section>
Repare que cada <ul> filho está dentro do <li> pai — é o erro da seção 2.3 que o validador sempre pega. O título é <h3> porque o <h2>Programação</h2> no topo do <main> é o título da página; as seções que vêm depois ficam um nível abaixo.
Passo 4 — As cinco mais procuradas, com <ol reversed>¶
Ainda em programacao.html, depois da seção de trilhas, um ranking. Aqui os atributos da lista ordenada (seção 2.2) fazem trabalho semântico de verdade: a contagem é decrescente, e a marcação diz isso.
programacao.html (trecho: novo <section> dentro do <main>)
HTML
<sectionid="mais-procuradas"><h3>As cinco atividades mais procuradas</h3><p>Posição pela procura na edição anterior, da quinta para a primeira.</p><olreversed><li>Dados abertos e cidades inteligentes</li><li>Segurança em aplicações web: dez erros comuns</li><li>Maratona de programação</li><li>Minicurso: primeiros passos com redes neurais</li><li>Minicurso: Git e GitHub do zero</li></ol></section>
Com reversed, o navegador numera 5, 4, 3, 2, 1 — sem uma linha de CSS e sem digitar número nenhum. Se depois você inserir um item no meio, a contagem se reajusta sozinha.
programacao.html (trecho: novo <section> dentro do <main>)
HTML
<sectionid="abertura"><h3>Abertura da edição anterior</h3><videocontrolswidth="640"height="360"poster="img/capa-abertura.jpg"preload="metadata"playsinline><sourcesrc="midia/abertura.mp4"type="video/mp4"><tracksrc="legendas/abertura-pt.vtt"kind="captions"srclang="pt"label="Português"default>
Seu navegador não suporta vídeo HTML5.
<ahref="midia/abertura.mp4">Baixe o vídeo</a>.
</video></section>
Crie o arquivo de legendas com pelo menos quatro blocos, seguindo o exemplo da seção 4.3. Se o vídeo for seu, transcreva as falas reais; se for um vídeo de amostra sem fala, descreva o que aparece e os sons.
legendas/abertura-pt.vtt
Texto
WEBVTT
00:00:00.000 --> 00:00:03.000
[música de abertura]
00:00:03.500 --> 00:00:07.000
Boa noite e sejam bem-vindos à Semana Acadêmica de Sistemas de Informação.
00:00:07.500 --> 00:00:11.000
Serão três noites de palestras, minicursos e maratona de programação.
00:00:11.500 --> 00:00:14.000
[aplausos]
programacao.html (trecho: novo <section> dentro do <main>)
HTML
<sectionid="depoimento"><h3>Depoimento de uma participante</h3><figure><audiocontrolspreload="metadata"><sourcesrc="midia/depoimento.mp3"type="audio/mpeg">
Seu navegador não suporta áudio HTML5.
<ahref="midia/depoimento.mp3">Baixe o áudio</a>.
</audio><figcaption>Maria Eduarda, estudante do 4º semestre, sobre o minicurso de Git.</figcaption></figure></section>
Este é o caso clássico de <figure>: um conteúdo autocontido (o áudio) com uma legenda visível que o identifica.
programacao.html (trecho: último <section> do <main>, antes do "Voltar ao topo")
HTML
<sectionid="onde"><h3>Onde acontece</h3><p>Auditório Central, em Sinop.</p><iframesrc="https://www.google.com/maps/embed?pb=CODIGO_GERADO_PELO_MAPS"title="Mapa: Auditório Central, em Sinop"width="600"height="450"loading="lazy"referrerpolicy="no-referrer-when-downgrade"></iframe></section>
Para obter o código: abra o Google Maps, pesquise o local que vai sediar o evento (por exemplo, o auditório da sua escola), clique em Compartilhar → Incorporar um mapa → Copiar HTML e substitua o src acima pelo que veio no código copiado. Mantenha o title — o código do Maps não o inclui.
Passo 8 — Fotos e elenco completo em palestrantes.html¶
A Aula 02 deixou três <article> nesta página, cada um com <h3>, dois parágrafos e um <dl> de atividades e contato. Hoje você faz duas coisas: acrescenta a foto dentro de cada <article> existente e completa o elenco com os três convidados que faltavam.
Em cada um dos três artigos que já existem, a <img> entra logo depois do <h3>:
palestrantes.html (trecho: o artigo de Ana Lúcia Ferreira, depois da mudança)
HTML
<articleid="ana-lucia-ferreira"><h3>Ana Lúcia Ferreira</h3><imgsrc="img/palestrante-01.jpg"alt="Ana Lúcia Ferreira em frente a um quadro com um diagrama de rede neural"width="400"height="400"decoding="async"><p><strong>Professora e pesquisadora</strong> na área de inteligência artificial.</p>
Faça o mesmo em Bruno Takahashi (img/palestrante-02.jpg) e Carla Mendes (img/palestrante-03.jpg), com alt que descreva aquela pessoa naquela foto. Esses três ficam visíveis sem rolar a página, então não levam loading="lazy".
Depois do artigo de Carla Mendes, e antes do parágrafo "Voltar ao topo", acrescente os três novos — agora com loading="lazy", porque estão abaixo da dobra:
palestrantes.html (trecho: os três novos <article>)
HTML
<articleid="diego-nascimento"><h3>Diego Nascimento</h3><imgsrc="img/palestrante-04.jpg"alt="Diego Nascimento sentado em frente a dois monitores com um portal do governo aberto"width="400"height="400"loading="lazy"decoding="async"><p><strong>Desenvolvedor web</strong> na Prefeitura de Sinop, responsável pelos portais de serviços ao cidadão.</p><p>Trabalha com acessibilidade em portais públicos e conduz auditorias de conformidade com a WCAG.</p><dl><dt>Atividades</dt><dd><ahref="programacao.html#dia-1">Minicurso: Git e GitHub do zero</a> (dia 1)</dd><dd><ahref="programacao.html#dia-2">Minicurso: acessibilidade na prática</a> (dia 2)</dd><dt>Contato</dt><dd><ahref="https://www.linkedin.com/"target="_blank"rel="noopener noreferrer">Perfil no LinkedIn</a></dd></dl></article><articleid="eduarda-ribeiro"><h3>Eduarda Ribeiro</h3><imgsrc="img/palestrante-05.jpg"alt="Eduarda Ribeiro apresentando slides em um auditório"width="400"height="400"loading="lazy"decoding="async"><p><strong>Professora</strong> do curso de Sistemas de Informação.</p><p>Coordena a Semana Acadêmica e orienta projetos de extensão que levam estudantes do HTML ao deploy.</p><dl><dt>Atividades</dt><dd><ahref="programacao.html#dia-1">Abertura e palestra magna: o futuro do desenvolvimento web</a> (dia 1)</dd><dd><ahref="programacao.html#dia-3">Maratona de programação</a> (dia 3)</dd><dt>Contato</dt><dd><ahref="mailto:eduarda@exemplo.com">eduarda@exemplo.com</a></dd></dl></article><articleid="felipe-arruda"><h3>Felipe Arruda</h3><imgsrc="img/palestrante-06.jpg"alt="Felipe Arruda em um canavial, segurando um tablet com imagens de drone"width="400"height="400"loading="lazy"decoding="async"><p><strong>Engenheiro de visão computacional</strong> na cooperativa Coopercana.</p><p>Usa imagens de drone e modelos de aprendizado profundo para detectar pragas antes que elas se espalhem.</p><dl><dt>Atividades</dt><dd><ahref="programacao.html#dia-2">Visão computacional no controle de pragas</a> (dia 2)</dd><dt>Contato</dt><dd><ahref="https://github.com/"target="_blank"rel="noopener noreferrer">Perfil no GitHub</a></dd></dl></article>
Cada alt descreve a pessoa — o que a foto mostra. O parágrafo abaixo traz o minicurrículo — a informação que o texto acrescenta. Nada se repete.
💡 Dica
Os seis nomes desta página são os mesmos que vão alimentar o js/dados.js na Aula 12 e a página gerada por JavaScript na Aula 13. Manter o elenco estável agora poupa retrabalho depois — e é exatamente o que acontece em um projeto real.
<fieldset><legend>Comprovante e pagamento</legend><p><labelfor="comprovante">Comprovante de matrícula (PDF ou imagem, até 5 MB) — obrigatório</label><inputtype="file"id="comprovante"name="comprovante"accept=".pdf,image/*"required></p><p><labelfor="certificados">Certificados de edições anteriores (opcional, vários arquivos)</label><inputtype="file"id="certificados"name="certificados"accept=".pdf"multiple></p><p><labelfor="cpf">CPF — obrigatório</label><inputtype="text"id="cpf"name="cpf"inputmode="numeric"requiredpattern="[0-9]{3}\.[0-9]{3}\.[0-9]{3}-[0-9]{2}"placeholder="000.000.000-00"title="Formato: 000.000.000-00"autocomplete="off"></p><p><labelfor="lote">Lote</label><selectid="lote"name="lote"><optionvalue="30">1º lote — R$ 30</option><optionvalue="45">2º lote — R$ 45</option></select></p><p><labelfor="qtd">Quantidade de ingressos</label><inputtype="number"id="qtd"name="qtd"min="1"max="5"value="1"></p><p>Total: <outputid="total"for="lote qtd">R$ 30</output></p><p><labelfor="vagas">Vagas ocupadas no minicurso de Git</label><meterid="vagas"value="38"min="0"max="40"low="20"high="35"optimum="0">38 de 40</meter></p></fieldset>
E, junto ao botão de envio que já existe, um segundo botão que salva sem validar:
O <output> fica parado em "R$ 30" por enquanto: sem JavaScript ele não calcula nada. Na Aula 13, com eventos, ele passa a mudar sozinho quando o lote ou a quantidade mudam.
Valide as cinco páginas em validator.w3.org (aba Validate by File Upload). Meta: zero erros.
Abra index.html no Live Server, DevTools → Network, marque Disable cache, recarregue e anote o peso total no rodapé da aba (transferred). Repita em programacao.html.
Ordene por Size. Otimize no Squoosh os três maiores arquivos que forem imagens; substitua-os na pasta img/.
Recarregue e anote o novo peso. Registre os dois valores em um comentário HTML no topo do <body> de cada uma das duas páginas:
HTML
<!-- Peso da página antes da otimização: 7,8 MB · depois: 1,1 MB (redução de 86%) -->
Menu: os cinco links levam às cinco páginas, todas existentes — nenhum 404. Em cada página, o item correspondente tem aria-current="page".
index.html: na aba Network, filtrando por Img, o banner baixado é .avif (ou .webp, conforme o navegador). Estreitando a janela para 400 px e recarregando com Disable cache, o arquivo escolhido pelo srcset muda.
programacao.html: a tabela dos três dias continua lá, com <caption>, três <tbody> e <tfoot>; a lista de trilhas mostra as atividades recuadas sob cada assunto; o ranking numera de 5 para 1.
Vídeo: o poster aparece antes do play; ao dar play, o botão CC dos controles liga a legenda e o texto aparece nos tempos definidos.
Áudio: o player aparece com play, tempo e volume, e a legenda da <figure> fica abaixo dele.
Mapa: o quadro mostra o campus; se estiver em branco, confira se o src é a URL de embed (contém /maps/embed?), não a URL normal do Maps.
palestrantes.html: seis artigos, cada um com foto e <dl>; na aba Network, as três últimas fotos só são baixadas quando você rola até elas.
inscricao.html: o seletor de arquivos filtra PDF e imagens; o botão "Enviar inscrição" bloqueia se o CPF estiver vazio ou fora do formato, e o "Salvar rascunho" envia mesmo assim.
Validador: zero erros nas cinco páginas.
Network: o peso de cada página ficou abaixo de 2 MB (com o vídeo em preload="metadata", ele não conta até o play).
Resultado esperado: o mesmo site de cinco páginas da Aula 02, agora com identidade visual (logo), navegação marcada, banner responsivo, listas de dois níveis, mídia acessível e o formulário completo — sem uma linha de CSS e sem ter descartado nada do que já estava pronto.
A1. Escreva o alt adequado para: (a) o logotipo da sua universidade ou escola; (b) uma linha decorativa entre duas seções; (c) um gráfico de pizza mostrando 60% de aprovados e 40% de reprovados; (d) uma foto do campus dentro de um link para a página de contato.
A2. Diferencie alt de figcaption: para quem cada um existe, e por que não devem ter o mesmo texto.
A3. Qual formato de imagem você usaria para: (a) foto de paisagem; (b) logotipo vetorial; (c) captura de tela com texto pequeno; (d) ícone que precisa escalar de 16 px a 512 px sem perder qualidade?
A4. O que faz loading="lazy" em uma <img>? Por que ele melhora a performance — e por que a imagem do topo da página não deve tê-lo?
A5. Por que um <button> sem type dentro de um formulário pode causar problemas? Cite os três valores possíveis de type e quando usar cada um.
A6. Para que serve o elemento <track> dentro de <video>? Qual a diferença entre kind="captions" e kind="subtitles"?
A7. Quando usar <ul>, <ol> e <dl>? Dê um exemplo real de cada, retirado do site do evento.
A8. Escreva uma lista de definição com três termos técnicos desta trilha (por exemplo: alt, srcset, WebVTT) e suas definições.
A9. Como se representa uma lista aninhada corretamente? Escreva o HTML de dois níveis e explique qual erro o validador do W3C acusa quando o aninhamento está errado.
A10. Por que um menu de navegação deve ser marcado como lista dentro de <nav>? Cite dois ganhos concretos.
A11. Um campo <input type="file"> está em um <form method="post"> sem enctype. O que chega ao servidor quando o usuário envia uma foto? Qual atributo corrige isso?
A12. Dado <meter value="38" min="0" max="40" low="20" high="35" optimum="0">, o que os atributos low, high e optimum comunicam ao navegador? Quando <progress> seria a escolha errada para esse mesmo dado?
B1. Crie uma galeria de seis imagens usando <figure> e <figcaption>, com alt descritivo em todas, loading="lazy" nas três últimas e width/height declarados. Otimize as imagens no Squoosh e documente, em comentários HTML acima de cada <figure>, o peso original e o peso após a otimização.
Resultado esperado: seis figuras com legenda visível; o validador não acusa erros; na aba Network, ordenando por Size, nenhuma imagem passa de 150 KB; os comentários mostram redução em todas as seis.
Dica
Redimensione antes de comprimir: uma foto exibida em 400 px de largura sai do Squoosh com 800 px (para telas 2×), não com 4000. Só depois escolha WebP a 75 de qualidade. Para saber o peso original, veja a coluna Size no Network antes de trocar o arquivo, ou clique com o botão direito no arquivo e escolha Propriedades.
B2. Monte uma página de contato (para o seu projeto autoral) com: formulário (nome, e-mail, assunto via <select>, mensagem em <textarea>), telefone e e-mail clicáveis (tel: e mailto:), mapa incorporado por <iframe> com title, e horário de atendimento em uma tabela com caption, thead e th scope.
Resultado esperado: ao clicar no telefone em um celular, o discador abre; ao clicar no e-mail, o cliente de e-mail abre com o destinatário preenchido; o mapa mostra o local; a tabela tem cabeçalho de coluna; zero erros no validador.
Dica
<a href="tel:+5566999990000">(66) 99999-0000</a> — o número no href vai sem espaços, parênteses ou traços, com o código do país. Para mailto:, você pode pré-preencher o assunto: mailto:contato@evento.br?subject=Inscrição. Se o mapa ficar em branco, confira se copiou o src da opção "Incorporar um mapa", e não a URL da barra de endereço.
B3. Implemente uma página com um <picture> servindo AVIF, WebP e JPG, e uma <img> com srcset/sizes em três larguras (400, 800 e 1600 px). Na aba Network do DevTools, com Disable cache ligado, teste em três larguras de janela (400, 900 e 1400 px) e registre em uma tabela qual arquivo foi baixado em cada caso e por quê.
Resultado esperado: o <picture> baixa o .avif (ou .webp, dependendo do navegador); a <img> com srcset baixa arquivos diferentes conforme a largura; a tabela explica cada escolha com base em sizes e na densidade da tela.
Dica
Use o modo dispositivo do DevTools para fixar larguras exatas. Se em todas as larguras o navegador baixar sempre o mesmo arquivo, confira se o sizes está presente — sem ele, o navegador assume 100vw. Se ele baixar sempre o maior, você provavelmente está em uma tela com densidade 2× (o DevTools mostra o DPR no modo dispositivo).
B4. Monte uma página de cardápio usando <dl> para os pratos (termo = nome; definição = descrição e preço), <ul> para os acompanhamentos e <ol> para o modo de preparo de um dos pratos. Inclua uma foto por prato em <figure> com <figcaption>.
Resultado esperado: cada prato aparece como termo com descrição recuada; o modo de preparo está numerado; os acompanhamentos, com marcadores; cada foto tem alt que descreve o prato e legenda com o nome; zero erros no validador.
Dica
Um prato pode ter dois <dd>: um para a descrição, outro para o preço. Para colocar a <figure> junto ao prato, ela pode ficar dentro do <dd> — <dd> aceita conteúdo de fluxo, inclusive figuras.
B5. Amplie o formulário de inscrição da Aula 03 com um <fieldset> "Comprovante e pagamento" contendo: upload do comprovante de matrícula (accept para PDF e imagens), upload opcional de vários certificados (multiple), CPF com inputmode="numeric" e pattern, <select> de lote, <input type="number"> de quantidade e um <output> com o total. Acrescente um botão "Salvar rascunho" com formaction diferente e formnovalidate.
Resultado esperado: o <form> tem enctype="multipart/form-data"; o seletor de arquivos filtra os tipos permitidos; o botão "Enviar" exige os campos obrigatórios e o "Salvar rascunho" envia sem validar; zero erros no validador.
Dica
Teste o formnovalidate deixando um campo required vazio e clicando nos dois botões: só o "Enviar" deve bloquear. Para o <output>, o valor fica fixo por enquanto — ele só passará a calcular sozinho quando você aprender eventos, na Aula 13.
C1. Landing page multimídia. Crie a landing page de um produto ou evento contendo: vídeo de fundo (muted, loop, autoplay, com poster), galeria responsiva com <picture> em três formatos, player de áudio com depoimento, formulário de captação de contatos (nome, e-mail, telefone, interesse em <select>) e mapa em <iframe>. Sem CSS ainda. Otimize todas as mídias e apresente uma tabela comparando o peso de cada arquivo antes e depois, com o percentual de redução total da página.
Dica
O vídeo de fundo só toca sozinho se tiver muted — sem isso, o navegador bloqueia o autoplay. Para o percentual de redução, some os pesos de todos os arquivos antes e depois: (antes − depois) ÷ antes × 100. A aba Network dá os dois totais se você trocar os arquivos e recarregar com Disable cache.
Alguém entregou a galeria abaixo dizendo que "abre normal no navegador". Abre — mas o validador do W3C acusa erros, o leitor de tela lê nomes de arquivo e o vídeo nunca mostra legenda. Há oito problemas no trecho. Encontre todos sem rodar o validador primeiro; depois use o validador para conferir quantos você achou sozinho.
HTML
<section><h2>Galeria</h2><ul><li><imgsrc="img/foto1.jpg"alt="imagem"></li><ul><li><imgsrc="img/foto2.jpg"></li></ul><li><figure><imgsrc="img/foto3.jpg"alt="foto3.jpg"loading="lazy"></figure></li></ul><videocontrols><sourcesrc="midia/abertura.mp4"><tracksrc="legendas/abertura.vtt"kind="captions"></video><iframesrc="https://www.youtube.com/watch?v=abc123"width="560"height="315"></iframe><imgsrc="img/logo.png"alt="logo da empresa"title="logo"></section>
Critérios de pronto
Uma lista numerada com os oito problemas, cada um com: a linha, o que está errado, por que importa (validação, acessibilidade ou funcionamento) e a correção.
O trecho reescrito, validando com zero erros no W3C.
Pelo menos três dos problemas são de acessibilidade (não apenas de validação).
Um parágrafo explicando por que "abre normal no navegador" não é critério de qualidade.
Pistas
Releia a seção 2.3 sobre o que pode ser filho direto de <ul>.
Releia a tabela de alt da seção 3.2: dois alt estão presentes mas errados, e um está ausente.
Um <source> sem type obriga o navegador a baixar o arquivo para descobrir se consegue tocá-lo; um <track> sem srclang e label não aparece no menu de legendas.
A URL do YouTube que funciona em <iframe> contém /embed/, e todo <iframe> precisa de um atributo que o leitor de tela anuncia.
Uma página de galeria com doze fotos de celular pesa em média 40 MB. Quanto tempo ela leva para abrir no 4G lento? Você vai medir. Monte uma página com doze fotos originais (sem tratamento), meça o peso e o tempo de carregamento com throttling Slow 4G e, depois, construa a mesma galeria com um pipeline completo de imagens responsivas — tamanhos, formatos e prioridade de carregamento — até reduzir o peso em pelo menos 90% sem que a qualidade visível caia.
Critérios de pronto
Duas versões da página (galeria-antes.html e galeria-depois.html) com as mesmas doze fotos.
Cada <img> da versão final usa srcset com três larguras, sizes coerente com o layout, e está dentro de <picture> com AVIF, WebP e JPG.
As duas ou três primeiras fotos têm fetchpriority="high"; as demais, loading="lazy".
Uma tabela em Markdown ou HTML com, para cada versão: peso total transferido, número de requisições e tempo até o evento Load (visível no rodapé do Network), medidos com Slow 4G e cache desabilitado.
Redução de peso igual ou superior a 90%.
Pistas
No Squoosh, a aba Resize reduz dimensões; a aba Compress troca o formato. Faça as duas coisas: a maior economia vem de reduzir a dimensão.
Em vez de tratar doze fotos uma a uma, procure o modo de linha de comando do Squoosh (@squoosh/cli) ou a ferramenta ImageMagick (magick foto.jpg -resize 800x foto-800.jpg) — vale a pena aprender agora.
sizes deve refletir o espaço real: se as fotos ficarão em três colunas em telas largas, cada uma ocupa cerca de 33vw.
Meça com Disable cache marcado e sempre na mesma largura de janela; senão os números não são comparáveis.
Para ir além: rode o Lighthouse (aba do DevTools) nas duas versões e compare a nota de Performance e a métrica Largest Contentful Paint.
A WCAG exige, para vídeo pré-gravado: legendas (critério 1.2.2), audiodescrição ou alternativa em texto (1.2.3) e, para áudio, transcrição (1.2.1). Quase nenhum site de evento cumpre isso — o seu vai cumprir. Pegue um vídeo de dois a três minutos com fala (pode ser uma palestra curta gravada por você ou um vídeo livre) e um áudio de um minuto, e construa uma página em que toda a informação da mídia esteja disponível para alguém que não ouve e para alguém que não vê.
Critérios de pronto
O <video> tem três <track>: captions em português com sons relevantes entre colchetes, chapters com pelo menos quatro capítulos, e descriptions descrevendo o que aparece na tela nos momentos em que a fala não explica.
Uma transcrição completa do vídeo em texto na própria página, dentro de <details> para não ocupar espaço, sincronizada com os capítulos por <h3>.
O <audio> tem, logo abaixo, a transcrição completa em texto.
Ao ligar as legendas nos controles do navegador, os três tipos de trilha aparecem no menu, com os rótulos corretos.
Um relatório de uma página: quanto tempo levou transcrever cada minuto de mídia, que ferramentas ajudaram (transcrição automática do YouTube, Whisper, transcrição do Google Docs) e o que precisou ser corrigido à mão.
Pistas
Leia a especificação de WebVTT na MDN ("WebVTT API") — capítulos e descrições usam o mesmo formato de tempo das legendas, mudando só o kind do <track>.
O YouTube gera legendas automáticas para vídeos enviados; é possível baixá-las em formato .vtt e corrigir os erros — muito mais rápido que transcrever do zero.
Descrições (kind="descriptions") são lidas em voz alta pelo leitor de tela nos intervalos sem fala; escreva-as curtas o bastante para caber no silêncio disponível.
Para ver a transcrição sincronizada por capítulos, cada <h3> da transcrição deve corresponder a um bloco do arquivo de capítulos — mesmo texto, mesma ordem.
Para ir além: publique a página e teste com o leitor de tela NVDA (Windows, gratuito) ou o VoiceOver (macOS): navegue só pelo teclado até o vídeo, dê play e ouça as descrições.
Parte 1 — Leitura (20 min). SILVA, M. S. Criando sites com HTML, capítulos sobre listas e imagens. TERUEL, E. C. HTML5 — Guia Prático, capítulo sobre APIs de mídia. Na MDN em pt-BR, leia "Imagens responsivas" e "Conteúdo de vídeo e áudio" (links em Para aprofundar).
Parte 2 — Produção (30 min). No seu projeto autoral, produza os exercícios B1 (galeria com a tabela de peso antes/depois) e B5 (formulário ampliado com upload, inputmode, <output> e botão de rascunho). Produza também as páginas de programação e de galeria do seu projeto, com todas as mídias e os três tipos de lista aplicados, validadas no W3C.
Critério de pronto: as páginas validam com zero erros; toda <img> tem alt, width e height; o vídeo tem <track> com arquivo .vtt funcionando; a página de galeria pesa menos de 2 MB na aba Network com cache desabilitado; o comentário HTML com o peso antes/depois está no topo do <body>.
Guarde no seu repositório: commit + push (ou a pasta do projeto, se você ainda não estiver usando Git).
Parte 3 — Registro (10 min). Em texto próprio (ou no fórum da turma, se houver), sob o título "Descrever imagens", escreva o alt de três imagens do seu próprio projeto — uma informativa, uma decorativa e uma dentro de link — e justifique cada escolha em duas linhas. Se puder, compare com o alt que outras pessoas escreveriam: ele entrega a mesma informação que a imagem entrega para quem vê?
Ao fim desta aula, o repositório do seu projeto autoral deve ter:
[ ] Página de programação (ou equivalente do seu domínio) com os horários em <time>, uma <ul> aninhada de dois níveis e uma <ol> usando pelo menos um dos atributos start, reversed ou type.
[ ] Menu de navegação como <ul> dentro de <nav aria-label>, com aria-current="page" na página atual.
[ ] Uma <dl> em uso (glossário, ficha de pessoa, cardápio — o que fizer sentido no seu domínio).
[ ] Página de galeria com seis <figure>/<figcaption>, alt descritivo, width/height e loading="lazy" nas imagens abaixo da dobra.
[ ] Banner em <picture> com AVIF, WebP e JPG, sem lazy.
[ ] Um <video> com controls, poster, preload="metadata" e <track kind="captions"> apontando para um .vtt válido.
[ ] Um <audio> com controls e fallback.
[ ] Um <iframe> com title e loading="lazy".
[ ] Formulário de inscrição com enctype, campo de upload com accept, inputmode no CPF e um <output>.
[ ] Zero erros no validador W3C em todas as páginas.
[ ] Comentário no topo do <body> com o peso da página antes e depois da otimização.
web.dev — Learn Images: https://web.dev/learn/images — curso gratuito sobre formatos, compressão e imagens responsivas.
Squoosh: https://squoosh.app — otimizador de imagens no navegador, usado nesta aula.
W3C — WCAG 2.1, critérios 1.1.1 (conteúdo não textual), 1.2.2 (legendas) e 1.3.1 (informação e relações): https://www.w3.org/Translations/WCAG21-ptbr/ — tradução oficial em português.
SILVA, Maurício Samy. Criando sites com HTML: sites de alta qualidade com HTML e CSS. Novatec, 2008 — capítulos sobre listas e imagens.
TERUEL, Evandro C. HTML 5 — Guia Prático. Saraiva, 2014 — capítulo sobre APIs de mídia.
Na próxima aula você dá ao site o esqueleto de layout com <header>, <main>, <section>, <article> e <aside> — e escreve as primeiras linhas de CSS.
Estruturar uma página com os elementos de seccionamento do HTML5 (header, nav, main, section, article, aside, footer) e decidir quando div é a escolha certa.
Aplicar o padrão de contêiner centralizado que sustenta praticamente todo site profissional.
Explicar o que é CSS, como o navegador o aplica e o que são seletor, declaração e regra.
Usar as três formas de aplicação de estilo e justificar por que o CSS externo é a única aceita nos trabalhos desta trilha.
Calcular o tamanho de uma caixa no modelo de caixa e explicar por que todo projeto começa com box-sizing: border-box.
Diferenciar os valores de display (block, inline, inline-block, none) e seu efeito no fluxo do documento.
Reconhecer o colapso de margens e prever o espaço real entre dois elementos.
[ ] As cinco páginas do site do evento (index.html, programacao.html, inscricao.html, palestrantes.html, contato.html) validando com zero erros no W3C.
[ ] VS Code com Live Server e DevTools abertos na aba Elements — hoje você usará muito o painel Styles e a aba Computed.
[ ] Revisar da Aula 02 a estrutura mínima de um documento (<!DOCTYPE html>, lang, charset, viewport).
Na Aula 04 você terminou o conteúdo das páginas do evento: listas, imagens otimizadas, vídeo com legenda e mapa incorporado. O site está completo em informação — e cru em aparência. Antes de estilizar, porém, a página precisa de um esqueleto correto: é sobre ele que o CSS vai trabalhar. Hoje você organiza esse esqueleto com os elementos de seccionamento, escreve a primeira folha de estilo e entende a regra que resolve a maior parte dos problemas de layout: o modelo de caixa. Esta aula fecha a Unidade 1.
Até a Aula 02 você usou <header>, <nav>, <main> e <footer> como "as partes da página". Hoje você entende o que cada um significa — e por que essa escolha muda o que o leitor de tela anuncia, o que o buscador indexa e como o CSS vai se apoiar na estrutura.
O <h1> continua no <header>, como na Aula 02: ele nomeia o site, é o mesmo nas cinco páginas, e o título de cada página é o <h2> do topo do <main>. Só na Aula 07, quando o site for redesenhado, o <h1> desce para dentro do <main>.
Elemento
Papel
Quantidade por página
<header>
Cabeçalho da página ou de uma seção (logo, título, menu)
Vários (um por contexto)
<nav>
Bloco de navegação principal
1 a 3, sempre rotulados com aria-label
<main>
Conteúdo único e central da página — o que muda de uma página para outra
Exatamente 1
<section>
Agrupamento temático com título
Vários
<article>
Conteúdo autocontido, que faz sentido isolado (notícia, cartão, comentário, produto)
Esses elementos são chamados de marcos (landmarks): o leitor de tela oferece um atalho para pular direto entre eles ("ir para o conteúdo principal", "ir para a navegação"). Uma página feita só de <div> não tem marcos — a pessoa cega precisa ouvir tudo, do topo, sempre.
A dúvida mais comum desta aula é "isso é uma section ou um article?". Três perguntas resolvem:
Faz sentido sozinho, fora desta página? Uma notícia, um cartão de palestrante, um comentário, um produto — se você conseguiria distribuí-lo por RSS ou compartilhar isoladamente, é <article>.
É um agrupamento temático com título, que só faz sentido dentro desta página? "Destaques", "Programação do primeiro dia", "Perguntas frequentes" — é <section>. Uma <section> sem título é sinal de que você queria uma <div>.
Não tem significado nenhum — existe só para agrupar por estilo? Um contêiner para centralizar, um invólucro para aplicar um fundo — é <div>.
💡 Dicadiv não é proibida — é a última opção. Precisa de um contêiner só para centralizar o conteúdo com max-width? div é a escolha certa. Precisa marcar o rodapé? Use footer. A pergunta a fazer é sempre: "existe um elemento que descreva o que isto é?" Se existe, use-o. Se não existe, div.
Um <article> pode ter seu próprio <header> e <footer> — e é comum:
HTML
<articleclass="cartao"><header><h3>Minicurso de Git e GitHub</h3><p><timedatetime="19:30">19h30</time> · Laboratório 2 · 40 vagas</p></header><p>Do primeiro commit ao pull request, em duas horas.</p><footer><ahref="inscricao.html">Inscrever-se</a></footer></article>
O <header> de dentro do <article> não é o cabeçalho da página — é o cabeçalho daquele artigo. O leitor de tela entende a diferença pelo contexto.
Uma página pode ter navegação principal, um menu de rodapé e a "trilha de migalhas" (breadcrumb). Todos são <nav>; o que os diferencia é o aria-label:
HTML
<navaria-label="Principal"><ul><li><ahref="index.html">Início</a></li><li><ahref="programacao.html">Programação</a></li></ul></nav><navaria-label="Você está em"><ol><li><ahref="index.html">Início</a></li><li><ahref="programacao.html">Programação</a></li><liaria-current="page">Primeiro dia</li></ol></nav><footer><navaria-label="Rodapé"><ul><li><ahref="contato.html">Contato</a></li><li><ahref="privacidade.html">Política de privacidade</a></li></ul></nav></footer>
Sem o rótulo, o leitor de tela anuncia três vezes "navegação" e a pessoa não sabe qual é qual.
Abra qualquer site profissional: o cabeçalho tem uma cor de fundo que vai de uma borda à outra da janela, mas o conteúdo (logo, menu) fica limitado a uma faixa central. Esse é o padrão de contêiner centralizado, e ele combina um elemento semântico de largura total com uma div interna que limita e centraliza:
index.html (trecho)
HTML
<headerid="topo"><divclass="container"><imgsrc="img/logo-sasi.svg"alt=""width="160"height="48"><h1>Semana Acadêmica de Sistemas de Informação</h1><navaria-label="Principal"><ul><li><ahref="index.html"aria-current="page">Início</a></li><li><ahref="programacao.html">Programação</a></li></ul></nav></div></header>
Leia a regra assim: "ocupe toda a largura disponível, mas nunca passe de 1100 px; as margens laterais automáticas dividem a sobra igualmente, centralizando; e sempre reserve 1 rem de respiro de cada lado". Em um monitor largo, o conteúdo para em 1100 px; em um celular, ocupa toda a largura menos o respiro. Esse número, 1100 px, é o do projeto e não muda mais — nas Aulas 06 e 07 ele vira a variável --largura-max. O elemento semântico (<header>) continua ocupando 100% — útil para a cor de fundo que sangra até as bordas.
Esse é o esqueleto de praticamente todo site que você já visitou. Guarde-o: você o usará em todas as páginas hoje e em todos os projetos da trilha.
🔬 Investigue
Abra index.html no Live Server, pressione F12, vá à aba Console e cole o comando abaixo. Ele desenha um contorno vermelho translúcido em todos os elementos da página — você enxerga a caixa de cada um e percebe na hora onde há div demais, section sem título ou conteúdo fora do <main>.
Depois, na aba Elements, clique com o botão direito no <body> e escolha Expand recursively: a árvore inteira se abre e você vê a hierarquia dos marcos. Recarregue a página para tirar os contornos.
🧠 Você sabia?
Antes do HTML5, todo site era feito de <div id="a05-header">, <div id="a05-nav">, <div id="a05-footer">. Os nomes dos elementos de seccionamento não foram inventados por um comitê: o grupo de trabalho analisou mais de um bilhão de páginas indexadas pelo Google e descobriu quais eram os id e class mais usados pelos desenvolvedores. header, footer, nav, content e sidebar estavam no topo — e viraram <header>, <footer>, <nav>, <main> e <aside>. O HTML5 padronizou o que a comunidade já fazia por convenção.
CSS (Cascading Style Sheets, folhas de estilo em cascata) é a linguagem que descreve como o conteúdo estruturado pelo HTML deve ser apresentado. O HTML responde "o que é isso?" (um título, uma lista, um formulário); o CSS responde "com que aparência?" (azul, 2 rem, centralizado, com sombra).
A separação é deliberada e vale ouro: o mesmo HTML pode ganhar dez aparências diferentes trocando só o CSS, e o mesmo CSS serve a cem páginas. É por isso que o Marco 1 fecha sem CSS e o Marco 2 estiliza o mesmo site: a estrutura e a apresentação são trabalhos diferentes.
🔎 Por baixo do capô
Quando a página carrega, o navegador faz três coisas em sequência: (1) lê o HTML e monta a árvore DOM (a estrutura de elementos que você vê na aba Elements); (2) lê o CSS e monta a CSSOM, a árvore de regras; (3) combina as duas na árvore de renderização, calculando para cada elemento quais regras se aplicam e qual é o valor final de cada propriedade — é isso que a aba Computed do DevTools mostra. Só então ele calcula posições e tamanhos (layout) e pinta os pixels (paint). Por isso um CSS externo referenciado no <head>bloqueia a renderização até ser baixado: o navegador se recusa a mostrar a página com a aparência errada e depois "piscar" para a certa.
Você vai ver esse fluxo na prática na Aula 09, quando medir o custo de animações. Por ora, o que importa: o CSS não "pinta por cima" do HTML — ele participa da construção da página desde o início.
🧠 Você sabia?
Nos primeiros anos da Web, quem decidia a aparência de uma página era o navegador, não o autor. Um <h1> era grande e em negrito porque o Mosaic decidiu assim. Quando o CSS foi proposto — por Håkon Wium Lie, que trabalhava com Tim Berners-Lee no CERN — a ideia central não era dar poder total ao autor, e sim criar uma cascata: o estilo final resulta da combinação entre o que o navegador sugere, o que o usuário prefere e o que o autor define. Essa ideia continua viva: se um usuário com baixa visão aumenta o tamanho da fonte no navegador, um site feito com rem respeita a escolha. Um site feito com px a ignora. O "C" de CSS existe para proteger o usuário.
Problemas: mistura conteúdo com apresentação, não é reaproveitável (cada parágrafo precisa repetir o estilo), tem prioridade altíssima (difícil de sobrescrever — você verá o porquê na Aula 06) e polui o HTML. Uso legítimo: quando o valor é gerado dinamicamente por JavaScript — uma barra de progresso cuja largura muda a cada segundo, por exemplo.
<head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Semana Acadêmica de Sistemas de Informação</title><linkrel="stylesheet"href="css/estilo.css"></head>
css/estilo.css
CSS
body{font-family:sans-serif;}h1{color:#0b3d5c;}
Vantagens:
Um arquivo serve o site inteiro. Mudou a cor do título? Mudou nas cinco páginas.
Cache. O navegador baixa estilo.css uma vez e reaproveita nas outras páginas — a segunda página abre mais rápido.
Separação de responsabilidades. HTML descreve; CSS apresenta; na Unidade 3, JavaScript comporta.
Trabalho em equipe. Uma pessoa mexe no conteúdo enquanto outra mexe no estilo, sem conflito.
Todos os trabalhos desta trilha usam CSS externo. O href segue as mesmas regras de caminho relativo dos links e imagens (Aula 02): css/estilo.css está na pasta css, ao lado do HTML.
⚠️ Atenção
Uma página pode ter vários <link rel="stylesheet">. Eles são aplicados na ordem em que aparecem: quando duas regras iguais conflitam, a do arquivo que veio por último vence. Guarde isso — é a terceira etapa da cascata, que você formaliza na Aula 06.
Todo elemento HTML é uma caixa retangular. Não importa se é um título, uma imagem, um link ou um <span> no meio de uma frase: para o navegador, é um retângulo com quatro camadas. Entender isso resolve 80% dos problemas de layout que você vai encontrar.
Você pediu 300 px e recebeu 350. Em um layout de duas colunas de 50% cada, basta um padding para a segunda coluna não caber e cair para a linha de baixo. Isso quebra layouts em porcentagem constantemente.
Com border-box, width passa a incluir padding e borda: a caixa ocupa exatamente 300 px, e o conteúdo fica com o que sobra (250 px). É como você pensa intuitivamente — "quero uma caixa de 300" — e é assim que todo projeto profissional trabalha. Por isso todo CSS desta trilha começa com este reset:
css/estilo.css (topo do arquivo)
CSS
/* 1. Reset e box-sizing */*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
Leia: "todo elemento (*), inclusive os pseudoelementos ::before e ::after (Aula 06), usa border-box e começa sem margem nem padding". O zerar de margens tira os espaçamentos padrão do navegador (os <h1> e <p> vêm com margens que variam entre navegadores) para que você decida cada espaço.
🔬 Investigue
No DevTools, aba Elements, selecione qualquer elemento e olhe o painel Styles: role até o fim e você verá o diagrama do box model daquele elemento, com os valores de margin, border, padding e content. Passe o mouse sobre cada camada — o navegador destaca a área correspondente na página em cores diferentes (laranja para margem, verde para padding, azul para conteúdo). Agora, na aba Computed, procure box-sizing: aplique o reset acima e veja o valor mudar de content-box para border-box — e a largura ocupada cair de 350 para 300.
margin e padding aceitam de um a quatro valores, sempre no sentido horário a partir do topo:
CSS
margin:10px;/* todos os lados */margin:10px20px;/* vertical | horizontal */margin:10px20px30px;/* topo | horizontal | base */margin:10px20px30px40px;/* topo | direita | base | esquerda (sentido horário) */margin:0auto;/* centraliza horizontalmente um bloco COM width definida */padding-top:10px;/* um lado só */margin-inline:auto;/* esquerda + direita (equivale a margin-left + margin-right) */margin-block:2rem;/* topo + base */
margin: 0 auto é o truque clássico de centralização: as margens laterais automáticas dividem igualmente o espaço que sobra. Só funciona quando o elemento é de bloco e tem largura menor que o pai — se ocupar 100%, não sobra nada para dividir. A versão moderna, margin-inline: auto, foi a que você usou no .container.
Margens verticais adjacentes entre elementos irmãos não se somam: prevalece a maior.
CSS
.a{margin-bottom:30px;}.b{margin-top:20px;}/* O espaço entre .a e .b é 30px, não 50px */
Isso é comportamento especificado, não bug — existe desde os primeiros dias do CSS para que parágrafos com margin: 1em 0 fiquem espaçados por 1em, e não por 2em. Três regras para não se surpreender:
Só acontece com margens verticais (topo e base). Margens horizontais sempre se somam.
Acontece também entre pai e primeiro/último filho: a margin-top do primeiro <p> dentro de uma <section> sem padding nem borda "vaza" para fora da seção.
Não acontece em Flexbox nem em Grid (Aula 07) — o que é um dos motivos de esses modelos serem mais previsíveis.
border:2pxsolid#333;/* largura | estilo | cor */border-bottom:3pxdashedred;/* um lado só */border-radius:8px;/* cantos arredondados */border-radius:50%;/* círculo perfeito, se a caixa for quadrada */
Estilos disponíveis: solid, dashed, dotted, double, none. A borda só aparece se os três valores estiverem presentes (uma borda sem estilo é invisível — erro frequente).
.foto-palestrante{width:100%;/* ocupa toda a largura do pai */max-width:400px;/* mas nunca passa de 400 px */height:auto;/* mantém a proporção */}.resumo{max-width:65ch;/* no máximo 65 caracteres por linha: legibilidade */min-height:120px;/* nunca menor que isso, mesmo com pouco texto */}.cartao{overflow:hidden;/* corta o que passar da caixa (auto: barra de rolagem; visible: vaza) */}
max-width é mais útil que width na maior parte dos casos: define um teto e deixa o elemento encolher em telas pequenas. Você usará isso o tempo todo na Aula 08.
O HTML renderiza os elementos em fluxo normal: de cima para baixo, da esquerda para a direita. A propriedade display define como cada caixa participa desse fluxo.
CSS
display:block;/* ocupa toda a largura disponível, aceita width/height. Padrão de div, p, h1, section, ul */display:inline;/* ocupa só o necessário, na linha do texto. IGNORA width/height e margens verticais. Padrão de span, a, strong, em */display:inline-block;/* fica na linha do texto, MAS aceita width/height e margens verticais */display:none;/* remove do fluxo — não ocupa espaço, como se não existisse */display:flex;/* Aula 07 */display:grid;/* Aula 07 */
Valor
Quebra linha?
Aceita width/height?
Exemplos padrão
block
Sim, antes e depois
Sim
div, p, h1, ul, section
inline
Não
Não
span, a, strong, em, code
inline-block
Não
Sim
img, button, input, select
none
Some
—
<template>, elementos com hidden
O caso clássico: você quer um <a> que pareça um botão, com 200 px de largura e padding vertical. Como <a> é inline, width e margin-top são ignorados. Solução: display: inline-block — o link continua na linha (pode ficar ao lado de outro) e passa a aceitar dimensões.
5.1 Três formas de "esconder", três resultados diferentes¶
Propriedade
Ocupa espaço
Clicável
Lido por leitor de tela
display: none
Não
Não
Não
visibility: hidden
Sim
Não
Não
opacity: 0
Sim
Sim
Sim
Escolha pela intenção: display: none para remover de verdade (um menu fechado); visibility: hidden para reservar o lugar (um placeholder que vai aparecer); opacity: 0só para animações de aparecer/desaparecer (Aula 09) — porque um botão com opacity: 0 continua clicável e continua sendo lido, e isso é um bug de acessibilidade quando não é intencional. O atributo HTML hidden equivale a display: none.
6. Um gostinho do que vem: herança, estados e variáveis¶
Para o Laboratório de hoje você precisa de três ideias que a Aula 06 aprofunda. Aqui vai o mínimo para usá-las.
body{font-family:"Segoe UI",Roboto,Arial,sans-serif;color:#333;line-height:1.6;}/* Todo o site herda a fonte, a cor e a altura de linha — escreva uma vez, vale para tudo */
Herdadas (passam de pai para filho)
Não herdadas (cada elemento define a sua)
color, font-family, font-size, font-weight
margin, padding, border
line-height, text-align, letter-spacing
width, height, background
visibility, cursor, list-style
display, position, overflow
A lógica: propriedades de texto são herdadas (faz sentido que o parágrafo tenha a fonte do body); propriedades de caixa não são (não faz sentido que todo filho de uma caixa com borda ganhe borda).
Um seletor pode mirar um estado do elemento — mouse por cima, foco pelo teclado — com as pseudoclasses:
CSS
.botao:hover{background-color:#1a7fb5;/* mouse por cima */}.botao:focus-visible{outline:3pxsolid#f0a500;/* foco visível pelo teclado */outline-offset:2px;}
⚠️ Atenção
Nunca escreva outline: none sem colocar outro indicador no lugar. Quem navega pelo teclado (por deficiência motora, por preferência ou porque o mouse quebrou) depende do contorno de foco para saber onde está. Um site sem foco visível é inutilizável por teclado — não importa quão bonito ele pareça.
:root é o <html>; declarar ali torna a variável visível na página inteira. Mudou a cor da marca? Uma linha. Na Aula 06 você monta um sistema completo de variáveis; hoje basta usar duas ou três.
6.4 Por que id não é para estilo e !important não é solução¶
Você verá na Aula 06 que o navegador decide conflitos entre regras por especificidade: um #id vence qualquer quantidade de .classes, e !important vence tudo. Parece prático — até que você precisa sobrescrever e não consegue. Regra desta trilha desde hoje: estilize com classes (reutilizáveis e fáceis de sobrescrever), reserve id para âncoras e JavaScript, e não use !important — se precisou dele, há um problema de organização no CSS, e o remédio é reorganizar, não escalar a guerra.
💻 Mão na massa — Esqueleto semântico das cinco páginas e a primeira folha de estilo¶
Hoje as cinco páginas do site do evento — todas já existentes desde as Aulas 02 e 03 — ganham o esqueleto semântico correto, contato.html é reestruturada (o formulário do B5 da Aula 03 continua lá) e nasce o arquivo css/estilo.css, que crescerá a cada aula da Unidade 2.
Passo 1 — criar css/estilo.css com reset, contêiner e base¶
Crie a pasta css e, dentro dela, o arquivo:
css/estilo.css
CSS
/* ========================================================== Semana Acadêmica de Sistemas de Informação — folha de estilo Ordem: 1. reset · 2. variáveis · 3. base · 4. layout · 5. componentes ========================================================== *//* 1. Reset e box-sizing */*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}/* 2. Variáveis */:root{--cor-primaria:#0b3d5c;--cor-secundaria:#1a7fb5;--cor-texto:#333333;--cor-fundo:#f7f9fb;--espaco:16px;}/* 3. Base */body{font-family:"Segoe UI",Roboto,Arial,sans-serif;line-height:1.6;color:var(--cor-texto);background-color:var(--cor-fundo);}img,video{max-width:100%;height:auto;display:block;}/* 4. Layout */.container{width:100%;max-width:1100px;/* largura do projeto, fixada de vez */margin-inline:auto;padding-inline:1rem;}header{background-color:var(--cor-primaria);color:#ffffff;padding:var(--espaco)0;}headera{color:#ffffff;}main{padding:calc(var(--espaco)*2)0;}main>.container>section{margin-bottom:calc(var(--espaco)*2);}aside{padding:var(--espaco)0;background-color:#e8eef3;}footer{background-color:var(--cor-primaria);color:#ffffff;padding:var(--espaco)0;text-align:center;}footera{color:#ffffff;}/* 5. Componentes */.cartao{background-color:#ffffff;border:1pxsolid#d9e0e7;border-radius:8px;padding:var(--espaco);margin-bottom:var(--espaco);}.botao{display:inline-block;padding:12px24px;background-color:var(--cor-primaria);color:#ffffff;text-decoration:none;border-radius:8px;}.botao:hover{background-color:var(--cor-secundaria);}.botao:focus-visible{outline:3pxsolid#f0a500;outline-offset:2px;}
A regra img, video { max-width: 100%; height: auto; display: block; } merece atenção: sem ela, uma foto de 1600 px vaza para fora do contêiner em telas menores. display: block remove o pequeno espaço que aparece embaixo de imagens inline (elas ficam alinhadas à linha de base do texto, como se fossem letras).
Passo 2 — ligar a folha de estilo nas cinco páginas¶
Em cada página, dentro do <head>, depois do <title>:
index.html, programacao.html, inscricao.html, palestrantes.html, contato.html (no <head>)
HTML
<linkrel="stylesheet"href="css/estilo.css">
Salve e olhe o Live Server: o cabeçalho fica azul-escuro, a fonte muda, as margens padrão somem. Se nada mudou, veja a aba Network: o estilo.css deve aparecer com status 200. Um 404 significa caminho errado.
A página inicial já tem conteúdo desde a Aula 02 (as seções "Sobre o evento", "Como participar" e "Glossário") e o banner responsivo da Aula 04. Nada disso sai. O que muda é o esqueleto em volta: cada região ganha o seu <div class="container">, nasce uma seção de abertura (.hero), nascem os cartões de destaque, e o conteúdo tangencial vai para um <aside>.
Três observações sobre o que não mudou: o <h1> continua no <header> (ele nomeia o site, não a página), o rodapé continua com os três parágrafos da Aula 02 — agora com um <nav> a mais — e as seções que a Aula 02 escreveu continuam no <main>, só que dentro do contêiner.
A quinta página existe desde a Aula 02 (esqueleto) e recebeu o formulário de mensagem no exercício B5 da Aula 03 — com nome, e-mail, assunto, mensagem e a escolha da forma de resposta em rádios. Esse formulário não é reescrito: ele é transplantado para dentro do novo esqueleto, ganhando apenas o <div class="container"> em volta e a classe .botao no botão de envio. O resto da página junta o que você aprendeu nas Aulas 02 a 04: tabela, links tel:/mailto: e mapa em <iframe>.
contato.html
HTML
<!DOCTYPE html><htmllang="pt-BR"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metaname="description"content="Fale com a organização da Semana Acadêmica de Sistemas de Informação: telefone, e-mail, formulário e localização."><title>Contato — Semana Acadêmica de Sistemas de Informação</title><linkrel="stylesheet"href="css/estilo.css"></head><body><headerid="topo"><divclass="container"><imgsrc="img/logo-sasi.svg"alt=""width="160"height="48"><h1>Semana Acadêmica de Sistemas de Informação</h1><p>Três noites de outubro · Auditório Central</p><navaria-label="Principal"><ul><li><ahref="index.html">Início</a></li><li><ahref="programacao.html">Programação</a></li><li><ahref="inscricao.html">Inscrição</a></li><li><ahref="palestrantes.html">Palestrantes</a></li><li><ahref="contato.html"aria-current="page">Contato</a></li></ul></nav></div></header><main><divclass="container"><h2>Contato</h2><section><h3>Fale com a organização</h3><address><p>Telefone: <ahref="tel:+5566999990000">(66) 99999-0000</a></p><p>E-mail: <ahref="mailto:sasi@semanasi.com.br?subject=Contato%20pelo%20site">sasi@semanasi.com.br</a></p><p>Sala 12 do Auditório Central, em Sinop</p></address></section><section><h3>Horário de atendimento</h3><table><caption>Atendimento presencial na sala da organização</caption><thead><tr><thscope="col">Dia</th><thscope="col">Manhã</th><thscope="col">Noite</th></tr></thead><tbody><tr><thscope="row">Segunda a quinta</th><td>8h às 11h</td><td>19h às 21h</td></tr><tr><thscope="row">Sexta</th><td>8h às 11h</td><td>Fechado</td></tr></tbody></table></section><section><h3>Envie uma mensagem</h3><formaction="/contato"method="post"><p><labelfor="nome">Nome completo</label><inputtype="text"id="nome"name="nome"requiredminlength="5"autocomplete="name"></p><p><labelfor="email">E-mail</label><inputtype="email"id="email"name="email"requiredautocomplete="email"></p><p><labelfor="assunto">Assunto</label><selectid="assunto"name="assunto"required><optionvalue="">Selecione</option><optionvalue="inscricao">Dúvida sobre inscrição</option><optionvalue="certificado">Certificado</option><optionvalue="patrocinio">Patrocínio</option><optionvalue="outro">Outro</option></select></p><p><labelfor="mensagem">Mensagem</label><textareaid="mensagem"name="mensagem"rows="6"maxlength="600"required></textarea></p><fieldset><legend>Como prefere receber a resposta?</legend><p><inputtype="radio"id="resposta-email"name="resposta"value="email"checked><labelfor="resposta-email">Por e-mail</label></p><p><inputtype="radio"id="resposta-telefone"name="resposta"value="telefone"><labelfor="resposta-telefone">Por telefone</label></p></fieldset><p><labelfor="telefone">Telefone (opcional, necessário se escolher resposta por telefone)</label><inputtype="tel"id="telefone"name="telefone"autocomplete="tel"placeholder="(66) 99999-0000"></p><buttontype="submit"class="botao">Enviar mensagem</button></form></section><section><h3>Como chegar</h3><iframesrc="https://www.google.com/maps/embed?pb=CODIGO_GERADO_PELO_MAPS"title="Mapa: Auditório Central, em Sinop"width="600"height="450"loading="lazy"referrerpolicy="no-referrer-when-downgrade"></iframe></section></div></main><footer><divclass="container"><p>Semana Acadêmica de Sistemas de Informação</p></div></footer></body></html>
Repare no <address>: é o elemento semântico para informações de contato do autor ou da organização responsável pela página — exatamente este caso.
Passo 5 — aplicar o contêiner nas outras três páginas¶
Em programacao.html, inscricao.html e palestrantes.html, envolva o conteúdo de <header>, <main> e <footer> com <div class="container">, como no Passo 3. O conteúdo em si não muda. Esse é um trabalho mecânico de cinco minutos — use a busca e substituição do VS Code (Ctrl+H) para o cabeçalho e o rodapé, que são idênticos em todas.
Aproveite para conferir, em cada página: há um único <h1> (o do <header>)? O título da página é o <h2> no topo do <main>? Em palestrantes.html, cada convidado já é um <article> desde a Aula 02 — acrescente a ele a classe cartao, que a folha de estilo do Passo 1 acabou de definir, e o <h3> continua sendo o nome da pessoa. Em programacao.html, a tabela permanece como está; ela ganha estilo na Aula 06.
Cole o comando de contorno da seção 1 no console de cada página e procure: conteúdo fora de <main>, <section> sem título, <div> que poderia ser um elemento semântico.
Na aba Elements, selecione o .container do <main> e confira no painel Styles que width resolve para no máximo 1100 px e que as margens laterais são iguais.
Redimensione a janela até 400 px: o conteúdo deve manter 1 rem de respiro em cada lado, sem barra de rolagem horizontal.
A1. Quais são as três formas de aplicar CSS? Qual delas deve ser usada em projetos reais e por quê? Cite um caso legítimo para cada uma das outras duas.
A2. Cite quatro propriedades herdadas e quatro não herdadas. Explique a lógica que separa os dois grupos.
A3. Um elemento tem width: 200px, padding: 15px e border: 5px. Qual a largura total ocupada com box-sizing: content-box? E com border-box? Em qual dos dois o conteúdo fica com menos espaço?
A4. Explique o colapso de margens com um exemplo numérico. Diga em que situação ele não ocorre.
A5. Diferencie display: none, visibility: hidden e opacity: 0 quanto a espaço ocupado, clicabilidade e leitura por leitor de tela. Para cada um, dê um uso adequado.
A6. O que faz margin: 0 auto? Que condição o elemento precisa satisfazer para funcionar? Qual a alternativa moderna com a mesma função?
A7. Por que !important deve ser evitado? O que geralmente indica quando alguém sente necessidade de usá-lo?
A8. Cite três elementos de seccionamento do HTML5 e o papel de cada um. O que são "marcos" para um leitor de tela?
A9. Quantos <main> uma página pode ter? Por quê? E quantos <header>?
A10. Qual a diferença entre <section> e <article>? Dê um exemplo de cada, retirado do site do evento.
A11. Quando o uso de <div> é a escolha correta? Dê o exemplo usado nesta aula.
A12. Escreva o esqueleto semântico completo de uma página institucional (sem conteúdo, só os elementos), incluindo o padrão de contêiner centralizado em cada região.
A13. Um <a> recebe width: 200px e margin-top: 20px, mas nada muda na tela. Por quê? Como corrigir sem tirar o link da linha do texto?
A14. Preveja o que acontece com a regra abaixo e explique o motivo:
B1. Crie três cartões (article.cartao) com: fundo branco, border-radius, box-shadow, padding interno, título, parágrafo e um link com classe .botao. No :hover, o cartão deve mudar de sombra e o botão de cor. Use apenas variáveis CSS para todas as cores e espaçamentos.
Resultado esperado: três cartões idênticos em estrutura, com sombra suave; ao passar o mouse, a sombra fica mais forte e o botão muda de cor; nenhum valor de cor ou espaçamento aparece fora de :root; o foco do botão via Tab é visível.
Dica
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) é uma sombra discreta; no :hover, troque por 0 6px 16px rgba(0, 0, 0, 0.2). Declare as duas como variáveis (--sombra-cartao e --sombra-cartao-hover). O :hover do cartão é .cartao:hover; o do botão dentro do cartão em hover é .cartao:hover .botao.
B2. Crie um menu de navegação horizontal usando display: inline-block nos itens (Flexbox só na Aula 07), com espaçamento entre eles, :hover com mudança de cor de fundo, :focus-visible visível e o item ativo destacado por uma classe .ativo (ou pelo atributo aria-current).
Resultado esperado: os cinco links ficam lado a lado na mesma linha, com espaço entre eles; passar o mouse muda o fundo do item; navegar por Tab mostra o contorno; o item da página atual tem aparência distinta sem depender do mouse.
Dica
li { display: inline-block; } coloca os itens na linha; o espaço entre eles vem de margin-right no li ou de padding no a. Para o item ativo sem criar classe, use o seletor de atributo a[aria-current="page"] — você já colocou esse atributo no HTML. Um pequeno espaço em branco entre os itens aparece por causa das quebras de linha do HTML: por enquanto, aceite; na Aula 07 o Flexbox resolve.
B3. Monte o esqueleto semântico das cinco páginas do seu projeto autoral, aplicando o padrão de contêiner centralizado em header, main e footer, ligando uma folha de estilo externa com o reset e as variáveis base, e valide todas no W3C.
Resultado esperado: cinco páginas com um único <h1> cada, <main> único, <nav aria-label>, conteúdo centralizado com respiro nas laterais; o mesmo css/estilo.css ligado em todas; zero erros no validador.
Dica
Comece pela página mais simples e copie o <head>, o <header> e o <footer> para as outras. Cole o comando de contorno no console de cada uma e procure conteúdo fora de <main>. Se o validador acusar "Element h1 not allowed" ou "Section lacks heading", releia a seção 1.2.
B4. Pegue a página abaixo, feita só com <div>, e reescreva-a usando os elementos de seccionamento adequados. Documente, em comentários HTML, cada troca e o motivo.
HTML
<divid="topo"><divclass="logo"><imgsrc="logo.png"></div><divclass="menu"><ahref="index.html">Início</a><ahref="noticias.html">Notícias</a></div></div><divid="conteudo"><divclass="titulo">Últimas notícias</div><divclass="noticia"><divclass="titulo-noticia">Inscrições abertas</div><divclass="data">segunda-feira</div><divclass="texto">As inscrições começaram hoje.</div></div><divclass="noticia"><divclass="titulo-noticia">Palestrante confirmada</div><divclass="data">terça-feira</div><divclass="texto">Ana Souza falará sobre HTML semântico.</div></div></div><divid="lateral"><divclass="titulo">Links úteis</div><ahref="https://www.wikipedia.org">Wikipédia</a></div><divid="rodape">Todos os direitos reservados</div>
Resultado esperado: zero <div> com significado disfarçado; títulos como <h1>/<h2>/<h3>; cada notícia é um <article> com <time>; o menu é uma lista dentro de <nav>; a imagem tem alt; zero erros no validador; pelo menos oito comentários explicando as trocas.
Dica
Faça a pergunta da seção 1.2 para cada div: "existe um elemento que descreva o que isto é?". topo é um cabeçalho; menu é navegação; conteudo é o principal; noticia faz sentido sozinha; lateral é tangencial; rodape é rodapé. Os "títulos" são cabeçalhos de nível 1, 2 e 3. A div que sobra, se sobrar, é o contêiner centralizado.
C1. Engenharia reversa de um site real. Escolha a página inicial de um site institucional de verdade (o portal da sua universidade ou escola, o site de uma prefeitura, o gov.br). Com o DevTools, mapeie a estrutura de marcos dela: quantos <header>, <nav>, <main>, <section>, <article>, <aside> e <footer> existem, e quantas <div>. Depois reconstrua o esqueleto dessa página (sem copiar conteúdo — use textos próprios) em HTML semântico, com o contêiner centralizado, e escreva um parágrafo comparando: o site original usa os marcos corretamente? O que você faria diferente?
Dica
No console, document.querySelectorAll("div").length conta as div; troque por main, nav, article para os outros. Para ver os marcos como um leitor de tela vê, instale a extensão Accessibility Insights ou use a aba Accessibility do DevTools (painel Elements → Accessibility → Accessibility Tree). Sites com dezenas de div e nenhum main são comuns — e é exatamente isso que vale a pena registrar.
Alguém passou uma hora tentando descobrir por que "o CSS não pega". O arquivo está abaixo, e o HTML o referencia com <link rel="stylesheet" href="estilo.css"> — mas o arquivo está salvo em css/estilo.css. Além desse, há seis erros no CSS. Encontre todos os sete usando só o DevTools (painel Styles mostra declarações inválidas riscadas; a aba Network mostra o 404) antes de recorrer a qualquer validador.
Lista numerada com os sete problemas: linha, sintoma observável na página ou no DevTools, causa e correção.
O CSS corrigido, com as regras que dependiam de display funcionando (a largura dos links e do aviso aplicada de fato).
Uma frase explicando o que o painel Styles mostra quando uma declaração é inválida, e o que a aba Network mostra quando o arquivo não é encontrado.
Pistas
Dois erros são de sintaxe pura: um ; que falta e um comentário no formato errado. Releia a seção 2.1 sobre o que acontece com a declaração seguinte em cada caso.
Um nome de cor está escrito errado; o DevTools risca a declaração inválida e mostra um triângulo amarelo ao lado.
Uma borda precisa de três valores para aparecer (seção 4.4).
Dois blocos aplicam width e margin-top a elementos que, por padrão, ignoram essas propriedades (seção 5).
Abra o perfil de qualquer pessoa no GitHub e olhe os cartões de "repositórios fixados" (pinned): ícone, nome em azul, descrição em cinza, etiqueta da linguagem com uma bolinha colorida, contador de estrelas. Parece simples — e é o tipo de componente que você fará dezenas de vezes na carreira. Reproduza um cartão idêntico usando só o que esta aula ensinou: box model, display: inline-block, bordas, cores e variáveis. Sem Flexbox, sem Grid, sem position. Use o DevTools para descobrir os valores reais de padding, borda, raio e cores do cartão original.
Critérios de pronto
O cartão tem borda de 1 px, cantos arredondados, padding interno e largura máxima iguais aos do original (medidos no DevTools, com tolerância de 2 px).
O nome do repositório é um link; a bolinha da linguagem é um elemento inline-block de 12×12 px com border-radius: 50%.
Todas as cores estão em variáveis em :root, com os valores extraídos do original.
Três cartões lado a lado (inline-block) cabem em 1200 px sem quebrar; em 400 px, ficam um embaixo do outro.
Um comentário no CSS lista os valores originais que você mediu e onde os encontrou no DevTools (painel Styles ou Computed).
Pistas
No DevTools, selecione o cartão original e leia a aba Computed: ela mostra o valor final de padding, border-radius, border-color e background-color já resolvidos.
Elementos inline-block lado a lado ganham um espaço em branco entre si por causa das quebras de linha no HTML; reduza-o escrevendo as tags coladas ou aceite-o por enquanto.
A bolinha e o nome da linguagem ficam na mesma linha porque ambos são inline/inline-block; vertical-align: middle alinha a bolinha ao texto.
Para os três cartões caberem em 1200 px, calcule: com border-box, três caixas de 32% mais o espaço em branco entre elas passam de 100%? Ajuste até caber.
Explicar o box model para outra pessoa é a melhor forma de descobrir se você entendeu. Construa uma página didática, box-model.html, que demonstre visualmente cada conceito desta aula com exemplos vivos: a mesma caixa em content-box e border-box lado a lado, com as medidas escritas; três casos de colapso de margem (irmãos, pai e filho, sem colapso dentro de um elemento com padding); block, inline e inline-block recebendo a mesma width e reagindo de formas diferentes; e as três formas de esconder (display: none, visibility: hidden, opacity: 0) com um botão em cada uma para o leitor testar o clique. Tudo com HTML semântico e CSS externo — nada de JavaScript.
Critérios de pronto
Cada demonstração é uma <section> com título, um parágrafo explicando o que observar e o exemplo vivo ao lado do código-fonte correspondente em <pre><code>.
As medidas reais de cada caixa (largura ocupada, espaço entre elementos) aparecem escritas na página e conferem com o que o DevTools mostra.
As caixas usam cores diferentes para content, padding e border, visíveis a olho nu.
Alguém que nunca viu CSS consegue, lendo só a página, responder às questões A3, A4, A5 e A13 deste Laboratório — teste com uma pessoa nessas condições e registre as respostas.
A página valida no W3C e usa o padrão de contêiner centralizado.
Pistas
Para mostrar o código-fonte na página, escreva o HTML dentro de <pre><code> trocando < por < — senão o navegador renderiza em vez de exibir.
Para tornar visível o padding, use background-clip: content-box em um elemento e compare com o padrão border-box: a cor de fundo mostra onde termina o conteúdo.
O caso "pai e filho" do colapso fica evidente se você der um fundo à <section> pai: a margem do primeiro filho aparece fora do fundo. Adicione padding-top: 1px ao pai e veja a margem voltar para dentro.
Para os botões de "esconder", coloque três <a href="#a05-clicou"> com cada técnica e peça ao leitor para tentar clicar e usar Tab: só o de opacity: 0 responde.
Para ir além: publique a página (Aula 15 ou trilha Deploy) e use-a como material de revisão sempre que precisar retomar o assunto.
🔥
🔥 Boss — Auditoria e reconstrução de um site real¶
htmlacessibilidadeprojetoinvestigacao
Sites institucionais brasileiros — de prefeituras, secretarias, campi — costumam ter dezenas de <div>, imagens sem alt, formulários sem <label> e tabelas usadas para layout. Você agora sabe reconhecer tudo isso. Escolha um site institucional real (que não seja o mesmo usado no C1), audite as suas quatro páginas principais com tudo o que a Unidade 1 ensinou e reconstrua essas páginas em HTML semântico, acessível e válido, com conteúdo próprio inspirado no original. É o mini-projeto que fecha a unidade: arquitetura da Web, estrutura de documento, textos, links, tabelas, formulários, mídias, listas e seccionamento — tudo junto.
Critérios de pronto
Relatório de auditoria (uma a duas páginas) com, para cada uma das quatro páginas originais: número de <div> e de marcos semânticos, erros do validador W3C, imagens sem alt, campos sem <label>, tabelas usadas para layout, peso total na aba Network e nota de Acessibilidade no Lighthouse.
Quatro páginas reconstruídas, interligadas por menu em <nav>, com: <main> único, um <h1> por página, hierarquia de títulos correta, os três tipos de lista, uma tabela de dados com caption/thead/th scope, um formulário com no mínimo seis campos e label em todos, uma imagem em <figure> com alt adequado, um vídeo ou áudio com controls e um <iframe> com title.
Folha de estilo externa única com reset, box-sizing: border-box, variáveis base e o contêiner centralizado — e nada além disso (a estilização completa é assunto da Unidade 2).
Zero erros no validador nas quatro páginas; nota de Acessibilidade no Lighthouse igual ou superior a 90.
Tabela comparativa final: original × reconstrução, nas mesmas métricas do relatório.
README no repositório com o link do site original, as decisões de estrutura e o que você aprendeu.
Pistas
Para a auditoria, o console resolve as contagens: document.querySelectorAll("div").length, document.querySelectorAll("img:not([alt])").length, document.querySelectorAll("input:not([id])").length.
O Lighthouse está na aba homônima do DevTools; rode só a categoria Accessibility para ser rápido, e leia os itens reprovados — eles apontam o elemento exato.
Comece a reconstrução pelo esqueleto (Passo 3 da Mão na massa) e só depois preencha o conteúdo; um esqueleto validado evita retrabalho.
Conteúdo "inspirado no original" significa mesma estrutura de informação (seções, tipos de dado) com textos seus — não copie parágrafos nem imagens sem licença.
Para ir além: este Boss pode virar parte do seu Marco 1 — mostre-o junto com o restante do projeto.
Parte 1 — Leitura (20 min). SILVA, M. S. Criando sites com HTML, capítulos sobre estrutura de página e introdução ao CSS. Na MDN em pt-BR: "Estrutura de documento e site" e "O modelo de caixa" (links em Para aprofundar).
Parte 2 — Produção (30 min). Exercício B3: o esqueleto semântico das cinco páginas do seu projeto autoral, validado no W3C, mais a primeira folha de estilo (css/estilo.css) com reset, box-sizing: border-box global, variáveis de cores base e o contêiner centralizado aplicado nas cinco páginas.
Critério de pronto: as cinco páginas abrem com o mesmo cabeçalho e rodapé estilizados; document.querySelectorAll("main").length retorna 1 em cada página; o DevTools mostra box-sizing: border-box em qualquer elemento; zero erros no validador.
Guarde no seu repositório: commit + push (ou a pasta do projeto, se ainda não usa Git).
Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: publique o tema do seu projeto e o wireframe das páginas nas três larguras (celular, tablet, desktop) — pode ser desenhado à mão e fotografado. Os três marcos da trilha acompanham este mesmo projeto, cada um acrescentando uma camada (HTML no Marco 1, CSS no Marco 2, JavaScript no Marco 3). Se puder, compare o wireframe com o de outra pessoa, apontando um problema de hierarquia visual.
SILVA, Maurício Samy. Criando sites com HTML: sites de alta qualidade com HTML e CSS. Novatec, 2008 — capítulos sobre estrutura de página e introdução ao CSS.
TERUEL, Evandro C. HTML 5 — Guia Prático. Saraiva, 2014 — capítulo sobre os novos elementos estruturais do HTML5.
Na próxima aula você entra no CSS de verdade — seletores, cascata, especificidade, cores, unidades, tipografia e variáveis — e é também o dia do Marco 1: o site em HTML puro, com tudo o que a Unidade 1 ensinou. As instruções completas estão na Aula 06, e o escopo são os itens não-CSS do Checkpoint acima.
[ ] Pasta site-evento/ com as cinco páginas em HTML semântico validadas no W3C e css/estilo.css ligado em todas elas (Aula 05).
[ ] VS Code com Live Server e um navegador com DevTools. Hoje o painel Styles e a aba Computed são as ferramentas principais.
[ ] Revisar da Aula 05: sintaxe de uma regra CSS (seletor, declaração, bloco), o modelo de caixa, box-sizing: border-box, os valores de display e o gostinho de herança, :hover e variáveis da seção 6.
[ ] Marco 1 pronto — o site em HTML puro. As instruções completas estão no fim desta aula.
Na aula passada você deu ao site do evento um esqueleto semântico correto e escreveu a primeira folha de estilo: um reset com box-sizing: border-box, três variáveis, uma tipografia base e alguns componentes. Aquela folha funciona, mas não escala: as cores estão espalhadas, os seletores são genéricos demais e, quando duas regras brigam, você ainda não sabe explicar quem ganha. Hoje isso muda — e o site ganha um sistema de design de verdade. Esta aula abre a Unidade 2 e é também o dia do Marco 1.
Um seletor responde a uma pergunta: quais elementos desta página recebem este estilo? Quanto mais preciso o seletor, menos você luta contra o próprio CSS depois.
*{}/* universal: todos os elementos */p{}/* tipo (ou elemento): todos os <p> */.destaque{}/* classe: todos os elementos com class="destaque" */#cabecalho{}/* id: o único elemento com id="cabecalho" */
Universal (*) existe para o reset. Fora dele, quase nunca é a ferramenta certa.
Tipo serve para estilos base: como deve ser todo parágrafo, todo link, toda tabela deste site.
Classe é o seletor do dia a dia. Reutilizável, aplicável a qualquer elemento, fácil de sobrescrever.
id identifica um elemento único na página. Para estilo, evite: ele tem especificidade altíssima e não é reutilizável.
💡 Dica
A regra desta trilha, que você já conhece desde a Aula 05: estilize com classes; reserve id para âncoras (#topo), para o atributo for dos rótulos e para o JavaScript da Unidade 3. Se um estilo aparece uma vez hoje, quase certamente vai aparecer duas amanhã — e uma classe já estará pronta.
Cuidado com o efeito colateral: se um dos seletores da lista estiver errado, alguns navegadores antigos descartavam a regra inteira. Hoje isso não acontece mais com seletores válidos, mas continua valendo a disciplina de agrupar apenas o que é realmente comum.
1.3 Combinadores: seletores que descrevem relações¶
CSS
articlep{}/* descendente: todo <p> dentro de <article>, em qualquer nível */article>p{}/* filho direto: só os <p> filhos imediatos de <article> */h2+p{}/* irmão adjacente: o <p> imediatamente depois de um <h2> */h2~p{}/* irmãos gerais: todos os <p> depois de um <h2>, no mesmo pai */
Texto
<article> article p pega os 3 parágrafos
<p>A</p> article > p pega só A e C
<div> h2 + p pega só o parágrafo logo após o h2
<p>B</p> h2 ~ p pega todos os parágrafos irmãos depois do h2
</div>
<h2>Título</h2>
<p>C</p>
</article>
O combinador de irmão adjacente resolve um problema clássico de tipografia: dar uma margem maior ao parágrafo que abre uma seção, sem tocar nos outros.
CSS
h2+p{font-size:1.125rem;/* o parágrafo de abertura é um pouco maior */}
p.destaque{}/* um <p> QUE TEM a classe destaque */p.destaque{}/* qualquer elemento com classe destaque DENTRO de um <p> */.cartao.ativo{}/* um elemento que tem AMBAS as classes */.cartao.ativo{}/* um elemento com classe ativo dentro de um .cartao */
Esse espaço é o erro de digitação mais caro do CSS iniciante: o estilo "simplesmente não pega" e não há mensagem de erro nenhuma. Quando isso acontecer, o primeiro passo é ler o seletor em voz alta, palavra por palavra: "pê ponto destaque" (o mesmo elemento) ou "pê espaço ponto destaque" (um dentro do outro)?
🔬 Investigue
Abra index.html no Live Server e o DevTools. No painel Elements, selecione um <h2> e olhe a aba Styles: à direita de cada regra aparece o arquivo e a linha de origem; regras riscadas foram vencidas por outras. Agora, na aba Console, digite document.querySelectorAll("main p").length e depois document.querySelectorAll("main > p").length. Os números são diferentes — e a diferença é exatamente o que o combinador > exclui. Repita com .cartao p, .cartao > p e h2 + p. O querySelectorAll aceita os mesmos seletores do CSS, e é a maneira mais rápida de conferir se um seletor pega o que você acha que pega.
a[target]{}/* tem o atributo, qualquer que seja o valor */a[target="_blank"]{}/* valor exato */a[href^="https"]{}/* COMEÇA com (^ = começo) */a[href$=".pdf"]{}/* TERMINA com ($ = fim) */a[href*="wikipedia"]{}/* CONTÉM em qualquer posição */input[type="checkbox"]{}/* muito usado em formulários */
Eles são poderosos justamente porque estilizam pelo significado do HTML, sem exigir uma classe extra. Dois usos que você vai aplicar hoje:
CSS
/* Avisa visualmente que o link sai do site */a[href^="http"]::after{content:" ↗";}/* Avisa que o link baixa um arquivo pesado */a[href$=".pdf"]::after{content:" (PDF)";font-size:.875rem;}
No site do evento, o menu vai usar a[aria-current="page"] para destacar a página atual — o mesmo atributo que você aprendeu na Aula 04 por acessibilidade vira, de graça, um seletor de estilo. Essa é a recompensa de escrever HTML semântico: o CSS fica mais curto.
Uma pseudoclasse (um : simples) mira um elemento em determinado estado.
CSS
a:link{}/* link ainda não visitado */a:visited{}/* já visitado */a:hover{}/* mouse por cima */a:active{}/* no instante do clique */a:focus{}/* com foco (teclado ou clique) */a:focus-visible{}/* com foco E o navegador julga útil mostrar o anel */input:required{}input:checked{}input:disabled{}input:valid{}input:invalid{}input:placeholder-shown{}
⚠️ AtençãoOrdem obrigatória das pseudoclasses de link: LVHA — :link, :visited, :hover, :active. Como todas têm a mesma especificidade, quem vem depois vence; escrever :hover antes de :visited faz o :hover "não funcionar" em links já visitados. O mnemônico clássico é LoVe HAte.
:focus ou :focus-visible? Use :focus-visible. Ele mostra o anel de foco quando a navegação é por teclado e o esconde quando o foco veio de um clique de mouse — exatamente o comportamento que as pessoas esperam. E nunca escreva outline: none sem colocar outro indicador visível no lugar: um site sem foco visível é inutilizável por teclado, ponto.
li:first-child{}/* o primeiro filho */li:last-child{}/* o último filho */li:only-child{}/* quando é o único */tr:nth-child(3){}/* o terceiro */tr:nth-child(odd){}/* ímpares: 1, 3, 5… */tr:nth-child(even){}/* pares: 2, 4, 6… */li:nth-child(3n){}/* de 3 em 3: 3, 6, 9… */li:nth-child(3n+1){}/* 1, 4, 7… */p:not(.destaque){}/* todos os <p> que NÃO têm a classe destaque */
:nth-child(even) é o que dá as linhas alternadas de uma tabela — o famoso "zebrado" que você vai aplicar na programação do evento. Já :not() evita regras duplicadas: em vez de escrever um estilo para todos os parágrafos e depois desfazê-lo em alguns, você exclui de uma vez.
📌 Vale gravar:nth-child(n) conta todos os irmãos, não só os do mesmo tipo. Em <div><h2></h2><p>A</p><p>B</p></div>, o parágrafo A é o segundo filho — p:nth-child(1) não pega nada. Quando você quer contar só os do mesmo tipo, existe :nth-of-type(n). Essa diferença é pegadinha clássica de entrevista e bug certo na vida real.
1.8 Pseudoelementos: partes que não existem no HTML¶
Um pseudoelemento (dois-pontos duplos, ::) cria ou alcança uma parte virtual do elemento.
CSS
p::first-line{font-weight:600;}p::first-letter{font-size:3rem;line-height:1;}.aviso::before{content:"⚠ ";}.externo::after{content:" ↗";}li::marker{color:var(--cor-secundaria);}/* o marcador da lista */::selection{background:#0b3d5c;color:#fff;}/* texto selecionado */
A propriedade content é obrigatória em ::before e ::after: sem ela, o pseudoelemento não é criado — nem mesmo com content: "" esquecido. Esse é o motivo número um de "meu ::after não aparece".
⚠️ Atenção
Conteúdo criado por content é decoração, não informação. Ele pode não ser lido por leitores de tela, não é copiado de forma confiável e some se o CSS não carregar. Use ::before/::after para ícones, setas e aspas decorativas — nunca para texto que a pessoa precisa ler para entender a página. A seta de link externo é aceitável porque a informação essencial ("este link sai do site") também deve estar no texto ou no title do link.
🧠 Você sabia?
A sintaxe de dois-pontos duplos (::before) só apareceu no CSS3, justamente para distinguir pseudoelementos (partes virtuais) de pseudoclasses (estados). Antes disso, tudo usava um dois-pontos só, e os navegadores aceitam :before até hoje por compatibilidade retroativa. Nunca deixaram de aceitar — porque remover algo da Web quebra páginas que ninguém pode mais consertar. É a mesma lógica que mantém <form> funcionando desde 1993, e o motivo pelo qual o CSS tem tantos "dois jeitos de fazer a mesma coisa". Escreva sempre com ::; leia : sem estranhar.
Estilos padrão do navegador (é por isso que um <h1> já nasce grande e em negrito).
Estilos do usuário (folha de estilo pessoal, configurações de acessibilidade).
Estilos do autor — você, no estilo.css.
Declarações !important do autor.
Declarações !important do usuário — a última palavra é sempre de quem lê a página.
Repare na inversão do topo: o !important do usuário vence o do autor. Isso é proposital. Alguém que precisa de fonte gigante ou de altíssimo contraste para conseguir ler tem prioridade sobre o seu design.
⚠️ Atenção!important resolve o conflito de agora e cria um problema permanente: para sobrescrevê-lo, só com outro !important mais específico — e a folha vira uma escada de gritos. Regra desta trilha: !important é proibido nos trabalhos, salvo justificativa escrita (a única exceção que você vai encontrar é dentro do bloco prefers-reduced-motion da Aula 09). Se precisou dele, o problema é de arquitetura, e o remédio é reorganizar os seletores.
Se duas regras têm a mesma origem, vence a mais específica. Conte cada seletor como um trio (A, B, C):
Peso
O que conta
Exemplos
A
id
#menu
B
Classes, atributos e pseudoclasses
.cartao, [type="text"], :hover
C
Elementos e pseudoelementos
p, div, ::before
O universal (*) e os combinadores (>, +, ~) valem zero. :not() não conta em si, mas o que está dentro dele conta.
Seletor
A, B, C
Leitura
p
0, 0, 1
0-0-1
.destaque
0, 1, 0
0-1-0
p.destaque
0, 1, 1
0-1-1
nav ul li a
0, 0, 4
0-0-4
.menu a
0, 1, 1
0-1-1
#menu
1, 0, 0
1-0-0
#menu li a:hover
1, 1, 2
1-1-2
style="…" (inline)
—
Vence tudo, exceto !important
Compara-se A primeiro; só em caso de empate compara-se B; e, empatando de novo, C. Um id vence qualquer quantidade de classes: #menu (1-0-0) ganha de .nav .lista .item .link (0-4-0). É exatamente por isso que estilizar com id é uma armadilha — você constrói uma parede que só um id maior derruba.
Um caso real que você vai encontrar na Aula 07. Um menu com um link em formato de botão:
O botão ficaria com texto escuro sobre fundo escuro, e nada no console avisaria. Quando um estilo "não pega", a especificidade é a primeira suspeita — e o DevTools mostra a regra vencida riscada.
Empatou em especificidade? Vence a última regra escrita.
CSS
.botao{background-color:#0b3d5c;}.botao{background-color:#1a7fb5;}/* esta vence: veio depois */
Isso vale também entre arquivos: se a página tem dois <link rel="stylesheet">, o segundo sobrescreve o primeiro em caso de empate. E é por isso que a ordem das seções dentro da folha (seção 7 desta aula) importa: base primeiro, componentes depois, utilitários por último.
Algumas propriedades passam automaticamente de pai para filho. Você já viu isso na Aula 05; agora vale a tabela completa e a lógica por trás.
Herdadas (passam para os filhos)
Não herdadas (cada elemento define a sua)
color, font-family, font-size, font-weight
margin, padding, border
line-height, text-align, letter-spacing
width, height, background
visibility, cursor, list-style
display, position, overflow
A lógica: propriedades de texto são herdadas (faz sentido que o parágrafo use a fonte do body); propriedades de caixa não são (não faz sentido que todo filho de uma caixa com borda ganhe borda também).
A herança é o que permite escrever pouco CSS:
CSS
body{font-family:var(--fonte-base);font-size:1rem;line-height:1.6;color:var(--cor-texto);}/* O site inteiro herda estas quatro declarações. */
Quatro palavras-chave funcionam em qualquer propriedade e controlam a herança na mão:
CSS
.filho{border:inherit;/* força a herança de uma propriedade que não é herdada */color:initial;/* volta ao valor padrão da especificação (preto, no caso) */padding:unset;/* herda se for herdável; volta ao inicial se não for */margin:revert;/* volta ao valor que o NAVEGADOR daria */}
revert é a mais útil das quatro: ela desfaz o seu CSS sem desfazer o do navegador. Útil quando um reset agressivo tirou algo que você quer de volta em um caso específico.
color:red;/* 1. nome — 147 nomes padronizados */color:#0b3d5c;/* 2. hexadecimal: #RRGGBB */color:#f0a;/* 3. hex abreviado = #ff00aa */color:#0b3d5c80;/* 4. hex com alfa (últimos dois dígitos) */color:rgb(11,61,92);/* 5. RGB decimal, 0 a 255 */color:rgba(11,61,92,0.5);/* 6. RGB com transparência, 0 a 1 */color:hsl(203,79%,20%);/* 7. matiz, saturação, luminosidade */color:hsla(203,79%,20%,0.5);/* 8. HSL com transparência */
O hexadecimal é a notação mais comum porque é a que as ferramentas de design cospem. Mas ele é ilegível para humanos: olhando #0b3d5c você não sabe se é claro ou escuro, nem qual é o "primo mais claro" dessa cor.
H (hue, matiz): a cor em si, em graus de 0 a 360 no círculo cromático (0 vermelho, 120 verde, 240 azul).
S (saturation, saturação): quanto de cor, de 0% (cinza) a 100% (vibrante).
L (lightness, luminosidade): de 0% (preto) a 100% (branco); 50% é a cor "pura".
Isso torna trivial construir uma paleta coerente: mantenha o H, varie o L.
CSS
:root{--azul-900:hsl(203,79%,20%);/* mais escuro — texto sobre claro */--azul-700:hsl(203,79%,32%);--azul-500:hsl(203,75%,40%);/* a cor "da marca" */--azul-300:hsl(203,60%,70%);--azul-100:hsl(203,60%,94%);/* mais claro — fundos suaves */}
Cinco tons que combinam entre si, escritos em cinco linhas, sem nenhuma ferramenta. Tente fazer isso adivinhando hexadecimais.
💡 Dica
No DevTools, clique no quadradinho de cor ao lado de qualquer declaração de color ou background: abre o seletor de cores. Clique no rótulo do formato (embaixo, à direita do valor) e ele converte entre hex, rgb e hsl na hora. É a maneira mais rápida de descobrir o hsl() equivalente a um hexadecimal que veio de um layout.
E o oklch()? Existe uma notação mais nova, oklch(luminosidade croma matiz) — por exemplo, oklch(45% 0.09 240). Ela resolve um defeito do HSL: dois tons com o mesmo L em HSL podem parecer bem diferentes em brilho para o olho humano (compare um amarelo e um azul com L: 50%). O oklch é perceptualmente uniforme, ou seja, mesma luminosidade significa mesmo brilho aparente. Já funciona em todos os navegadores atuais e vale conhecer; nesta trilha continuamos com hsl() e hexadecimal, que é o que você vai encontrar em 99% do código existente.
background-color:#f5f5f5;background-image:url("../img/textura.png");background-repeat:no-repeat;background-position:center;background-size:cover;/* cobre todo o espaço, cortando o excesso */background-size:contain;/* cabe inteira, podendo sobrar espaço *//* Forma abreviada: cor, imagem, repetição, posição / tamanho */background:#f5f5f5url("../img/fundo.jpg")no-repeatcenter/cover;/* Gradientes são imagens, não cores */background-image:linear-gradient(toright,#0b3d5c,#1a7fb5);background-image:linear-gradient(160deg,#0b3d5c0%,#1a7fb5100%);background-image:radial-gradient(circle,#ffffff,#cccccc);
Repare no caminho url("../img/textura.png"): dentro de css/estilo.css, o ../ sobe para a raiz do site antes de entrar em img/. Caminhos em CSS são relativos ao arquivo CSS, não ao HTML — é a causa mais comum de imagem de fundo que não aparece.
A WCAG exige 4,5:1 de contraste entre texto e fundo (3:1 para texto grande — 24 px, ou 18,5 px em negrito). Texto cinza-claro sobre branco reprova; texto branco sobre amarelo reprova com folga.
Só texto grande e bordas — reprova em texto normal
Essa última linha é o tipo de descoberta que o verificador entrega e o olho não: #1a7fb5 parece perfeitamente legível, e falha por pouco. Por isso --cor-secundaria é usada, no site do evento, em bordas, ícones e estado :hover — nunca como fundo de texto pequeno.
⚠️ Atenção
Nunca comunique uma informação só por cor. Campo inválido com borda vermelha e mais nada exclui quem tem daltonismo (cerca de 8% dos homens). Acrescente sempre um segundo sinal: um ícone, um texto, uma mudança de espessura de borda. Isso entra no checklist de qualidade dos Marcos 2 e 3.
font-size do próprio elemento (ou do pai, quando aplicada ao próprio font-size)
Acumula em aninhamentos
rem
font-size da raiz (<html>)
Previsível; padrão para tipografia
vw / vh
1% da largura / altura da janela
100vh é a altura total da tela
vmin / vmax
Menor / maior dimensão da janela
Útil em elementos quadrados
ch
Largura do caractere "0" da fonte atual
Ideal para limitar a linha de texto
CSS
html{font-size:100%;}/* respeita o padrão do navegador: 16px */h1{font-size:2rem;}/* 32px */p{font-size:1rem;}/* 16px */small{font-size:0.875rem;}/* 14px */
⚠️ AtençãoA armadilha do em. Se .pai { font-size: 1.5em } e, dentro dele, .filho { font-size: 1.5em }, o filho fica com 2,25× o tamanho base. Em três níveis, 3,4×. Com rem isso não acontece, porque a referência é sempre a raiz — a fonte de um item de menu não muda porque alguém aninhou mais uma <div>. Use rem para tipografia e em só quando quiser essa proporcionalidade local (o padding de um botão que cresce junto com o texto dele, por exemplo).
🧠 Você sabia?
Nunca escreva html { font-size: 14px } para "deixar tudo menor". Essa linha é uma das piores coisas que se pode fazer com acessibilidade: uma pessoa com baixa visão que configurou o navegador para fonte 20 px acabou de ter a escolha dela anulada — e não faz ideia do porquê. Todo o valor do rem está em ele ser relativo a uma raiz que o usuário controla. Se você precisa de tudo menor, reduza os valores em rem das suas regras, não a raiz. html { font-size: 100% } é a única declaração de tamanho de raiz aceitável nesta trilha.
4.3 Fazendo contas: calc(), min(), max() e clamp()¶
CSS
width:calc(100%-40px);/* largura total menos um espaço fixo */height:calc(100vh-80px);/* tela inteira menos o cabeçalho */width:min(1100px,100%-2rem);/* o MENOR dos dois: nunca estoura a tela */width:max(300px,30%);/* o MAIOR dos dois: nunca fica estreito demais */font-size:clamp(1rem,2.5vw,1.5rem);/* mínimo, ideal fluido, máximo */
Os espaços em volta dos operadores de calc() são obrigatórios: calc(100%-40px) não funciona, e o navegador não avisa. Motivo histórico: sem espaço, -40px seria interpretado como um número negativo, não como uma subtração.
clamp() é a peça central da tipografia fluida da Aula 08 — por ora, guarde a leitura: "no mínimo 1rem, idealmente 2,5% da largura da janela, no máximo 1.5rem".
A pilha é lida da esquerda para a direita: o navegador usa a primeira que existir no sistema do usuário. Sempre termine com uma família genérica (sans-serif, serif, monospace) — é a garantia de que existe um fim de linha.
Nomes com espaço vão entre aspas ("Segoe UI"). Uma pilha "de sistema" — que usa a fonte nativa de cada sistema operacional e não baixa nada — é uma escolha profissional e rápida:
body{font-family:var(--fonte-base);font-size:1rem;font-weight:400;/* 100 a 900; 400 = normal, 700 = negrito */font-style:normal;/* normal | italic | oblique */line-height:1.6;/* SEM unidade = múltiplo do font-size. Recomendado */letter-spacing:0.01em;color:var(--cor-texto);}h1{font-size:2rem;line-height:1.2;/* títulos pedem entrelinha menor */text-transform:none;/* uppercase | lowercase | capitalize */text-align:left;/* center | right | justify */}a{text-decoration:underline;text-underline-offset:2px;/* afasta o sublinhado da base das letras */}
💡 Dicaline-heightsem unidade (1.6) é sempre a escolha certa. Com unidade (line-height: 24px ou 1.6em), o valor calculado é herdado pelos filhos — então um título de 2rem dentro de um body com line-height: 24px herda 24 px de entrelinha e as letras se sobrepõem. Sem unidade, o que se herda é o fator, e cada elemento calcula a própria entrelinha a partir do próprio tamanho.
preconnect abre a conexão com o servidor de fontes antes de ela ser necessária — economiza cerca de 100 ms.
wght@400;600;700 importa três pesos. Cada peso é um arquivo baixado: importar oito "por precaução" pode custar centenas de kilobytes e atrasar a primeira pintura da página.
display=swap mostra o texto imediatamente com a fonte de reserva e troca quando a externa chegar. Sem ele, alguns navegadores escondem o texto por até três segundos.
🔎 Por baixo do capô
A troca de fonte tem dois nomes que aparecem em toda auditoria de performance: FOUT (flash of unstyled text, o texto aparece com a fonte de reserva e "pula" quando a definitiva carrega) e FOIT (flash of invisible text, o texto simplesmente não aparece até a fonte chegar). display=swap escolhe deliberadamente o FOUT — porque texto visível com a fonte errada é melhor do que página em branco. Para reduzir o "pulo", escolha uma fonte de reserva com largura parecida (Arial para Inter, Georgia para uma serifada) — é o motivo real de a pilha de fontes ter mais de um nome.
O nome precisa começar com dois hífens (--) e diferencia maiúsculas de minúsculas (--Cor e --cor são variáveis diferentes). :root é o seletor do elemento <html>: declarar ali torna a variável visível na página inteira.
Variáveis são herdadas, e é isso que as torna poderosas: você pode redefinir uma variável em qualquer escopo e tudo abaixo dela muda.
CSS
:root{--cor-fundo-cartao:#ffffff;}.destaque{--cor-fundo-cartao:#fff8e1;}/* só dentro de .destaque */.cartao{background-color:var(--cor-fundo-cartao);}
Um .cartao dentro de .destaque fica amarelado; os outros continuam brancos — e a regra do .cartao não foi tocada. É esse mecanismo que permite temas claro e escuro trocando poucas linhas (assunto da Aula 09 e requisito do projeto final).
Vire variável tudo que se repete e pode mudar junto: cores da marca, espaçamentos da escala, raios de borda, sombras, a pilha de fontes. Não vire variável valores que aparecem uma vez só, nem valores que por acaso são iguais mas não têm relação (o padding de um botão e a margin de um parágrafo podem ser 16 px hoje e nada obriga que continuem iguais).
E cuidado com o nome: --azul-escuro é um nome ruim, porque no dia em que a identidade virar verde, a variável passa a mentir. --cor-primaria continua verdadeira.
Uma folha de estilo cresce todo dia. Sem uma ordem combinada, em duas semanas ninguém acha nada — e regras duplicadas começam a brigar. A ordem que você vai usar da Aula 06 até a Aula 09:
A ordem não é arbitrária: ela vai do mais genérico ao mais específico, e a etapa 3 da cascata (ordem no arquivo) faz o resto. Um utilitário escrito na seção 6 sobrescreve um componente da seção 5 sem precisar de especificidade extra — que é exatamente o que se espera de um utilitário.
O nome da classe deve descrever a função, não a aparência:
Ruim
Bom
Por quê
.texto-vermelho
.mensagem-erro
Se mudar para laranja, o nome não mente
.caixa-esquerda
.barra-lateral
Se mudar de lado, o nome continua válido
.f18b
.titulo-secao
Legibilidade para quem lê depois (inclusive você)
.azul-grande
.botao--primario
Descreve o papel, não os pixels
O padrão bloco__elemento--modificador (.cartao, .cartao__titulo, .cartao--destaque) tem nome: BEM. Você não é obrigado a segui-lo à risca nesta trilha, mas vai encontrá-lo na Aula 07 e em quase todo projeto profissional — e ele resolve um problema real: um nome de classe longo e específico nunca colide com o de outro componente.
💻 Mão na massa — O sistema de design do site do evento¶
Hoje você reescreve css/estilo.css inteiro. A folha da Aula 05 tinha 5 seções e 3 variáveis; a nova terá 7 seções, um sistema de design em 10 variáveis, uma escala tipográfica, botões com todos os estados e a tabela de programação estilizada.
Na Aula 05, todas as regiões receberam um <div class="container">. O <main> não precisa dele: como o conteúdo principal não tem fundo colorido sangrando até as bordas da tela, o próprio <main> pode assumir a largura máxima do projeto. Em cada página, remova o <div class="container"> de dentro do <main> (e o </div> correspondente), promovendo as seções um nível:
site-evento/index.html (trecho do <main>, depois da mudança)
HTML
<main><sectionclass="hero"><h2>Três noites de palestras, minicursos e maratona de programação</h2><p>Participação gratuita, com certificado de 20 horas para quem comparecer a pelo menos 75% das atividades.</p><ahref="inscricao.html"class="botao">Inscreva-se</a></section></main>
O <div class="container"> continua no <header> e no <footer>, onde ele é necessário: essas duas regiões têm fundo azul de ponta a ponta da tela, e só o conteúdo delas fica limitado a 1100 px — o mesmo 1100 px que a Aula 05 fixou.
💡 Dica
Guarde esse número. Na Aula 07 ele vira a variável --largura-max, e a classe .container volta ao <main> — não mais como uma <div> interna, mas aplicada ao próprio <main> ou à <section> de topo. A largura nunca muda; o que muda é quem carrega a classe.
Abra css/estilo.css e substitua o arquivo inteiro. Comece pelas duas primeiras seções:
site-evento/css/estilo.css — seções 1 e 2
CSS
/* ========================================================== Semana Acadêmica de Sistemas de Informação — folha de estilo 1. Reset · 2. Variáveis · 3. Base · 4. Layout 5. Componentes · 6. Utilitários · 7. Media queries ========================================================== *//* 1. Reset e box-sizing */*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}/* 2. Variáveis */:root{--cor-primaria:#0b3d5c;--cor-secundaria:#1a7fb5;--cor-texto:#333333;--cor-fundo:#f7f9fb;--fonte-base:"Inter",Arial,sans-serif;--espaco-pequeno:8px;--espaco-medio:16px;--espaco-grande:32px;--raio-borda:8px;--sombra-cartao:02px8pxrgba(0,0,0,0.1);}
Dez variáveis: quatro cores, uma fonte, três espaçamentos, um raio e uma sombra. É o sistema de design do projeto — a partir de agora, nenhum valor de cor ou de espaçamento é escrito solto no meio da folha. Na Aula 07 você vai acrescentar mais três.
💡 Dica
Três espaçamentos parecem pouco, e são suficientes. Uma escala curta (8 / 16 / 32) força consistência: o olho percebe imediatamente quando um espaço "não é nenhum dos três". Quando você precisar de um valor intermediário, quase sempre o certo é usar um dos três, não inventar o quarto.
Passo 4 — Estilos base: tipografia, links e imagens¶
site-evento/css/estilo.css — seção 3
CSS
/* 3. Base */html{font-size:100%;/* respeita o tamanho escolhido pelo usuário */scroll-behavior:smooth;/* rolagem suave nas âncoras "Voltar ao topo" */}body{font-family:var(--fonte-base);font-size:1rem;line-height:1.6;color:var(--cor-texto);background-color:var(--cor-fundo);}h1,h2,h3{line-height:1.2;color:var(--cor-primaria);margin-bottom:var(--espaco-medio);}h1{font-size:2rem;}h2{font-size:1.5rem;}h3{font-size:1.25rem;}p,ul,ol,dl,table{margin-bottom:var(--espaco-medio);}p{max-width:65ch;/* linha de leitura confortável */}ul,ol{padding-left:var(--espaco-grande);/* devolve o recuo que o reset tirou */}a{color:var(--cor-primaria);text-decoration:underline;text-underline-offset:2px;}a:hover{color:var(--cor-secundaria);}/* Sinaliza links que saem do site */a[href^="http"]::after{content:" ↗";font-size:.875rem;}/* Foco visível em TUDO que é focável */:focus-visible{outline:3pxsolidvar(--cor-secundaria);outline-offset:2px;}img,video{max-width:100%;height:auto;display:block;}
Quatro decisões que merecem explicação:
ul, ol { padding-left: var(--espaco-grande) } devolve o recuo que o reset universal apagou. Sem essa linha, os marcadores das listas ficam para fora da caixa e somem no corte.
p { max-width: 65ch } limita a linha de leitura mesmo antes de existir qualquer layout. É o ajuste de legibilidade mais barato que existe.
:focus-visible sem seletor de elemento aplica-se a tudo que recebe foco: links, botões, campos. Uma regra, o site inteiro acessível pelo teclado.
a[href^="http"]::after usa um seletor de atributo para marcar links externos. Como todos os links internos do site são relativos (programacao.html), só os externos começam com http.
/* 4. Layout */.container{max-width:1100px;margin:0auto;padding-inline:var(--espaco-medio);}header{background-color:var(--cor-primaria);color:#ffffff;padding-block:var(--espaco-grande);}headerh1,headerh2{color:#ffffff;}headera{color:#ffffff;}headernavul{padding-left:0;list-style:none;}headernavli{display:inline-block;/* menu na horizontal, sem Flexbox ainda */margin-right:var(--espaco-medio);}main{max-width:var(--largura-max,1100px);margin:0auto;padding:var(--espaco-grande)var(--espaco-medio);}mainsection{margin-bottom:var(--espaco-grande);}mainsection>h2{border-bottom:3pxsolidvar(--cor-secundaria);padding-bottom:var(--espaco-pequeno);}footer{background-color:var(--cor-primaria);color:#ffffff;text-align:center;padding-block:var(--espaco-grande);}footera{color:#ffffff;}footerp{max-width:none;/* o rodapé é centralizado, não é leitura corrida */margin-inline:auto;}
O menu horizontal com display: inline-block é um paliativo consciente: na Aula 07 ele será refeito com Flexbox, que resolve alinhamento e espaçamento de forma muito mais limpa. Por hoje, funciona e não atrapalha.
⚠️ Atenção
As regras header a { color: #ffffff } e footer a { color: #ffffff } têm especificidade 0-0-2 e vencem a { color: var(--cor-primaria) } (0-0-1) — que é o que queremos, porque azul-escuro sobre azul-escuro seria ilegível. Se você tivesse escrito .container a, a especificidade seria 0-1-1 e o resultado seria o mesmo; mas aí o main também seria afetado, porque ele também estava dentro de um .container até o Passo 2. É por isso que seletores mais precisos são mais fáceis de manter.
/* 5. Componentes */.botao{display:inline-block;padding:12px24px;background-color:var(--cor-primaria);color:#ffffff;font-weight:600;text-decoration:none;border:2pxsolidvar(--cor-primaria);border-radius:var(--raio-borda);cursor:pointer;}.botao:hover,.botao:focus-visible{background-color:var(--cor-secundaria);border-color:var(--cor-secundaria);}.botao:active{background-color:#072a40;/* um tom mais escuro no instante do clique */}.botao--contorno{background-color:transparent;color:var(--cor-primaria);}.botao--contorno:hover,.botao--contorno:focus-visible{background-color:var(--cor-primaria);color:#ffffff;}.botao:disabled,.botao[aria-disabled="true"]{background-color:#9aa8b2;border-color:#9aa8b2;cursor:not-allowed;}
Quatro estados em um componente só: repouso, :hover/:focus-visible, :active e desabilitado. Um botão que não muda de aparência ao receber foco é um botão quebrado para quem usa teclado.
site-evento/css/estilo.css — seção 5, cartão
CSS
.cartao{background-color:#ffffff;border:1pxsolid#dfe6ec;border-radius:var(--raio-borda);padding:var(--espaco-medio);margin-bottom:var(--espaco-medio);box-shadow:var(--sombra-cartao);}.cartaoh3{margin-bottom:var(--espaco-pequeno);}.cartaop:last-child{margin-bottom:0;/* sem espaço sobrando no fim do cartão */}
site-evento/css/estilo.css — seção 5, tabela da programação
CSS
table{width:100%;border-collapse:collapse;/* junta as bordas duplas em uma só */}caption{text-align:left;font-weight:600;color:var(--cor-primaria);padding-bottom:var(--espaco-pequeno);}th,td{padding:var(--espaco-pequeno)var(--espaco-medio);border-bottom:1pxsolid#dfe6ec;text-align:left;vertical-align:top;}theadth{background-color:var(--cor-primaria);color:#ffffff;}tbodyth[scope="colgroup"]{background-color:#e8eef3;color:var(--cor-primaria);}tbodytr:nth-child(even){background-color:#ffffff;/* zebrado: linhas pares em branco */}tbodytr:hover{background-color:#eef4f8;}tfoottd{font-size:.875rem;font-style:italic;}
Três seletores desta tabela vieram direto da teoria de hoje: tbody tr:nth-child(even) (pseudoclasse estrutural), tbody th[scope="colgroup"] (seletor de atributo, aproveitando a semântica que você escreveu na Aula 02) e tbody tr:hover (pseudoclasse de estado). Nenhuma classe nova foi necessária — o HTML bem marcado já dizia tudo.
Duas tabelas do site usam essas regras: a da programação, escrita na Aula 02, e a de horário de atendimento em contato.html, escrita na Aula 05. Na Aula 07 a programação será redesenhada em cartões; a folha de estilo da tabela continua valendo para a de contato.html, então nada aqui é desperdício.
⚠️ Atençãoborder-collapse: collapse é o que transforma a tabela padrão (com bordas duplas e um espaço entre as células) em uma tabela de aparência profissional. Ela precisa ir no elemento table, não nas células — e, com collapse ativo, border-radius na tabela deixa de ter efeito visível nas células das pontas.
Passo 7 — Utilitários e o espaço das media queries¶
site-evento/css/estilo.css — seções 6 e 7
CSS
/* 6. Utilitários */.texto-centro{text-align:center;}.sem-margem{margin:0;}/* Some da tela, continua existindo para leitores de tela */.oculto-visualmente{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;}/* 7. Media queries *//* Reservado para a Aula 08: mobile first e breakpoints. */
.oculto-visualmente é o utilitário mais importante da lista. Ele resolve um caso frequente: um rótulo que precisa existir para o leitor de tela mas ficaria redundante na tela (um campo de busca com ícone de lupa, por exemplo). Note que não é display: none: isso removeria o elemento da árvore de acessibilidade, e ele deixaria de ser lido — o oposto da intenção.
Crie, na raiz do projeto, um arquivo contraste.md anotando cada par (texto × fundo), a razão obtida e onde ele é usado. Esse arquivo faz parte do Marco 2 (Aula 10).
No DevTools, selecione qualquer texto e abra a aba Computed: confira font-size (16 px no corpo), line-height (25,6 px = 1,6 × 16) e color.
As cinco páginas abrem no Live Server com a fonte Inter, cabeçalho e rodapé azul-escuros e conteúdo centralizado com no máximo 1100 px.
Cada <h2> de seção tem a linha azul-clara embaixo; nenhum parágrafo passa de cerca de 65 caracteres por linha.
Os links externos (LinkedIn e GitHub, na página de palestrantes) mostram a seta ↗; os internos, não.
Pressionando Tab, cada link e cada botão ganha um contorno azul de 3 px visível — inclusive dentro do cabeçalho azul.
Em programacao.html, a tabela ocupa a largura toda, o cabeçalho é branco sobre azul-escuro, as linhas alternam de cor e a linha sob o cursor muda de fundo.
Passando o mouse sobre o botão "Inscreva-se", ele fica azul-claro; segurando o clique, escurece.
No DevTools, mudar --cor-primaria no bloco :root muda cabeçalho, rodapé, títulos, links, botões e cabeçalho da tabela ao mesmo tempo. Esse é o teste definitivo de que o sistema de design está funcionando: faça, é impressionante.
Resultado esperado: o mesmo site da Aula 05, agora com identidade visual coerente, tipografia legível, botões com quatro estados, tabela zebrada e uma folha de estilo em que qualquer alteração de marca custa uma linha. O layout ainda é uma pilha de blocos — isso é a Aula 07.
A1. Escreva o seletor CSS para: (a) todos os parágrafos; (b) elementos com a classe aviso; (c) o elemento com id="a06-topo"; (d) parágrafos dentro de article; (e) apenas os li filhos diretos de uma ul; (f) links que abrem em nova aba.
A2. Qual é a diferença entre p.destaque e p .destaque? Escreva um trecho de HTML em que o primeiro pega um elemento e o segundo não pega nenhum.
A3. Calcule a especificidade (A, B, C) de: (a) p; (b) .cartao; (c) #menu a; (d) nav ul li a:hover; (e) .cartao .titulo span; (f) article > p.intro.
A4. Dado o CSS e o HTML abaixo, de que cor fica o parágrafo? Justifique com o cálculo das quatro especificidades.
B1. Estilize uma tabela de notas em exercicios/aula06/notas.html: border-collapse: collapse, cabeçalho com fundo escuro e texto claro, linhas pares com fundo cinza-claro (:nth-child(even)), destaque no :hover da linha, colunas numéricas alinhadas à direita e caption estilizado acima da tabela.
Resultado esperado: a tabela ocupa 100% da largura do contêiner; as bordas não são duplas; as linhas alternam visivelmente; passar o mouse muda o fundo da linha inteira; as notas ficam alinhadas à direita e os nomes à esquerda.
Dica
Para alinhar só a coluna de notas, use um seletor de posição (td:nth-child(3)) ou uma classe nas células numéricas. A segunda opção é mais robusta: se você acrescentar uma coluna, o número muda e o nth-child mira a coluna errada.
B2. Construa uma paleta documentada em exercicios/aula06/paleta.html: dez quadrados coloridos, cada um exibindo o nome da cor, o hexadecimal e o hsl(). Todas as cores declaradas como variáveis em :root. A paleta deve ser coerente: uma primária, uma secundária, uma de destaque, três neutros e as quatro cores semânticas (sucesso, alerta, erro, informação).
Resultado esperado: dez blocos, cada um com o nome do papel da cor (não o nome da cor), os dois valores e a razão de contraste com preto e com branco; as cores da mesma família compartilham o matiz (H) e variam a luminosidade (L).
Dica
Escolha o matiz primeiro (um número de 0 a 360) e construa a família variando só o L. Para as semânticas, os matizes convencionais são: sucesso perto de 140, alerta perto de 40, erro perto de 0 e informação perto de 210.
B3. Estilize completamente o formulário de inscrição da Aula 03. Em css/estilo.css, na seção 5, acrescente um bloco de formulário: campos com padding, borda e border-radius; :focus-visible com borda colorida; :invalid com borda vermelha e um segundo sinal que não seja cor; :valid discreto; fieldset com borda suave e legend destacada; rótulos em bloco acima dos campos.
Resultado esperado: a inscricao.html fica legível e agradável sem uma linha de JavaScript; navegando por Tab, cada campo mostra claramente que está focado; um campo obrigatório vazio, depois de tocado, indica o problema por borda e por outro sinal visual.
Dica
input:invalid marca o campo como inválido desde o carregamento da página, o que assusta o usuário antes de ele digitar qualquer coisa. Combine com :not(:placeholder-shown) para só sinalizar depois que a pessoa começou a preencher. Para o segundo sinal, ::after no rótulo ou um border-left mais grosso funcionam bem.
B4. Experimento de especificidade em exercicios/aula06/especificidade.html: uma página com um único parágrafo e seis regras diferentes que o atingem, cada uma com uma cor distinta. Preveja no papel qual cor vencerá, teste no navegador e escreva um relatório explicando o resultado com a tabela (A, B, C) de cada seletor.
Resultado esperado: as seis regras, a previsão escrita antes do teste, uma captura de tela do painel Styles mostrando as cinco regras riscadas, e a explicação de por que a vencedora venceu (etapa 2 ou etapa 3 da cascata).
Dica
Inclua pelo menos um empate proposital — duas regras com a mesma especificidade — para que a etapa 3 (ordem no arquivo) apareça no relatório. E inclua um seletor com :not() para testar se você entendeu que o conteúdo dele conta.
B5. Refatoração. Reescreva o CSS abaixo eliminando toda a repetição por meio de variáveis, agrupamento de seletores e herança. Comente cada mudança e conte as linhas antes e depois.
Resultado esperado: a versão refatorada tem menos da metade das linhas, nenhuma cor repetida em literal, font-family declarada uma única vez (no body, aproveitando a herança) e um comentário por decisão tomada.
Dica
font-family é herdada: declarar no body elimina as seis repetições de uma vez. As três cores viram duas variáveis (--cor-superficie-escura e --cor-texto-claro) mais uma variação. E .header, .footer agrupados resolvem as duas primeiras regras em uma.
C1. Sistema de design documentado. Crie design-system.css e design-system.html no seu projeto autoral, documentando visualmente o seu sistema: paleta completa em variáveis (com o papel de cada cor), escala tipográfica (seis tamanhos, cada um com nome e valor em rem), escala de espaçamentos (três a cinco níveis), botões em quatro variações (primário, secundário, contorno, desabilitado) com os quatro estados cada, campos de formulário em todos os estados, um cartão e um alerta em quatro cores semânticas. Todos os contrastes precisam passar no WCAG AA — apresente a tabela de verificação.
Dica
Construa a página como um catálogo: uma seção por família de componente, com o exemplo visual à esquerda e o código que o produz à direita (dentro de <pre><code>). Essa página é o seu material de consulta pelo resto da trilha e entra bem no Marco 2 — e é exatamente assim que sistemas de design profissionais são documentados.
Existe um estilo que "não pega" em praticamente todo projeto de CSS — e quase sempre a culpa é da especificidade. Neste desafio você vai treinar o olho: dada uma lista de conflitos reais, prever quem vence antes de abrir o navegador e depois conferir. Quem acerta dez de dez raramente volta a perder tempo caçando esse tipo de bug.
Critérios de pronto
Um arquivo detetive.html com dez pares de regras conflitantes, cada par atingindo um elemento diferente da página.
Uma tabela em previsoes.md com, para cada par: os dois seletores, a especificidade calculada de cada um, a sua previsão e o resultado observado.
Pelo menos dois pares em que a decisão é tomada pela etapa 3 (ordem no arquivo), não pela especificidade.
Pelo menos um par envolvendo :not() e um envolvendo um seletor de atributo.
Uma captura de tela do painel Styles de um dos casos, com a regra vencida riscada, e uma frase explicando o que o DevTools está mostrando.
Pistas
Comece pelos casos fáceis (p × .classe) e vá subindo até #id × muitas classes — a surpresa está sempre nos empates.
:not(.a) não conta como pseudoclasse, mas o .a de dentro conta. Teste e confirme.
Um style="" inline vence qualquer seletor: inclua um par com ele e explique por que isso torna o atributo style uma má ideia em código de produção.
No painel Styles, as regras aparecem da mais específica para a menos específica, de cima para baixo. Use isso para conferir sua tabela em segundos.
Escolha três sites reais que você usa toda semana — um portal público, uma loja e uma rede social — e audite o contraste deles. Você vai encontrar texto cinza-claro sobre branco, botão desabilitado ilegível e link que só se distingue do texto pela cor (o que também reprova, por outro critério da WCAG). Depois, conserte: reescreva a paleta de um deles mantendo a identidade visual e passando em AA.
Critérios de pronto
Uma tabela com pelo menos doze pares de cor auditados (texto × fundo), a razão de contraste medida e o veredito (AA, AAA, reprova).
Pelo menos três reprovações encontradas, com captura de tela da tela original.
Uma paleta corrigida de um dos sites, em variáveis CSS, com cada cor ajustada apenas na luminosidade (o matiz da marca precisa ser preservado).
Uma página antes-depois.html mostrando os dois estados lado a lado, com conteúdo próprio.
Uma frase por correção explicando o que mudou e por quanto passou a razão de contraste.
Pistas
O verificador do WebAIM aceita colar os dois hexadecimais; para pegar os hexadecimais do site, use o conta-gotas do seletor de cores do DevTools.
O Lighthouse (aba do DevTools) tem uma auditoria de contraste que lista os elementos reprovados de uma vez — use como ponto de partida, não como conclusão.
Para corrigir mantendo a marca: converta a cor para hsl(), deixe H e S intactos e baixe (ou suba) o L de 5 em 5 pontos até passar.
Texto grande tem exigência menor (3:1). Antes de escurecer um título gigante, confira se ele já não passa.
Pegue este HTML sujo, cheio de classes que descrevem aparência, e reconstrua o mesmo visual sem tocar no HTML — usando apenas seletores de tipo, de atributo, combinadores e pseudoclasses. Depois reescreva o HTML como ele deveria ser e compare as duas folhas de estilo. O objetivo é sentir na prática o que um HTML semântico economiza de CSS.
componente-sujo.html
HTML
<divclass="caixa-branca-borda-cinza"><divclass="texto-azul-grande-negrito">Minicurso de Git e GitHub</div><divclass="texto-cinza-pequeno">Dia 2 · 19h · Laboratório 2</div><divclass="texto-normal">Do primeiro commit ao primeiro pull request, em duas horas.</div><divclass="linha-botoes"><spanclass="botao-azul"onclick="inscrever()">Inscrever-se</span><spanclass="botao-branco"onclick="detalhes()">Ver detalhes</span></div></div>
Critérios de pronto
Uma folha versao-a.css que reproduz o visual do componente sem alterar uma vírgula do HTML acima.
Um arquivo componente-limpo.html com a mesma informação em HTML semântico (article, h3, time, p, a ou button) e sem nenhuma classe que descreva aparência.
Uma folha versao-b.css para a versão limpa, com o mesmo resultado visual.
Uma comparação: número de linhas de cada folha, número de classes de cada HTML e três problemas de acessibilidade que a versão suja tinha e a limpa não tem.
Os dois componentes precisam ser utilizáveis por teclado — o que, na versão suja, exige explicar por que <span onclick> não é um botão.
Pistas
Na versão A, div:first-child, div:nth-child(2) e .linha-botoes span:first-child fazem quase todo o trabalho — e você vai perceber como isso é frágil.
Na versão B, article h3, article time e article .botao bastam, e o CSS sobrevive a uma reordenação do conteúdo.
Um <span> não é focável nem anunciado como botão. Liste isso, junto com a ausência de rótulo e a dependência de JavaScript inline.
Meça o tempo: quanto você demorou para escrever cada folha? Esse número também é um resultado.
Escolha a página inicial de um site cuja identidade visual você admira e reproduza a identidade — paleta, tipografia, escala de espaçamentos, estilo de botões, tratamento de links — em uma página com conteúdo totalmente diferente e seu. Não é copiar o layout (isso é a Aula 07): é extrair o sistema por trás da aparência e escrevê-lo em variáveis. Ao terminar, você vai olhar para qualquer site e enxergar as decisões, não os pixels.
Critérios de pronto
Um arquivo identidade.css com todo o sistema em variáveis: paleta com o papel de cada cor, escala tipográfica completa, escala de espaçamentos, raios e sombras.
Uma página clone.html com conteúdo próprio (pode ser sobre o seu projeto autoral) que usa apenas as variáveis — nenhum valor de cor ou espaçamento escrito solto.
Um documento de meia página analisando as decisões do site original: por que essas cores, por que essa escala, qual é a razão entre os tamanhos de fonte, quantos espaçamentos diferentes ele usa de verdade.
Uma tabela de contraste provando que a sua versão passa em AA em todos os pares de texto — inclusive se o original não passar (e diga se não passa).
A troca de tema: mudando apenas os valores dentro do :root, a página inteira assume outra identidade coerente. Entregue as duas capturas de tela.
Pistas
No DevTools, a aba Computed de um elemento mostra os valores finais; anote font-size, line-height, padding e color de uns dez elementos e procure os padrões. Você vai descobrir que quase todo site usa cinco ou seis valores, repetidos.
Muitos sites profissionais já expõem o sistema deles: procure por -- no painel Styles do elemento <html> e veja as variáveis do próprio site.
Para a escala tipográfica, divida cada tamanho pelo anterior: se der sempre perto de 1,25 ou de 1,333, você encontrou a razão.
O teste final do sistema é o item 5 dos critérios: se trocar o :root não muda tudo, é porque ainda há valores soltos. Procure-os com Ctrl+F por # no seu CSS.
Parte 1 — Leitura (20 min). SILVA, Maurício Samy. Criando sites com HTML, capítulos de seletores e cascata. Na MDN em pt-BR: "Seletores CSS" e "Cascata, especificidade e herança" (links em Para aprofundar). Anote um seletor que apareceu na leitura e não nesta aula.
Parte 2 — Produção (30 min). Hoje fecha o Marco 1 (instruções completas logo abaixo). Além dele, produza o exercício B5 (refatoração com variáveis) aplicado ao seu projeto autoral: o bloco :root completo do seu sistema de design, com no mínimo dez variáveis, e a folha de estilo reorganizada nas sete seções.
Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: traga um conflito de estilo real que você enfrentou (no site do evento ou no seu projeto), o cálculo de especificidade dos seletores envolvidos e como resolveu sem!important. Se puder, compare a solução com a de outra pessoa.
Critério de pronto: o css/estilo.css do seu projeto abre com o comentário das sete seções, tem um :root com dez ou mais variáveis, nenhum valor de cor escrito solto fora do :root, nenhum !important e nenhum id usado como seletor de estilo.
Guarde no seu repositório: commit + push (ou a pasta do projeto, se ainda não usa Git).
Escopo. Ao fim da Unidade 1, o seu projeto autoral precisa ser um site em HTML puro, sem CSS, sobre o tema que você definiu na Aula 01. O escopo são os itens não-CSS do Checkpoint da Aula 05: as cinco páginas, o seccionamento semântico, a hierarquia de títulos, os <article>, a página de contato e a validação no W3C. Os itens de CSS daquele Checkpoint — css/estilo.css ligado nas páginas, o bloco :root, a regra base de imagens e o .botao com :hover/:focus-visible — não entram neste marco; eles fecham o Marco 2, sobre este mesmo site. A página vai parecer crua, e isso é intencional: o que este marco mede é a estrutura. A estilização é o Marco 2, e a interatividade é o Marco 3 — mesmo site, três camadas.
Requisitos.
#
Requisito
Onde foi estudado
1
Mínimo de 5 páginas interligadas por caminhos relativos, sem link quebrado
Aula 02
2
Estrutura HTML5 válida em todas as páginas: <!DOCTYPE>, lang="pt-BR", charset, viewport, description e title próprios
Aula 02
3
Seccionamento semântico: header, nav, main (um por página), section, article, aside, footer
Aulas 02 e 05
4
Hierarquia de títulos correta, com um único <h1> por página e sem pular níveis
Aula 02
5
Elementos de texto aplicados com significado: strong, em, blockquote com cite, time e q (o <abbr> é opcional e conta como item extra)
Aula 02
6
Os três tipos de lista em uso: ul, ol e dl
Aulas 02 e 04
7
Menu de navegação como <ul> dentro de <nav aria-label>, com aria-current="page" na página atual
Aula 04
8
Uma tabela de dados com caption, thead, tbody, tfoot e th scope
Aula 02
9
Um formulário completo: mínimo de 10 campos de tipos diferentes, <label> associado a cada um, fieldset/legend, <select>, <textarea> e validação nativa
Aulas 03 e 04
10
Imagens com alt adequado a cada situação, ao menos uma em figure/figcaption, com width/height e loading="lazy" abaixo da dobra
Aula 04
11
Um vídeo ou áudio incorporado com controls e fallback; se vídeo, com <track kind="captions">
Aula 04
12
Ao menos um link externo com target="_blank" e rel="noopener noreferrer"
Aula 02
13
Zero erros no validador do W3C, em todas as páginas
Aula 02
Checklist de qualidade. O que separa um projeto pronto de um feito às pressas na última hora:
Validação W3C sem erros em nenhuma das cinco páginas.
Seccionamento semântico coerente e hierarquia de títulos sem saltos.
Formulário completo e acessível: todo campo com rótulo associado, agrupamento em fieldset, validação nativa funcionando.
Tabela, listas e elementos de texto usados pelo que significam, não só pela aparência.
Mídias com alt que descreve o que a imagem comunica (não o nome do arquivo), legendas de vídeo funcionando, width/height e loading="lazy" presentes.
Navegação funcional e idêntica entre todas as páginas, sem link quebrado.
Arquivos e pastas em minúsculas, sem espaço nem acento; nomes que dizem o que cada arquivo é.
Nenhum <link rel="stylesheet">, <style> ou atributo style — este marco é só estrutura.
Como saber que está pronto.
Rode cada página no validador do W3C (upload de arquivo ou por URL, se já estiver publicada) e confira "Document checking completed. No errors or warnings to show."
No Console do DevTools, document.querySelectorAll("main").length deve retornar 1 em cada página.
Navegue pelo site inteiro só com Tab e Enter: todo link e todo campo do formulário precisa ser alcançável e visível em foco.
Peça para alguém (colega, familiar, ou você mesmo depois de um dia) abrir as cinco páginas sem explicação prévia e apontar qual link parece quebrado ou qual página parece fora do lugar.
Use IA para entender um erro do validador ou revisar uma dúvida de sintaxe — não para gerar o site inteiro no seu lugar. Se você não conseguir explicar uma linha do seu próprio HTML, ela ainda não é sua.
Google Fonts: https://fonts.google.com/ — escolha a fonte e copie os <link> já prontos, com os pesos que você realmente vai usar.
CSS Specificity Calculator: https://specificity.keegan.st/ — cole um seletor e veja o trio (A, B, C); ótimo para conferir o Laboratório A3.
SILVA, Maurício Samy. Criando sites com HTML: sites de alta qualidade com HTML e CSS. Novatec, 2008 — a parte de CSS, capítulos de seletores e cascata.
TERUEL, Evandro C. HTML 5 — Guia Prático. Saraiva, 2014 — capítulo de folhas de estilo (Minha Biblioteca).
MILETTO, Evandro M.; BERTAGNOLLI, Silvia C. Desenvolvimento de software II. Bookman, 2014 — capítulo sobre camada de apresentação em aplicações web.
Na próxima aula o site deixa de ser uma pilha de blocos: você vai aprender posicionamento, Flexbox e CSS Grid, decidir qual dos dois cada problema pede e construir o esqueleto de layout definitivo do site do evento, com um menu de navegação fixo, acessível e com link de salto.
[ ] Pasta site-evento/ com as cinco páginas em HTML semântico da Unidade 1 (index.html, programacao.html, inscricao.html, palestrantes.html, contato.html), validadas no W3C.
[ ] site-evento/css/estilo.css criado na Aula 06, com o reset box-sizing: border-box, o bloco :root de variáveis e a folha organizada na ordem recomendada (reset → variáveis → base → layout → componentes → utilitários → media queries).
[ ] VS Code com a extensão Live Server e um navegador com DevTools (Chrome ou Firefox).
[ ] Revisar da Aula 06: especificidade de seletores, display: block versus display: inline, o modelo de caixa (margin, border, padding) e var(--nome).
Na aula passada você estilizou o site do evento: sistema de design em variáveis, tipografia, cores, botões e tabela. Mas as páginas ainda são uma pilha de blocos de cima para baixo — o cabeçalho não fica ao lado do logo, os cartões de palestra não formam uma grade e o rodapé sobe quando a página tem pouco conteúdo. Hoje você aprende as duas ferramentas que resolvem isso — Flexbox e Grid — e constrói o esqueleto de layout definitivo do site, com um menu de navegação que qualquer pessoa consegue usar.
1.1 O que o navegador faz quando você não diz nada¶
Antes de qualquer CSS de layout, o navegador já tem um plano: o fluxo normal. Elementos de bloco (div, p, section, h1) são empilhados de cima para baixo, cada um ocupando toda a largura disponível. Elementos em linha (a, strong, span, img) são dispostos lado a lado, da esquerda para a direita, quebrando linha quando falta espaço — exatamente como as palavras de um parágrafo.
Pense no fluxo normal como a água escorrendo por uma calha: cada caixa cai na primeira posição livre. Tudo o que você vai aprender hoje — position, Flexbox, Grid — é uma forma de desviar a água: tirar um elemento do fluxo ou trocar as regras de como os filhos de um contêiner se organizam.
🧠 Você sabia?
Toda vez que o navegador precisa recalcular a posição e o tamanho das caixas, ele faz um reflow (ou layout). Mudar a largura de um elemento no topo da página pode obrigar o navegador a recalcular a posição de todos os elementos abaixo dele — em uma página grande, isso custa milissegundos que se transformam em travadas visíveis. É por isso que, na Aula 09, você vai aprender a animar só propriedades que não disparam reflow. Por enquanto, guarde a ideia: layout é a etapa cara do desenho de uma página.
1.2 position: cinco maneiras de dizer "fique aqui"¶
A propriedade position define em relação a quê um elemento é posicionado e se ele ainda ocupa espaço no fluxo. As propriedades top, right, bottom e left só têm efeito quando position é diferente de static.
Exemplo — os cinco valores lado a lado
CSS
.a{position:static;}/* padrão; ignora top/right/bottom/left */.b{position:relative;top:10px;left:20px;}/* desloca-se da própria posição original */.c{position:absolute;top:0;right:0;}/* canto do ancestral posicionado mais próximo */.d{position:fixed;bottom:20px;right:20px;}/* preso à janela; não rola com a página */.e{position:sticky;top:0;}/* normal até encostar no topo; então gruda */
Valor
Referência de posicionamento
Ocupa espaço no fluxo?
static
Nenhuma — segue o fluxo normal
Sim
relative
A própria posição original
Sim (o espaço original é mantido, mesmo deslocado)
absolute
O ancestral posicionado mais próximo (ou o <html>)
Não — sai do fluxo
fixed
A janela do navegador (viewport)
Não — sai do fluxo
sticky
Fluxo normal até atingir o limite (top, bottom); depois, o contêiner de rolagem
Sim
A regra de ouro do absolute. Um elemento absolute procura, subindo na árvore, o ancestral mais próximo cujo position seja diferente de static. Se não encontrar nenhum, ele se posiciona em relação à página inteira — e vai parar no canto do documento, longe de onde você queria. Por isso a receita clássica é: position: relative no pai (que não se move, porque sem top/left o relative não desloca nada) e position: absolute no filho.
No site do evento, é assim que o selo "Esgotado" fica preso no canto do cartão de um minicurso:
site-evento/css/estilo.css — trecho
CSS
.cartao{position:relative;/* vira a referência do selo, sem se mover */}.cartao__selo{position:absolute;/* sai do fluxo e ancora no cartão */top:8px;right:8px;}
sticky merece atenção especial. Ele é o híbrido: o elemento rola normalmente com a página até que a distância definida (top: 0) seja atingida; a partir daí, ele "gruda" e a página continua rolando por baixo. É o comportamento perfeito para um cabeçalho: não rouba espaço no topo quando a página carrega, mas fica sempre à mão. Duas condições para funcionar: declarar top (ou bottom) e nenhum ancestral com overflow diferente de visible — você vai reencontrar essa segunda condição na tabela de erros comuns.
💡 Dicafixed e sticky parecem iguais no cabeçalho, mas diferem no resto: fixed sai do fluxo (o conteúdo abaixo sobe e fica escondido atrás do cabeçalho, obrigando você a compensar com padding-top no body); sticky continua ocupando o espaço original. Prefira sticky para cabeçalhos.
Quando dois elementos posicionados se sobrepõem, quem fica na frente? Por padrão, o que vem depois no HTML. z-index muda isso: valores maiores ficam à frente. Só funciona em elementos com position diferente de static (e em itens de Flexbox e Grid).
⚠️ Atenção
Evite z-index: 9999. Quando cada componente "grita mais alto" que o anterior, ninguém sabe mais quem está na frente de quem. Defina uma escala do projeto e documente-a na folha de estilo. No site do evento: 100 para o cabeçalho fixo, 900 para sobreposições escuras, 1000 para modais e para o link de salto.
🔎 Por baixo do capôz-index não é uma escala global. Todo elemento posicionado com z-index diferente de auto cria um contexto de empilhamento: os filhos dele só competem entre si, e o grupo inteiro é comparado com os vizinhos usando o z-index do pai. É por isso que, às vezes, um z-index: 9999 "não funciona": o elemento está dentro de um contexto com z-index: 1, e o contexto inteiro está atrás de outro com z-index: 2. opacity menor que 1, transform e filter também criam contextos de empilhamento — lembre-se disso na Aula 09.
float foi criado com um único propósito: fazer texto envolver uma imagem, como em uma revista. Como durante mais de dez anos não existia nada melhor, a comunidade usou float para construir colunas, menus e páginas inteiras — com gambiarras como clearfix e contêineres que colapsavam. Hoje não use float para layout: Flexbox e Grid resolvem tudo isso de forma previsível. Ainda é válido para o caso original:
CSS
figure{float:left;margin:016px16px0;}/* texto envolve a figura */.limpa{clear:both;}/* próximo elemento não envolve nada */
Você vai encontrar float em código antigo, em tutoriais desatualizados e em respostas de fórum. Saiba ler; não escreva.
1.5 overflow: o que acontece quando o conteúdo não cabe¶
CSS
overflow:visible;/* padrão: o conteúdo transborda e aparece fora da caixa */overflow:hidden;/* corta o excesso */overflow:auto;/* barra de rolagem só quando precisar */overflow:scroll;/* barra sempre visível */overflow-x:auto;/* controle por eixo */overflow-y:hidden;
Um uso legítimo no site do evento: uma tabela de programação larga, dentro de um contêiner com overflow-x: auto, rola horizontalmente no celular em vez de estourar a página inteira.
🔬 Investigue
Abra index.html com o Live Server e o DevTools (F12). No painel Elements, selecione o <header> e, na aba Styles, adicione position: sticky; top: 0; clicando no bloco element.style. Role a página: o cabeçalho gruda. Agora troque para position: fixed: veja o conteúdo "pular" para cima, porque o cabeçalho saiu do fluxo e deixou de ocupar espaço. Por fim, adicione overflow-x: hidden ao <body> com o sticky de volta — e observe que o cabeçalho para de grudar. Você acabou de encontrar o bug de sticky mais comum da web.
Flexbox distribui os filhos de um contêiner ao longo de um eixo — uma linha ou uma coluna — e resolve, com poucas propriedades, o que antes exigia truques: alinhar verticalmente, distribuir espaço sobrando, fazer itens crescerem ou encolherem, mudar a ordem visual.
CSS
.contêiner{display:flex;}
Só essa linha já transforma todos os filhos diretos em flex items e os coloca lado a lado. Os netos não são afetados — Flexbox age em um nível só. Isso importa: se você quiser um Flexbox dentro de um item, o item precisa ser, ele mesmo, um contêiner display: flex.
Com flex-direction: column, os eixos trocam de papel: o principal passa a ser vertical. Todas as propriedades a seguir são definidas em relação a esses eixos, não em relação a "horizontal" e "vertical". Essa é a fonte da confusão mais comum com Flexbox — e a razão de justify-content alinhar horizontalmente em uma linha, mas verticalmente em uma coluna.
.contêiner{display:flex;flex-direction:row;/* row | row-reverse | column | column-reverse */flex-wrap:wrap;/* nowrap (padrão) | wrap | wrap-reverse */justify-content:center;/* distribui no EIXO PRINCIPAL *//* flex-start | flex-end | center | space-between | space-around | space-evenly */align-items:center;/* alinha no EIXO TRANSVERSAL *//* stretch (padrão) | flex-start | flex-end | center | baseline */align-content:center;/* alinha as LINHAS entre si (só com wrap e várias linhas) */gap:16px;/* espaço entre itens — use isto, não margin */row-gap:16px;column-gap:24px;}
Valor de justify-content
Resultado
flex-start
Todos encostados no início do eixo (padrão)
flex-end
Todos encostados no fim
center
Agrupados no centro
space-between
Primeiro na ponta, último na ponta, o resto distribuído por igual
space-around
Espaço igual em volta de cada item (as bordas ficam com metade)
space-evenly
Espaços exatamente iguais, inclusive nas bordas
Repare em align-items: stretch, o padrão: os itens esticam para ocupar toda a altura do contêiner. É por isso que, em uma linha de cartões flex, todos ficam com a mesma altura sem nenhum esforço — algo que era quase impossível com float.
A centralização perfeita. Centralizar um elemento nos dois eixos foi, por vinte anos, um dos problemas mais frustrantes do CSS. Hoje são três linhas:
CSS
.centro{display:flex;justify-content:center;/* eixo principal */align-items:center;/* eixo transversal */}
🧠 Você sabia?
O primeiro rascunho do Flexbox é de 2009 — mas ele só se tornou utilizável em todos os navegadores por volta de 2015. Foram três sintaxes diferentes no caminho (display: box, depois display: flexbox, por fim display: flex), cada uma com prefixos de fornecedor (-webkit-, -ms-), e um Internet Explorer que implementou a versão intermediária e nunca atualizou. Se você encontrar display: -webkit-box em código antigo, é um fóssil dessa história. A lição: uma especificação só vira ferramenta quando todos os navegadores concordam — e isso leva tempo.
As propriedades do contêiner organizam o conjunto; as dos itens dizem como cada um reage ao espaço disponível.
CSS
.item{flex-grow:1;/* fator de crescimento quando sobra espaço (padrão 0: não cresce) */flex-shrink:1;/* fator de encolhimento quando falta espaço (padrão 1: encolhe) */flex-basis:200px;/* tamanho inicial antes de distribuir (padrão auto: o tamanho do conteúdo) */flex:1;/* atalho = flex: 1 1 0 — todos os itens com o mesmo tamanho */flex:00250px;/* largura fixa: não cresce, não encolhe */flex:11280px;/* começa com 280px, cresce e encolhe conforme o espaço */align-self:flex-end;/* sobrescreve align-items só neste item */order:2;/* muda a ordem visual (padrão 0; menores vêm primeiro) */}
Como ler flex: 1 1 280px: "comece com 280 px; se sobrar espaço, cresça na proporção 1; se faltar, encolha na proporção 1". Quando todos os irmãos têm flex: 1, o espaço é dividido igualmente. Quando um tem flex: 2 e os outros flex: 1, ele recebe o dobro do espaço sobrando — não o dobro do tamanho total.
⚠️ Atençãoorder muda apenas a ordem visual, não a ordem do DOM. Quem navega com Tab ou com um leitor de tela continua seguindo a ordem do HTML — e, se a tela mostra "Contato" antes de "Início" mas o foco vai na ordem contrária, a experiência fica confusa. Use order com parcimônia e nunca para inverter a sequência lógica de navegação.
/* 1. Barra de navegação: logo à esquerda, menu à direita, tudo alinhado no centro */.cabecalho__interno{display:flex;justify-content:space-between;align-items:center;padding:16px32px;}/* 2. Menu horizontal com espaçamento uniforme */.menu{display:flex;gap:24px;list-style:none;}/* 3. Linha de cartões que quebra sozinha quando falta espaço */.cartoes-flex{display:flex;flex-wrap:wrap;gap:24px;}.cartoes-flex.cartao{flex:11280px;/* mínimo 280px; cresce para preencher; quebra linha quando não couber */}/* 4. Rodapé sempre no fim da tela, mesmo com pouco conteúdo */body{display:flex;flex-direction:column;min-height:100vh;}main{flex:1;/* o main absorve todo o espaço sobrando; o rodapé é empurrado para baixo */}
O padrão 4 resolve um problema clássico do site do evento: a página contato.html tem pouco conteúdo, e o rodapé ficava flutuando no meio da tela. Com body em coluna e main { flex: 1 }, o rodapé vai para o fim da janela — e, quando a página cresce, ele desce junto.
🔬 Investigue
No DevTools, painel Elements, todo elemento com display: flex ganha um selo cinza escrito flex ao lado da tag. Clique nele: o navegador desenha o eixo principal, o contorno de cada item e os espaços de gap. Agora abra a aba Layout (Chrome) ou o painel Flexbox (Firefox): você pode ligar o overlay de vários contêineres ao mesmo tempo e ver, ao vivo, o que justify-content: space-between está fazendo com o espaço. Troque o valor na aba Styles e observe o desenho mudar.
Flexbox pensa em uma fila. Grid pensa em uma tabela invisível: você declara colunas e linhas, e os filhos ocupam as células. É a ferramenta certa para a estrutura geral de uma página — cabeçalho, lateral, conteúdo, rodapé — e para qualquer coisa que precise alinhar nos dois eixos, como uma galeria.
CSS
.grade{display:grid;grid-template-columns:200px1fr200px;/* três colunas: fixa, elástica, fixa */grid-template-rows:auto1frauto;/* três linhas: conteúdo, elástica, conteúdo */gap:20px;}
A unidade fr (fraction) representa uma fração do espaço que sobra depois de descontar tamanhos fixos e gaps. Em um contêiner de 1000 px com 200px 1fr 200px e gap: 20px, sobram 1000 − 200 − 200 − 40 = 560 px para a coluna do meio. Com 1fr 2fr, o espaço livre é dividido em três partes: uma para a primeira coluna, duas para a segunda.
grid-template-columns:200px200px200px;/* três fixas */grid-template-columns:1fr1fr1fr;/* três iguais e elásticas */grid-template-columns:repeat(3,1fr);/* idem, abreviado */grid-template-columns:250px1fr;/* lateral fixa + conteúdo elástico */grid-template-columns:repeat(auto-fit,minmax(250px,1fr));/* responsivo sem media query */
💡 Dica
A linha repeat(auto-fit, minmax(250px, 1fr)) é uma das mais poderosas do CSS moderno. Leia-a assim: "crie quantas colunas couberem (auto-fit), cada uma com no mínimo 250 px e no máximo uma fração igual do espaço (minmax(250px, 1fr))". Em uma janela de 1100 px cabem quatro colunas; em 800 px, três; em um celular, uma. A grade se reorganiza sozinha — sem nenhuma media query. É assim que os cartões de programação e de palestrantes do site do evento vão se comportar.
auto-fit ou auto-fill? Os dois criam quantas colunas couberem. A diferença aparece quando há poucos itens: auto-fill mantém as colunas vazias (três cartões em uma grade de cinco colunas ficam encolhidos à esquerda); auto-fit colapsa as faixas vazias e deixa os itens existentes esticarem para ocupar a largura toda. Para cartões, quase sempre você quer auto-fit.
Grid numera as linhas-guia (as divisórias), não as células. Uma grade de três colunas tem quatro linhas-guia verticais: 1, 2, 3 e 4.
CSS
.item{grid-column:1/3;/* da linha-guia 1 até a 3 — ocupa duas colunas */grid-column:span2;/* ocupa duas colunas a partir de onde estiver */grid-row:2/4;/* da linha-guia 2 até a 4 — ocupa duas linhas */grid-area:2/1/4/3;/* linha-início / coluna-início / linha-fim / coluna-fim */}
Itens sem posição explícita são colocados automaticamente na próxima célula livre, na ordem do HTML. Isso é o que faz auto-fit funcionar sem que você precise posicionar cartão por cartão.
3.4 Áreas nomeadas — o layout desenhado no código¶
Números de linha-guia funcionam, mas ninguém lê grid-area: 2 / 1 / 4 / 3 e visualiza a página. Áreas nomeadas resolvem isso: você desenha o layout com palavras.
Cada string é uma linha da grade; cada palavra, uma célula. Repetir o nome em células vizinhas faz a área se estender (o cabeçalho ocupa as duas colunas). Um ponto (.) marca uma célula vazia. Para reorganizar tudo no celular, basta redesenhar as áreas dentro de uma media query — uma coluna só, cada área em uma linha — sem tocar no HTML. Você fará exatamente isso na Aula 08.
As propriedades de alinhamento têm os mesmos nomes do Flexbox, mas com uma camada a mais: alinhar os itens dentro das células e alinhar a grade inteira dentro do contêiner.
CSS
.grade{justify-items:center;/* cada item, horizontalmente, dentro da sua célula */align-items:center;/* cada item, verticalmente, dentro da sua célula */place-items:center;/* atalho: align-items + justify-items */justify-content:center;/* a grade inteira, horizontalmente, no contêiner */align-content:center;/* a grade inteira, verticalmente, no contêiner */}
📌 Vale gravar
Em Grid, justify-* sempre se refere ao eixo horizontal (colunas) e align-* ao vertical (linhas). Em Flexbox, justify-content segue o eixo principal, que muda com flex-direction. Essa diferença cai no dia a dia — vale gravar.
🔬 Investigue
Selecione no DevTools qualquer elemento com display: grid; ele ganha um selo grid. Clique nele e vá à aba Layout: ative "Show line numbers" e "Show area names". O navegador desenha as linhas-guia numeradas e escreve o nome de cada área por cima da página. Redimensione a janela com o overlay ligado e veja auto-fit criando e removendo colunas. Esse overlay vai economizar horas de tentativa e erro — use-o sempre que um item cair na célula errada.
A pergunta certa não é "qual é melhor", e sim "quantas dimensões o problema tem".
Use Flexbox quando
Use Grid quando
O layout é uma linha ou uma coluna
Você precisa controlar linhas e colunas
O tamanho dos itens (conteúdo) define a distribuição
O contêiner define a estrutura, e o conteúdo se encaixa
Barra de navegação, grupo de botões, cartões em linha, campo de busca com botão
Estrutura da página, galerias, dashboards, formulários em colunas
Alinhar coisas dentro de um componente
Posicionar os componentes na página
Eles não competem — se combinam. A arquitetura padrão de um site hoje é: Grid para a estrutura da página (onde ficam cabeçalho, lateral, conteúdo, rodapé) e Flexbox para o interior de cada componente (o que há dentro do cabeçalho, de um cartão, de um formulário). É assim que o site do evento vai ficar ao fim desta aula.
Dois testes rápidos para decidir:
Você consegue descrever o layout com uma frase que tenha "ao lado" ou "abaixo", mas não os dois? Flexbox. ("Logo ao lado do menu"; "campos empilhados abaixo do outro".)
Você precisa que a segunda linha se alinhe com a primeira? Grid. (Uma galeria em que as colunas precisam bater; um formulário em que os rótulos alinham nas duas linhas.)
O menu é o componente que mais concentra erros de acessibilidade em projetos reais. Ele também é a primeira coisa que todo visitante toca. Vale construí-lo com cuidado.
<headerclass="cabecalho"><divclass="container cabecalho__interno"><ahref="index.html"class="logo"><imgsrc="img/logo-sasi.svg"alt="Página inicial — Semana Acadêmica de Sistemas de Informação"></a><navaria-label="Principal"><ulclass="menu"><li><ahref="index.html"aria-current="page">Início</a></li><li><ahref="programacao.html">Programação</a></li><li><ahref="palestrantes.html">Palestrantes</a></li><li><ahref="contato.html">Contato</a></li><li><ahref="inscricao.html"class="menu__cta">Inscreva-se</a></li></ul></nav></div></header>
⚠️ Atenção
Este cabeçalho substitui o <header id="a07-topo"> das Aulas 02 a 06 (logo solto, <h1> do site, parágrafo de local e <nav> sem classe). Apague o bloco antigo inteiro nas cinco páginas e cole este no lugar. Junto com ele vão embora duas coisas: o parágrafo "Três noites de outubro · Auditório Central", que passa a viver no herói da página inicial e no rodapé, e o <h1> do cabeçalho — a partir de hoje o <h1> é o título da página, dentro do <main> (o que era <h2>Programação</h2> vira <h1>Programação</h1>, e assim por diante nas cinco páginas). O nome do evento continua acessível: ele está no alt do logo e no <title>.
Três pontos que valem nota:
O menu é uma lista (ul/li). Leitores de tela anunciam "lista com 5 itens" ao entrar nela — o usuário sabe o tamanho da navegação antes de percorrê-la. Uma sequência de <a> soltos não dá essa informação.
<nav aria-label="Principal"> distingue esta navegação de outras que a página pode ter (a do rodapé, uma trilha de "você está aqui", os filtros da programação). Sem o rótulo, o leitor de tela anuncia só "navegação", e o usuário não sabe qual.
aria-current="page" marca a página atual semanticamente, não só com cor. Quem não enxerga a cor (por daltonismo ou por usar leitor de tela) recebe a mesma informação. Em cada página do site, o atributo vai no link correspondente — e o CSS usa o atributo como seletor, então o destaque visual vem de graça.
O alt do logo diz para onde o link leva ("Página inicial") e o que a imagem é, porque um logo dentro de um link é, antes de tudo, um link. Na Aula 04 esse mesmo logo tinha alt="", porque o <h1> ao lado já dizia o nome do evento; agora que o logo virou link e o <h1> saiu do cabeçalho, o alt volta a carregar informação. O alt correto depende do contexto, não do arquivo.
Uma mudança de ordem, declarada. O menu passa de cinco itens em ordem de "índice" (Início, Programação, Inscrição, Palestrantes, Contato) para quatro itens de navegação mais um botão de ação no fim: "Inscreva-se". A decisão é de experiência de uso — a ação que o site quer que a pessoa faça deve estar no fim da linha de leitura e visualmente distinta do resto, não perdida no meio do menu. O rótulo muda de "Inscrição" (substantivo, nome de página) para "Inscreva-se" (verbo, chamada para ação) pelo mesmo motivo. O arquivo continua sendo inscricao.html.
.cabecalho{background:var(--cor-superficie);border-bottom:1pxsolidvar(--cor-borda);position:sticky;top:0;z-index:100;}.cabecalho__interno{display:flex;align-items:center;justify-content:space-between;gap:var(--espaco-grande);padding-block:var(--espaco-medio);}.logoimg{display:block;/* remove o espaço fantasma abaixo da imagem em linha */height:40px;width:auto;}.menu{display:flex;align-items:center;gap:1.5rem;list-style:none;}.menua{position:relative;/* referência para o sublinhado (::after) */display:block;padding:.5rem0;color:var(--cor-texto);text-decoration:none;font-weight:500;}/* Sublinhado que cresce — a Aula 09 explica transform e transition em detalhe */.menua::after{content:"";position:absolute;left:0;bottom:0;width:100%;height:2px;background:var(--cor-primaria);transform:scaleX(0);transition:transform200msease;}.menua:hover::after,.menua:focus-visible::after,.menua[aria-current="page"]::after{transform:scaleX(1);}.menua[aria-current="page"]{color:var(--cor-primaria);font-weight:600;}/* O botão-pílula de inscrição */.menua.menu__cta{padding:.5rem1.25rem;background:var(--cor-primaria);color:#fff;border-radius:999px;}.menua.menu__cta::after{display:none;/* o botão não recebe o sublinhado */}.menua.menu__cta:hover,.menua.menu__cta:focus-visible{background:var(--cor-secundaria);}
💡 Dica
Repare no seletor .menu a.menu__cta em vez de só .menu__cta. Como vimos na Aula 06, .menu a tem especificidade (0,1,1) e vence .menu__cta (0,1,0) — se você escrevesse só a classe, o color: #fff seria ignorado e o botão ficaria com texto escuro sobre fundo escuro. .menu a.menu__cta tem (0,2,1) e ganha. Quando um estilo "não pega", a especificidade é a primeira suspeita.
Duas decisões pequenas com efeito grande:
:focus-visible ao lado de :hover em todos os estados. Quem navega por teclado precisa ver onde está — e o contorno padrão do navegador, que você talvez tenha removido com outline: none em algum momento, era exatamente isso. Nunca remova o outline sem oferecer uma alternativa visível.
display: block nos links aumenta a área clicável para o padding inteiro, e não só para o texto. No celular, isso é a diferença entre acertar e errar o toque.
Quem navega por teclado não deveria ter que percorrer o menu inteiro, em toda página, para chegar ao conteúdo. O link de salto é o primeiro elemento do <body>: invisível para quem usa mouse, é o primeiro que aparece ao pressionar Tab.
site-evento/index.html — logo após <body> e o início do <main>
HTML
<body><ahref="#conteudo"class="salto">Pular para o conteúdo</a><headerclass="cabecalho"><divclass="container cabecalho__interno"><ahref="index.html"class="logo"><imgsrc="img/logo-sasi.svg"alt="Página inicial — Semana Acadêmica de Sistemas de Informação"></a><navaria-label="Principal"><ulclass="menu"><li><ahref="index.html"aria-current="page">Início</a></li><li><ahref="programacao.html">Programação</a></li><li><ahref="palestrantes.html">Palestrantes</a></li><li><ahref="contato.html">Contato</a></li><li><ahref="inscricao.html"class="menu__cta">Inscreva-se</a></li></ul></nav></div></header><mainid="conteudo"tabindex="-1"><h1>Semana Acadêmica de Sistemas de Informação</h1></main></body>
.salto{position:absolute;left:-9999px;/* fora da tela, mas presente para o teclado e o leitor de tela */top:1rem;padding:.75rem1.25rem;background:var(--cor-primaria);color:#fff;border-radius:var(--raio-borda);z-index:1000;}.salto:focus{left:1rem;/* ao receber foco, entra na tela */}
O tabindex="-1" no <main> permite que ele receba foco ao ser alvo do link (sem entrar na sequência normal do Tab), o que faz o leitor de tela começar a ler dali. Sem isso, alguns navegadores rolam até o conteúdo, mas o foco continua no link de salto.
🔎 Por baixo do capô
Por que left: -9999px e não display: none? Porque display: noneremove o elemento da árvore de acessibilidade: ele deixa de existir para o teclado e para o leitor de tela. Deslocar para fora da tela mantém o elemento "vivo" — invisível, mas focável. É o mesmo raciocínio que você vai reencontrar na Aula 09, quando display não puder ser animado.
Esse menu funciona perfeitamente em telas largas. No celular, cinco links não cabem lado a lado — e a solução é o menu "hambúrguer", que abre e fecha. A versão responsiva do menu fica para a Aula 08, porque depende de media queries e de um pouco de raciocínio mobile first que você ainda não viu. Hoje, o menu quebra linha de forma aceitável graças ao flex-wrap que você vai adicionar na Mão na massa.
💻 Mão na massa — Esqueleto do site do evento com Grid e Flexbox¶
Você vai aplicar tudo isso no site da Semana Acadêmica de Sistemas de Informação. Ao final, as cinco páginas terão o mesmo cabeçalho fixo com menu acessível, o rodapé no fim da tela, a página inicial com um herói em duas colunas, a programação com lateral de filtros e grade de cartões, e os palestrantes em uma galeria que se reorganiza sozinha.
Abra site-evento/css/estilo.css. Na seção 2 (variáveis), acrescente três variáveis ao bloco :root que você criou na Aula 06 e documente a escala de empilhamento em um comentário.
/* 2. Variáveis */:root{--cor-primaria:#0b3d5c;--cor-secundaria:#1a7fb5;--cor-texto:#333333;--cor-fundo:#f7f9fb;--cor-superficie:#ffffff;/* fundo de cabeçalho e cartões */--cor-borda:#dfe6ec;/* linhas divisórias */--fonte-base:"Inter",Arial,sans-serif;--espaco-pequeno:8px;--espaco-medio:16px;--espaco-grande:32px;--raio-borda:8px;--sombra-cartao:02px8pxrgba(0,0,0,0.1);--largura-max:1100px;/* largura máxima do conteúdo */}/* Escala de z-index do projeto: 100 cabeçalho fixo 900 sobreposições escuras (overlay) 1000 modais e link de salto */
Passo 2 — Rodapé no fim da tela e o contêiner central¶
Na seção 4 (layout), apague as três declarações do main que a Aula 06 escreveu (max-width, margin e padding) e ponha no lugar o padrão do body em coluna mais a classe .container reutilizável. A largura máxima continua sendo a mesma de sempre — 1100 px —, só que agora ela mora na variável --largura-max e é a .container que a aplica.
site-evento/css/estilo.css — seção 4 (layout), início
CSS
/* 4. Layout */body{display:flex;flex-direction:column;min-height:100vh;}main{flex:1;/* empurra o rodapé para o fim da janela */}.container{width:100%;max-width:var(--largura-max);margin-inline:auto;/* centraliza horizontalmente */padding-inline:var(--espaco-medio);}
margin-inline e padding-inline são as versões "lógicas" de margin-left/margin-right — funcionam também em idiomas escritos da direita para a esquerda. Use-as sempre que a intenção for "dos lados".
Onde a .container entra no HTML. Na Aula 05 ela era uma <div> dentro de cada região; na Aula 06 a <div> saiu de dentro do <main>. Hoje ela volta ao conteúdo principal, mas como classe do próprio elemento, sem <div> extra. Percorra as cinco páginas e garanta que cada uma tenha exatamente um contêiner no conteúdo:
Página
Onde vai a .container
index.html
na <section class="container hero"> (Passo 5)
programacao.html
no <main id="a07-conteudo" tabindex="-1" class="container pagina"> (Passo 6)
palestrantes.html
no <main id="a07-conteudo" tabindex="-1" class="container"> (Passo 7)
inscricao.html
no <main id="a07-conteudo" tabindex="-1" class="container">
contato.html
no <main id="a07-conteudo" tabindex="-1" class="container">
Sem esse passo, inscricao.html e contato.html ficam com o conteúdo colado nas bordas da janela em monitores largos — e é o erro mais comum desta aula.
Coloque o cabeçalho da seção 5.1 nas cinco páginas, trocando o aria-current="page" para o link da página correspondente. Em programacao.html, por exemplo:
site-evento/programacao.html — cabeçalho
HTML
<ahref="#conteudo"class="salto">Pular para o conteúdo</a><headerclass="cabecalho"><divclass="container cabecalho__interno"><ahref="index.html"class="logo"><imgsrc="img/logo-sasi.svg"alt="Página inicial — Semana Acadêmica de Sistemas de Informação"></a><navaria-label="Principal"><ulclass="menu"><li><ahref="index.html">Início</a></li><li><ahref="programacao.html"aria-current="page">Programação</a></li><li><ahref="palestrantes.html">Palestrantes</a></li><li><ahref="contato.html">Contato</a></li><li><ahref="inscricao.html"class="menu__cta">Inscreva-se</a></li></ul></nav></div></header>
Antes de colar o CSS novo, apague da seção 4 da folha de estilo as três regras de cabeçalho que a Aula 06 escreveu: header { … }, header nav ul { … } e header nav li { display: inline-block; margin-right: var(--espaco-medio) }. As regras header h1, header h2 e header a também saem — o cabeçalho agora tem fundo claro, e texto branco nele ficaria invisível. Se você deixar o margin-right do header nav li no arquivo, ele soma com o gap: 1.5rem do .menu e o espaçamento entre os itens fica dobrado; é exatamente o primeiro sintoma da tabela de Erros comuns desta aula.
Feito isso, cole o CSS do cabeçalho e do menu da seção 5.2 na seção 4 da folha de estilo, e acrescente flex-wrap: wrap ao .menu, para que os links quebrem linha em vez de estourar o cabeçalho quando a janela for estreita — um paliativo até a Aula 08:
Adicione o <a href="#a07-conteudo" class="salto"> como primeiro filho do <body> em todas as páginas, coloque id="a07-conteudo" tabindex="-1" no <main> de cada uma e cole o CSS de .salto na seção 6 (utilitários).
A página inicial abre com um "herói": título, texto de chamada, dois botões e a imagem do evento. Em telas largas, texto e imagem ficam lado a lado; em telas estreitas, empilham — e o auto-fit decide sozinho.
⚠️ Atenção
Este bloco substitui a <section class="hero"> que a Aula 05 criou (aquela com o <h2>, um parágrafo e um botão só) e também o <picture> do banner da Aula 04, cuja imagem passa a ser a do herói. Apague os dois e cole o <main> abaixo no lugar. As seções "Sobre o evento", "Como participar", "Glossário", "Destaques desta edição" e "O evento em números" continuam onde estão, logo depois do herói, dentro do <main>.
site-evento/index.html — conteúdo do <main>
HTML
<mainid="conteudo"tabindex="-1"><sectionclass="container hero"><divclass="hero__texto"><h1>Semana Acadêmica de Sistemas de Informação</h1><p>Três dias de palestras, minicursos e maratona de programação no campus de Sinop. Aberto a estudantes de todos os cursos.</p><divclass="hero__acoes"><aclass="botao"href="inscricao.html">Inscreva-se</a><aclass="botao botao--contorno"href="programacao.html">Ver programação</a></div></div><imgsrc="img/banner.jpg"alt="Auditório lotado durante a abertura da edição anterior"width="1200"height="800"></section></main>
O segundo botão usa .botao--contorno, o modificador que você já escreveu na Aula 06 — fundo transparente, texto na cor primária e inversão no :hover/:focus-visible. Não crie um .botao--secundario: seria o mesmo componente com outro nome, e componentes duplicados é como uma folha de estilo começa a apodrecer. Se você não encontrar o .botao--contorno na seção 5 da sua folha, é sinal de que o Passo 6 da Aula 06 ficou pela metade — volte lá antes de seguir.
Passo 6 — Programação: lateral de filtros e grade de cartões¶
A programação é a página mais rica do site. Em telas largas, ela tem uma lateral com filtros por dia e uma área de conteúdo com os cartões de cada atividade. É Grid para a estrutura (lateral + conteúdo) e Grid de novo, com auto-fit, para os cartões — e Flexbox dentro de cada cartão.
⚠️ Atenção
Este <main>substitui o conteúdo da programacao.html das Aulas 02 e 04: a <table> dos três dias, a seção "Trilhas do evento" e a seção "As cinco atividades mais procuradas" saem daqui e dão lugar aos cartões. As seções de vídeo, áudio e mapa (Passos 5 a 7 da Aula 04) continuam, depois dos cartões, dentro da <section class="conteudo">. E as regras de table, caption, th, td, thead, tbody e tfoot que você escreveu na Aula 06 ficam na folha de estilo: elas continuam servindo a tabela de horário de atendimento de contato.html.
Repare também em onde o <h1> está: fora da <section class="conteudo">, como primeiro filho do <main>. Se ele ficasse depois do <aside>, a página abriria com um <h2> ("Filtrar por dia") antes do <h1> — hierarquia quebrada, e uma reprovação certa na auditoria de acessibilidade do Lighthouse.
site-evento/programacao.html — conteúdo do <main>
HTML
<mainid="conteudo"tabindex="-1"class="container pagina"><h1>Programação</h1><asideclass="lateral"><h2>Filtrar por dia</h2><navaria-label="Dias do evento"><ulclass="lista-filtros"><li><ahref="#dia-1">Dia 1</a></li><li><ahref="#dia-2">Dia 2</a></li><li><ahref="#dia-3">Dia 3</a></li></ul></nav></aside><sectionclass="conteudo"><h2id="dia-1">Dia 1</h2><ulclass="cartoes"><liclass="cartao"><h3>Abertura e palestra magna</h3><pclass="cartao__meta"><timedatetime="19:00">19h</time> · Auditório Central</p><p>O futuro do desenvolvimento web e o papel de quem está começando agora.</p><aclass="botao"href="inscricao.html">Inscrever-se</a></li><liclass="cartao"><spanclass="cartao__selo">Esgotado</span><h3>Minicurso: Git e GitHub do zero</h3><pclass="cartao__meta"><timedatetime="20:00">20h</time> · Laboratório 2</p><p>Versionamento, branches e o primeiro pull request. Traga o notebook.</p><aclass="botao"href="inscricao.html"aria-disabled="true">Lista de espera</a></li><liclass="cartao"><h3>Mesa-redonda: mercado de trabalho em Sinop</h3><pclass="cartao__meta"><timedatetime="20:00">20h</time> · Sala 105</p><p>Egressos e empresas da região conversam sobre estágios e primeiro emprego.</p><aclass="botao"href="inscricao.html">Inscrever-se</a></li></ul><h2id="dia-2">Dia 2</h2><ulclass="cartoes"><liclass="cartao"><h3>Minicurso: acessibilidade na prática</h3><pclass="cartao__meta"><timedatetime="19:00">19h</time> · Laboratório 1</p><p>Teste seu site com leitor de tela e só com o teclado.</p><aclass="botao"href="inscricao.html">Inscrever-se</a></li><liclass="cartao"><h3>Palestra: segurança em aplicações web</h3><pclass="cartao__meta"><timedatetime="20:30">20h30</time> · Auditório Central</p><p>Os dez erros mais comuns e como evitá-los desde o primeiro commit.</p><aclass="botao"href="inscricao.html">Inscrever-se</a></li></ul><h2id="dia-3">Dia 3</h2><ulclass="cartoes"><liclass="cartao"><h3>Maratona de programação</h3><pclass="cartao__meta"><timedatetime="18:30">18h30</time> · Laboratórios 1 e 2</p><p>Equipes de três pessoas, quatro horas, dez problemas. Premiação no encerramento.</p><aclass="botao"href="inscricao.html">Inscrever equipe</a></li><liclass="cartao"><h3>Encerramento e premiação</h3><pclass="cartao__meta"><timedatetime="22:00">22h</time> · Auditório Central</p><p>Resultados da maratona, sorteios e confraternização.</p><aclass="botao"href="inscricao.html">Inscrever-se</a></li></ul></section></main>
site-evento/css/estilo.css — seção 4 (layout) e seção 5 (componentes)
CSS
/* Estrutura da página de programação: lateral + conteúdo */.pagina{display:grid;grid-template-columns:240px1fr;grid-template-areas:"titulo titulo""lateral conteudo";gap:var(--espaco-grande);padding-block:var(--espaco-grande);}.pagina>h1{grid-area:titulo;}.lateral{grid-area:lateral;}.conteudo{grid-area:conteudo;}.lateral{align-self:start;/* não estica até o fim da coluna */position:sticky;/* acompanha a rolagem, abaixo do cabeçalho */top:88px;/* altura do cabeçalho + respiro */}.lista-filtros{list-style:none;display:flex;flex-direction:column;gap:var(--espaco-pequeno);}.lista-filtrosa{display:block;padding:var(--espaco-pequeno)var(--espaco-medio);border-left:3pxsolidvar(--cor-borda);color:var(--cor-texto);text-decoration:none;}.lista-filtrosa:hover,.lista-filtrosa:focus-visible{border-left-color:var(--cor-primaria);color:var(--cor-primaria);}/* Grade de cartões: quantas colunas couberem, mínimo 280px cada */.cartoes{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:var(--espaco-medio);margin-block:var(--espaco-medio)var(--espaco-grande);}/* 5. Componentes */.cartao{position:relative;/* referência para o selo */display:flex;/* Flexbox DENTRO do cartão */flex-direction:column;gap:var(--espaco-pequeno);padding:var(--espaco-medio);background:var(--cor-superficie);border:1pxsolidvar(--cor-borda);border-radius:var(--raio-borda);box-shadow:var(--sombra-cartao);}.cartao.botao{margin-top:auto;/* empurra o botão para o fim, alinhando todos os cartões */align-self:flex-start;/* o botão não estica até a largura do cartão */}.cartao__meta{color:var(--cor-secundaria);font-size:.875rem;font-weight:600;}.cartao__selo{position:absolute;top:var(--espaco-pequeno);right:var(--espaco-pequeno);padding:2px10px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;background:#b42318;color:#fff;border-radius:999px;}
Repare no truque margin-top: auto dentro de um contêiner flex em coluna: a margem automática absorve todo o espaço sobrando e empurra o botão para o rodapé do cartão. Como align-items: stretch (o padrão do Grid) deixa todos os cartões da linha com a mesma altura, os botões ficam alinhados mesmo que os textos tenham tamanhos diferentes.
Passo 7 — Palestrantes: galeria que se reorganiza sozinha¶
A página de palestrantes reaproveita .cartoes e .cartao, com uma variação para a foto.
⚠️ Atenção
Este <main>substitui os seis <article> que as Aulas 02 e 04 escreveram (nome em <h3>, dois parágrafos e um <dl> de atividades e contato). Os nomes e as fotos são exatamente os mesmos — muda a marcação, que passa a ser uma <ul class="cartoes"> com um <li class="cartao"> por pessoa. Mostramos quatro aqui para o exemplo caber; repita o padrão para os seis do elenco.
site-evento/palestrantes.html — conteúdo do <main>
HTML
<mainid="conteudo"tabindex="-1"class="container"><h1>Palestrantes</h1><p>Profissionais e pesquisadores que vão compartilhar experiências nesta edição.</p><ulclass="cartoes"><liclass="cartao cartao--palestrante"><imgclass="cartao__foto"src="img/palestrante-01.jpg"alt="Ana Lúcia Ferreira"width="400"height="400"><h2>Ana Lúcia Ferreira</h2><pclass="cartao__meta">Professora e pesquisadora · Inteligência Artificial</p><p>Pesquisa redes neurais para prever a safra de soja e ministra o minicurso de redes neurais.</p></li><liclass="cartao cartao--palestrante"><imgclass="cartao__foto"src="img/palestrante-02.jpg"alt="Bruno Takahashi"width="400"height="400"><h2>Bruno Takahashi</h2><pclass="cartao__meta">Startup AgroData · Ciência de Dados</p><p>Constrói dashboards que os produtores realmente usam e fala sobre dados abertos.</p></li><liclass="cartao cartao--palestrante"><imgclass="cartao__foto"src="img/palestrante-03.jpg"alt="Carla Mendes"width="400"height="400"><h2>Carla Mendes</h2><pclass="cartao__meta">UFMT · Segurança</p><p>Pesquisa o que um ataque de phishing ensina sobre experiência de uso.</p></li><liclass="cartao cartao--palestrante"><imgclass="cartao__foto"src="img/palestrante-04.jpg"alt="Diego Nascimento"width="400"height="400"><h2>Diego Nascimento</h2><pclass="cartao__meta">Prefeitura de Sinop · Desenvolvimento Web</p><p>Trabalha com acessibilidade em portais públicos e conta os erros que a equipe já viu.</p></li></ul></main>
.cartao--palestrante{align-items:center;/* centraliza foto, nome e texto no eixo transversal */text-align:center;}.cartao__foto{width:120px;height:120px;border-radius:50%;/* foto redonda */object-fit:cover;/* recorta sem distorcer — a Aula 08 aprofunda */}
⚠️ Atenção
Este rodapé substitui o <footer> de três parágrafos da Aula 02 (realização, e-mail/telefone e linha de copyright). Nenhuma informação se perde: a realização vira a coluna "Sobre o evento", o e-mail vai para o <address> da coluna "Contato" e a linha de copyright vira o .rodape__creditos. Apague o bloco antigo nas cinco páginas e cole este no lugar — inclusive o <nav aria-label="Rodapé"> que a Aula 05 acrescentou em index.html, cujos links agora vivem na coluna "Links".
site-evento/index.html — antes de </body>, em todas as páginas
HTML
<footerclass="rodape"><divclass="container rodape__grade"><section><h2>Sobre o evento</h2><p>Organizado por professores e estudantes dos cursos de computação, com apoio do centro acadêmico.</p></section><section><h2>Links</h2><ul><li><ahref="programacao.html">Programação completa</a></li><li><ahref="inscricao.html">Inscrições</a></li><li><ahref="contato.html">Fale com a organização</a></li></ul></section><section><h2>Contato</h2><address><ahref="mailto:semana@exemplo.edu.br">semana@exemplo.edu.br</a><br>
Avenida dos Ingás, 3001 — Sinop, MT
</address></section></div><pclass="rodape__creditos">Semana Acadêmica de Sistemas de Informação</p></footer>
A grade de cartões e o rodapé já se adaptam sozinhos graças ao auto-fit. A página de programação, não: com 240px 1fr, em um celular a coluna de conteúdo fica com 100 px de largura. Por hoje, uma media query simples resolve — a Aula 08 vai inverter essa lógica para mobile first e formalizar os breakpoints.
/* 7. Media queries */@media(max-width:1023px){.pagina{grid-template-columns:1fr;grid-template-areas:"titulo""conteudo""lateral";/* no celular, os filtros vão para depois do conteúdo */}.lateral{position:static;/* deixa de acompanhar a rolagem */}}
Abra index.html com o Live Server. O cabeçalho deve ficar grudado no topo ao rolar, com o logo à esquerda e o menu à direita, e o link "Início" destacado com o sublinhado na cor primária.
Pressione Tab uma vez: o botão "Pular para o conteúdo" deve aparecer no canto superior esquerdo. Pressione Enter: o foco vai para o <main>. Continue com Tab: cada link do menu mostra o sublinhado crescendo.
Abra contato.html (a página com menos conteúdo): o rodapé deve estar colado no fim da janela, não flutuando no meio, e o conteúdo deve parar em 1100 px no centro — se ele encostar nas bordas, faltou a .container no <main> desta página. Confira o mesmo em inscricao.html.
Passe o mouse entre dois itens do menu e meça o espaço no DevTools: ele deve ser um único gap de 24 px. Se parecer o dobro, a regra header nav li da Aula 06 continua na folha — apague-a.
Em programacao.html, a lateral fica à esquerda e os cartões à direita, em colunas de no mínimo 280 px; o selo "Esgotado" aparece no canto superior direito do minicurso de Git; todos os botões de uma mesma linha ficam alinhados na base. Redimensione a janela: abaixo de 1024 px, tudo vira uma coluna e a lateral vai para o fim.
Em palestrantes.html, as fotos são redondas e os cartões se reorganizam de 4 para 3, 2 e 1 coluna conforme você estreita a janela — sem nenhuma media query.
No DevTools, clique no selo grid ao lado de <ul class="cartoes"> e no selo flex ao lado de <div class="cabecalho__interno"> e confira, com o overlay, que a estrutura é a que você desenhou.
A1. Explique a diferença entre position: relative, absolute e fixed quanto a dois critérios: a referência de posicionamento e a ocupação de espaço no fluxo.
A2. Por que um elemento com position: absolute costuma exigir position: relative no elemento pai? O que acontece se o pai não tiver essa declaração?
A3. Em um contêiner com display: flex e flex-direction: row, qual propriedade alinha os itens horizontalmente? E verticalmente? E se a direção for column?
A4. Diferencie space-between, space-around e space-evenly. Desenhe (em papel ou em texto) três itens em cada caso, marcando os espaços.
A5. O que significa flex: 1? Escreva sua forma expandida (as três propriedades) e explique o que cada valor faz.
A6. Escreva o CSS para centralizar perfeitamente um elemento na tela, horizontal e verticalmente, usando Flexbox. Depois reescreva usando Grid com uma linha só.
A7. O que a unidade fr representa? Em grid-template-columns: 200px 1fr 2fr, com um contêiner de 1100 px e gap: 0, qual é a largura de cada coluna? Mostre a conta.
A8. Explique, palavra por palavra, o que faz repeat(auto-fit, minmax(250px, 1fr)). Quantas colunas ela cria em uma janela de 1100 px? E em uma de 600 px?
A9. Escreva um grid-template-areas para um layout com cabeçalho no topo (largura total), lateral à esquerda, conteúdo à direita e rodapé embaixo (largura total). Inclua o grid-template-columns e o grid-template-rows correspondentes.
A10. Quando você usaria Flexbox e quando usaria Grid? Dê dois exemplos concretos de cada, tirados do site do evento.
A11. Por que float não deve mais ser usado para layout? Cite o único caso em que ele ainda é a ferramenta certa.
B1. Construa uma barra de navegação com Flexbox: logo à esquerda, cinco links à direita, item ativo destacado com aria-current="page". Em telas abaixo de 768 px, os links devem virar uma coluna centralizada abaixo do logo.
Resultado esperado: em 1200 px, logo e links na mesma linha, com space-between; em 500 px, o logo centralizado em cima e os cinco links empilhados e centralizados abaixo; o link ativo tem cor e sublinhado diferentes; Tab percorre os links na ordem do HTML com foco visível.
Dica
Na media query, troque flex-direction do contêiner do cabeçalho para column e do .menu também; align-items: center centraliza os dois. Não use order — a ordem do HTML já é a que você quer.
B2. Implemente o layout "santo graal": cabeçalho, três colunas (lateral esquerda fixa de 200 px, conteúdo elástico, lateral direita fixa de 200 px) e rodapé, com o rodapé sempre no fim da janela mesmo com pouco conteúdo. Em telas pequenas, tudo vira uma única coluna. Faça com Grid e áreas nomeadas.
Resultado esperado: em 1200 px, cinco áreas visíveis com grid-template-areas de três linhas; com um parágrafo só no conteúdo, o rodapé continua colado no fim da janela; abaixo de 768 px, as áreas se empilham na ordem cabeçalho → conteúdo → lateral esquerda → lateral direita → rodapé.
Dica
Use min-height: 100vh e grid-template-rows: auto 1fr auto no contêiner: a linha 1fr absorve o espaço sobrando. Na media query, redesenhe as áreas com uma palavra por linha — a ordem das strings define a ordem visual, sem tocar no HTML.
B3. Crie uma seção de "planos de inscrição" para o site do evento com três planos lado a lado (Flexbox): Estudante, Profissional e Apoiador. O do meio deve ser destacado (maior, com borda colorida e um selo "Mais popular" posicionado com absolute). Responsivo: os três empilham abaixo de 768 px.
Resultado esperado: três cartões com a mesma altura em telas largas, o do meio 8% maior (transform: scale(1.08) ou padding maior) e com borda na cor primária; o selo fica preso no canto superior direito do cartão do meio, mesmo ao redimensionar; no celular, os cartões ficam um abaixo do outro e o do meio deixa de ser maior.
Dica
align-items: stretch (o padrão) já iguala as alturas. Para o selo, position: relative no cartão e absolute no selo — a receita da seção 1.2. Lembre-se de flex: 1 1 260px nos cartões para que eles cresçam e quebrem linha.
B4. Reescreva o layout do exercício B3 duas vezes: uma versão só com Flexbox e outra só com Grid. Escreva meia página comparando a quantidade de código, a legibilidade e a facilidade de alteração de cada abordagem (ex.: o que muda para acrescentar um quarto plano?).
Resultado esperado: dois arquivos HTML funcionais e visualmente idênticos, e um texto de comparação com pelo menos três diferenças concretas, apontando qual versão você escolheria para o site do evento e por quê.
Dica
Conte as linhas de CSS de cada versão. Na versão Grid, experimente grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)) e veja o que acontece ao adicionar um quarto cartão. Na versão Flexbox, veja o que flex-wrap faz com um número ímpar de cartões na última linha.
B5. Construa um painel (dashboard) da organização do evento com Grid: cabeçalho, quatro cartões de indicadores na primeira linha (inscritos, vagas, palestrantes, minicursos), um cartão grande de "inscrições por dia" ocupando três colunas e um de "últimas inscrições" ocupando uma coluna na segunda linha, e uma tabela de atividades ocupando toda a largura na terceira. Responsivo em três breakpoints.
Resultado esperado: em 1200 px, uma grade de quatro colunas com os spans corretos; em 768 px, duas colunas (os indicadores em 2 × 2, o gráfico em largura total); no celular, tudo em uma coluna; a tabela nunca estoura a largura (use um contêiner com overflow-x: auto).
Dica
Defina grid-template-columns: repeat(4, 1fr) na base e use grid-column: span 3 e span 4 nos itens grandes. Nas media queries, troque o repeat(4, 1fr) por repeat(2, 1fr) e 1fr, e ajuste os spans para que nenhum item peça mais colunas do que a grade tem — se pedir, o Grid cria colunas implícitas e o layout estoura.
C1. Base do projeto autoral. Defina o tema do seu projeto (o "site do evento" da sua versão: uma feira, um campeonato, uma semana cultural, um curso de extensão) e construa o esqueleto completo de todas as páginas: HTML semântico, estrutura de layout com Grid e áreas nomeadas, componentes internos com Flexbox, sistema de design em variáveis, navegação funcional entre as páginas, cabeçalho fixo e rodapé consistentes em todas elas, link de salto funcionando. Produza também um wireframe (papel fotografado, Figma ou Excalidraw) de três larguras: celular, tablet e desktop. Este exercício é a primeira etapa do que o Marco 2 vai cobrar (Aula 10).
Dica
Comece pelo wireframe, não pelo código: desenhe as três larguras em dez minutos e só então decida quais áreas nomeadas cada página precisa. Reaproveite .container, .cabecalho, .menu, .cartoes e .rodape da Mão na massa — o que muda no seu projeto é o conteúdo e as cores, não a arquitetura. Valide o HTML de cada página no W3C antes de estilizar.
Você consegue levar 24 sapos para as vitórias-régias certas usando só justify-content, align-items e flex-wrap? E regar 28 cenouras com grid-column, grid-area e grid-template-areas? Os dois jogos foram criados para fixar, por repetição, exatamente as propriedades desta aula — e muita gente que "já sabia Flexbox" descobre no nível 18 que não sabia align-content. Jogue os dois em português (https://flexboxfroggy.com/#pt-br e https://cssgridgarden.com/#pt-br) e anote o que travou.
Critérios de pronto
Captura de tela do último nível de cada jogo concluído (nível 24 do Froggy e nível 28 do Garden).
Um arquivo aprendizados.md com três propriedades ou valores que você não conhecia (ou usava errado) antes dos jogos, com uma frase explicando cada um.
Para cada uma das três, um trecho do site do evento onde ela poderia ser aplicada.
Pistas
Se travar em um nível, abra a referência de Flexbox da MDN em pt-BR e procure a propriedade que o enunciado do nível cita — o jogo sempre diz qual é.
No Froggy, os níveis a partir do 19 combinam flex-direction com justify-content: lembre que o eixo principal muda de lugar.
No Garden, grid-column: 2 / span 3 e grid-column: 2 / 5 fazem a mesma coisa — os níveis finais testam se você entende as duas formas.
O código abaixo era para produzir um cabeçalho fixo com menu à direita, uma página com lateral à esquerda e conteúdo à direita, e um selo "Esgotado" no canto do cartão. Nada disso acontece: o cabeçalho rola junto com a página, a lateral cai embaixo do conteúdo, o selo aparece no canto da janela e o menu tem marcadores e espaços duplos. Há cinco bugs. Encontre e corrija todos usando apenas o DevTools como ferramenta de investigação — sem reescrever do zero.
caca-ao-bug.html
HTML
<body><headerclass="topo"><divclass="topo__interno"><aclass="logo"href="index.html">Semana</a><navaria-label="Principal"><ulclass="menu"><li><ahref="index.html">Início</a></li><li><ahref="programacao.html">Programação</a></li><li><ahref="contato.html">Contato</a></li></ul></nav></div></header><mainclass="grade"><asideclass="lateral">Filtros</aside><sectionclass="conteudo"><divclass="cartao"><spanclass="selo">Esgotado</span><h2>Minicurso de Git</h2><p>Versionamento do zero ao primeiro pull request.</p></div></section></main></body>
A lateral fica à esquerda e o conteúdo à direita, na mesma linha.
O selo "Esgotado" fica no canto superior direito do cartão, mesmo ao rolar e redimensionar.
O menu não tem marcadores de lista e os links têm exatamente 24 px de espaço entre si.
Um comentário acima de cada correção explica o bug em uma linha (por que o comportamento errado acontecia).
Pistas
sticky precisa de duas coisas para funcionar: uma delas está faltando na regra, a outra está sendo sabotada por um ancestral — releia a seção 1.2 e o Investigue da seção 1.5.
No DevTools, ative o overlay da grade e "Show area names": o nome que aparece na primeira coluna é o mesmo que a .lateral está pedindo?
absolute procura o ancestral posicionado mais próximo. Qual é o ancestral posicionado mais próximo do selo neste código?
gap e margin não se anulam — se somam. E ul tem um estilo padrão que ninguém removeu.
A página inicial do site do evento precisa de um bloco de destaques em formato de revista: sete cartões de tamanhos diferentes — um grande (2 × 2), dois largos (2 × 1), um alto (1 × 2) e três pequenos (1 × 1) — encaixados sem buracos, como um mosaico. O detalhe: quando a janela estreita e a grade perde colunas, os buracos aparecem, a menos que você conheça uma propriedade que faz o Grid "preencher para trás". Descubra qual é e construa o mosaico.
Critérios de pronto
Em 1100 px, os sete cartões ocupam uma grade de quatro colunas sem nenhuma célula vazia.
Em 700 px (duas colunas) e em 400 px (uma coluna), continua não havendo buracos e nenhum cartão pede mais colunas do que existem.
Nenhum cartão é posicionado com números de linha fixos (grid-column: 3 / 5) — só com span.
A ordem de leitura por teclado (Tab nos links dos cartões) faz sentido mesmo quando o Grid reordena visualmente.
Pistas
Procure grid-auto-flow na MDN e leia sobre o valor dense.
grid-auto-rows com um valor fixo (ou minmax) dá altura às linhas implícitas, para que span 2 em linhas tenha efeito visível.
Na media query de duas colunas, reduza todo span maior que 2 para span 2; em uma coluna, span 1 para todos.
dense pode mover um cartão para antes de outro que vem primeiro no HTML. Teste com Tab e, se a ordem ficar estranha, reordene o HTML — não o CSS.
Você consegue reproduzir a página inicial de um site que usa todo dia? Escolha a home de um site conhecido (o portal da sua universidade ou escola, um jornal, uma loja, a página inicial do GitHub) e reproduza seu layout — estrutura e disposição, com conteúdo próprio e sem copiar imagens — usando Grid para a página e Flexbox para os componentes, com responsividade em três larguras. Requisito inegociável: zero float e zero position: absolute para layout (o absolute só pode aparecer em selos e ícones). Ao terminar, você vai perceber quanto dos sites que admira é só Grid e Flexbox bem combinados. Isso entra bem no Marco 2 da unidade.
Critérios de pronto
Três pares de capturas lado a lado (original × sua versão) em 375 px, 768 px e 1440 px.
O arquivo CSS tem, no topo, um comentário com o desenho das áreas nomeadas de cada largura.
O DevTools não mostra nenhum float e nenhum position: absolute em elementos estruturais (cabeçalho, colunas, seções, rodapé).
O menu principal é uma lista dentro de <nav aria-label>, com aria-current e link de salto.
HTML validado sem erros no W3C.
Pistas
Antes de escrever CSS, abra o site escolhido com o DevTools e clique nos selos grid e flex: muitos sites profissionais deixam a estrutura à mostra.
Desenhe o grid-template-areas das três larguras em papel primeiro; se você não consegue desenhar, não consegue codificar.
Comece pela largura maior (a mais complexa) e reduza — nesta aula ainda é aceitável; na Aula 08 você vai inverter.
Onde o original usa uma imagem de fundo com texto por cima, use uma célula do Grid com background-image e um div com Flexbox para o texto — sem absolute.
Parte 1 — Leitura (20 min). SILVA, Maurício Samy. Criando sites com HTML, capítulos de posicionamento e layout. MDN Web Docs em pt-BR: os guias "Conceitos básicos do Flexbox" e "Conceitos básicos do Grid Layout" (links em "Para aprofundar"). Anote uma propriedade de cada guia que não apareceu nesta aula.
Parte 2 — Produção (30 min). No seu projeto autoral:
Exercícios B1 (barra de navegação) e B5 (dashboard), em arquivos separados na pasta exercicios/aula07/.
O layout principal do projeto construído com Grid e áreas nomeadas em pelo menos uma página (a mais complexa), com Flexbox no interior dos componentes.
O menu de navegação completo em todas as páginas: lista semântica, nav aria-label, aria-current na página atual, :focus-visible em todos os links e link de salto funcionando.
Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: traga um componente do seu projeto (um trecho de HTML + CSS), diga qual dos dois você usou (Grid ou Flexbox) e justifique tecnicamente com base na tabela da seção 4. Se puder, compare a escolha com a de outra pessoa.
Critério de pronto: ao pressionar Tab na primeira página do seu projeto, o link de salto aparece; ao pressionar Enter, o foco vai para o conteúdo; navegando pelo menu, cada link mostra foco visível e a página atual está marcada com aria-current; ao abrir a página mais curta, o rodapé está no fim da janela; ao clicar nos selos grid/flex do DevTools, a estrutura desenhada é a que você planejou.
Guarde no seu repositório: commit + push (ou a pasta do projeto, se ainda não usa Git).
web.dev — Learn CSS: https://web.dev/learn/css — os módulos "Flexbox", "Grid" e "Z-index and stacking contexts" explicam o contexto de empilhamento com ilustrações.
SILVA, Maurício Samy. Criando sites com HTML: sites de alta qualidade com HTML e CSS. Novatec, 2008 — capítulos de posicionamento e layout (contexto histórico do float; compare com o que você aprendeu hoje).
TERUEL, Evandro C. HTML 5 — Guia Prático. Saraiva, 2014 — capítulo sobre estruturação de páginas com os elementos semânticos que o Grid organiza.
Na próxima aula, o site do evento vai para o celular de verdade: você vai reescrever a folha de estilo no modo mobile first, definir breakpoints onde o layout quebra (e não onde o iPhone manda), transformar o menu em um hambúrguer acessível e respeitar as preferências do usuário — tema escuro e redução de movimento.
[ ] Pasta site-evento/ com as cinco páginas e o css/estilo.css como ficaram ao fim da Aula 07: cabeçalho fixo com menu horizontal, link de salto, grade de cartões com auto-fit, página de programação com áreas nomeadas e rodapé em três colunas.
[ ] VS Code com a extensão Live Server e um navegador com DevTools (Chrome ou Firefox).
[ ] Um celular conectado à mesma rede Wi-Fi do computador (opcional, mas vai valer a pena no bloco 3).
[ ] Revisar da Aula 07: repeat(auto-fit, minmax()), grid-template-areas e a diferença entre justify-content e align-items.
Na aula passada você estruturou o site da Semana Acadêmica de Sistemas de Informação com Grid e Flexbox e construiu o menu horizontal — mas só olhou o resultado no monitor do laboratório. Abra a mesma página no celular: o menu estoura para fora da tela, os cartões ficam estreitos demais e a lateral de filtros esmaga o conteúdo. Hoje você reescreve a folha de estilo de trás para a frente — primeiro o celular, depois as telas maiores — e cumpre a promessa da Aula 07: o menu hambúrguer.
Viewport, os três pilares do design responsivo, media queries, mobile first e breakpoints
2
50 min
Grades fluidas sem media query, imagens responsivas (object-fit, srcset), tipografia com clamp()
3
50 min
Menu hambúrguer acessível, preferências do usuário, testes no celular real e Mão na massa
1. Por que "responsivo" — e o que a meta viewport tem a ver com isso¶
O mesmo index.html que você escreveu na Aula 02 vai ser aberto em um celular de 360 px de largura, em um tablet de 768 px, em um notebook de 1366 px e em um monitor de 2560 px. Não existe "a tela": existe um intervalo contínuo de larguras, e o site precisa funcionar em qualquer ponto dele.
Até o começo dos anos 2010 a solução era manter dois sites: www.site.com para o computador e m.site.com para o celular, com HTML duplicado e conteúdo sempre desatualizado em um dos dois. Design responsivo é a ideia de que um único HTML se adapta à largura disponível, e ele se apoia em três pilares:
A meta viewport, que diz ao celular para não fingir que é um monitor.
Layouts fluidos, feitos com larguras relativas (%, fr, minmax(), max-width) em vez de pixels fixos.
Media queries, para os ajustes pontuais que a fluidez sozinha não resolve.
Repare na ordem: as media queries vêm por último. Um layout bem feito com Grid e Flexbox, como o da Aula 07, já se adapta a boa parte das larguras sem nenhuma query. Elas entram para os pontos em que a fluidez não basta — e a Mão na massa de hoje vai mostrar que são bem poucos.
🧠 Você sabia?
O termo Responsive Web Design foi cunhado por Ethan Marcotte em um artigo de 2010 na revista A List Apart. Ele emprestou a ideia da arquitetura responsiva — prédios cujas paredes e iluminação reagem à presença das pessoas — e propôs que páginas fizessem o mesmo com a largura da janela. Antes disso, "site para celular" significava um segundo site.
A largura de layout da página passa a ser a largura real do aparelho em pixels CSS (360 px em um celular comum), e não os 980 px que ele finge ter por padrão.
initial-scale=1.0
O zoom inicial é 1:1 — um pixel CSS corresponde a um pixel "lógico" do aparelho. Sem isso, alguns navegadores abrem a página afastada.
Sem essa linha, o celular renderiza a página como se a janela tivesse 980 px de largura e depois encolhe tudo para caber na tela. O resultado é aquele site minúsculo em que você precisa dar zoom para ler qualquer coisa — e nenhuma media query dispara, porque para o navegador a largura "é" 980 px.
⚠️ Atenção
Nunca acrescente user-scalable=no nem maximum-scale=1 à meta viewport. Isso bloqueia o zoom, que é a única forma de muita gente conseguir ler o seu site. Os navegadores atuais ignoram essa proibição por questão de acessibilidade, e o Lighthouse penaliza a página que tenta usá-la.
🔎 Por baixo do capô
O navegador do celular trabalha com duas "janelas". A viewport de layout é a largura que o CSS enxerga — é nela que width: 100% e as media queries são calculadas. A viewport visual é o pedaço que aparece na tela depois do zoom. A meta viewport iguala as duas na abertura da página. E "pixel CSS" não é o mesmo que pixel físico: um celular com tela de 1080 px físicos e devicePixelRatio igual a 3 reporta 360 px CSS. É por isso que o seu layout de 360 px fica nítido: cada pixel CSS é desenhado com 3 × 3 pixels de verdade.
Toda vez que a largura da viewport muda — você gira o celular, redimensiona a janela, abre o DevTools — o navegador recalcula a posição e o tamanho de todas as caixas da página. Esse recálculo se chama reflow (ou layout). Ele é caro, e vai voltar a aparecer na Aula 09 quando falarmos de animações que travam.
🔬 Investigue
Abra site-evento/index.html no Live Server e o console do DevTools (F12). Digite window.innerWidth e anote o valor. Ative o modo dispositivo (Ctrl+Shift+M), escolha "Pixel 7" ou outro celular e repita: o valor cai para algo como 412, mesmo com screen.width informando outra coisa. Agora digite window.devicePixelRatio. Por fim, comente a linha da meta viewport no HTML, salve e observe window.innerWidth de novo no modo dispositivo: 980. Descomente antes de seguir.
Os layouts fluidos são o assunto da seção 2; as media queries, da seção 3. Vale antecipar a regra de ouro que conecta os dois: primeiro tente resolver com fluidez; só escreva uma media query quando o layout quebrar.
Compare com a versão que muita gente escreve e que quebra no celular:
CSS
/* ❌ Quebra em qualquer tela com menos de 1100 px */.container{width:1100px;margin:0auto;}
A diferença é o par width: 100% + max-width. Em uma tela de 360 px o contêiner ocupa 360 px; em uma de 1920 px ele para em 1100 px e centraliza. Uma regra só, sem media query. O padding-inline garante que o texto não encoste na borda do celular.
⚠️ Atençãowidth fixa em pixels é a causa número um de rolagem horizontal no celular. Se você precisa de um limite, o limite é max-width. Se precisa de um mínimo, é min-width — e mesmo assim pense duas vezes.
Uma foto de 1600 px de largura, sem essa regra, empurra o contêiner de 360 px para 1600 px e cria uma barra de rolagem horizontal na página inteira. Com max-width: 100% ela encolhe até caber; o height: auto mantém a proporção mesmo quando o HTML declara width e height no <img> — e você vai declarar, como mostra a seção 4.
💡 Dicamin-height: 100vh em um celular pode ficar mais alto do que a tela visível, porque a barra de endereço do navegador entra na conta. A unidade dvh (dynamic viewport height) resolve: min-height: 100dvh acompanha a barra aparecendo e sumindo. Funciona em todos os navegadores atuais.
Hoje vale fazer a conta do que ela faz em cada largura. O contêiner tem padding-inline de 16 px de cada lado; a largura útil é a largura da tela menos 32 px.
Largura da tela
Largura útil
Colunas de no mínimo 280 px que cabem
360 px
328 px
1 (com 328 px)
768 px
736 px
2 (com 360 px cada, descontado o gap)
1024 px
992 px
3 (com 320 px cada)
1440 px
1100 px (limite do .container)
3 (com 356 px cada)
O navegador cria quantas colunas de 280 px couberem, distribui o que sobrar entre elas e reorganiza tudo a cada mudança de largura. Nenhuma media query, nenhum breakpoint escolhido por você — o breakpoint nasce do conteúdo, que é exatamente onde ele deve nascer.
📌 Vale gravarauto-fit e auto-fill fazem a mesma conta de quantas colunas cabem. A diferença aparece quando há menos itens do que colunas: auto-fill mantém as colunas vazias (os itens ficam estreitos, com espaço sobrando à direita); auto-fit colapsa as vazias e deixa os itens existentes crescerem. Para grades de cartões, você quase sempre quer auto-fit.
⚠️ Atençãominmax(280px, 1fr) tem um ponto cego: em um contêiner com menos de 280 px de largura (um celular pequeno com padding generoso, ou a lateral estreita da programação), a coluna mínima é maior que o espaço e a grade estoura. A correção é minmax(min(280px, 100%), 1fr): o mínimo passa a ser "280 px ou a largura toda, o que for menor". Você vai aplicar isso na Mão na massa.
Leia como uma pergunta: "a viewport tem pelo menos 768 px de largura?". Se a resposta for sim, as regras dentro das chaves valem; se for não, é como se não existissem. Dentro do bloco você escreve CSS comum — seletores, propriedades, valores — nada muda.
🔎 Por baixo do capô
Uma media query não aumenta a especificidade de nada. .grade dentro de @media tem exatamente o mesmo peso de .grade fora. O que decide quem vence, quando a query está ativa, é a regra da cascata que você aprendeu na Aula 06: entre seletores de mesmo peso, a última declarada ganha. Por isso as media queries ficam depois das regras base no arquivo — se você escrever a query antes, a regra base a sobrescreve e parece que "a media query não funciona". O navegador reavalia todas as queries a cada mudança de largura, e cada mudança de resultado dispara um reflow.
Mobile first é uma ordem de escrita: o CSS sem nenhuma media query é o CSS do celular; cada media query de min-width acrescenta o que muda quando a tela cresce.
site-evento/css/estilo.css (exemplo completo)
CSS
/* Base — celular: uma coluna, sem query nenhuma */.grade{display:grid;grid-template-columns:1fr;gap:16px;}/* Tablet — a partir de 768px: duas colunas */@media(min-width:768px){.grade{grid-template-columns:repeat(2,1fr);}}/* Notebook — a partir de 1024px: três colunas */@media(min-width:1024px){.grade{grid-template-columns:repeat(3,1fr);}}/* Monitor grande — a partir de 1440px: quatro colunas */@media(min-width:1440px){.grade{grid-template-columns:repeat(4,1fr);}}
Em um monitor de 1600 px as quatro queries estão ativas ao mesmo tempo. As três primeiras definem grid-template-columns, e a última vence porque é a última. É o efeito cascata a seu favor: cada query só precisa declarar o que muda naquele tamanho.
É onde está o público. Mais da metade dos acessos a sites no Brasil vem de celulares. Um site que começa pelo monitor e "adapta" para o celular está otimizando para a minoria.
Obriga a priorizar. Em 360 px não cabe tudo. Escrever primeiro para a tela pequena força a decidir o que é essencial — e o que é essencial no celular continua essencial no monitor.
Gera CSS aditivo. Queries de min-width acrescentam; queries de max-width precisam desfazer regras já aplicadas. Compare o exemplo acima com a versão desktop first a seguir.
O celular baixa menos CSS aplicável. O aparelho mais fraco, na rede mais lenta, processa só a base. As queries de telas grandes ficam ali, mas não alteram nada.
Desktop first — a mesma grade, escrita ao contrário
CSS
/* Base — monitor grande: quatro colunas */.grade{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;}/* Até 1439px: desfaz para três */@media(max-width:1439px){.grade{grid-template-columns:repeat(3,1fr);}}/* Até 1023px: desfaz para duas */@media(max-width:1023px){.grade{grid-template-columns:repeat(2,1fr);}}/* Até 767px: desfaz para uma */@media(max-width:767px){.grade{grid-template-columns:1fr;}}
Funciona, mas repare nos limites "menos um" (1439px, 1023px, 767px) — um convite a erro de conta — e na lógica invertida: a base é a tela que menos gente usa, e o celular é o último caso tratado. Nesta trilha você escreve sempre mobile first.
📌 Vale gravar
Uma pergunta clássica: "no CSS mobile first, o que acontece se o navegador não suportar media queries?". Resposta: ele mostra a versão de celular, que é a base e funciona em qualquer largura. No desktop first, o mesmo navegador mostraria as quatro colunas espremidas em 360 px.
Um breakpoint é a largura em que uma media query entra em ação. A pergunta que todo iniciante faz é "quais são os breakpoints certos?", e a resposta honesta é: os que o seu layout pedir.
A tabela abaixo dá as faixas típicas, para você ter uma referência de vocabulário:
Faixa
Dispositivo típico
até 480 px
Celular pequeno
481–767 px
Celular grande
768–1023 px
Tablet
1024–1439 px
Notebook / desktop
1440 px ou mais
Monitor grande
💡 Dica
Não decore breakpoints de aparelhos. O método é: abra o site em uma janela larga, arraste a borda devagar e observe. No instante em que algo quebra — um título vira três linhas feias, os cartões ficam com 200 px, o menu estoura —, olhe o número que o DevTools mostra no canto superior direito. Esse é o seu breakpoint. O design manda no breakpoint, não o iPhone.
Para o site do evento, três pontos bastam e são os que você vai usar hoje: 768 px (o menu cabe em linha e a grade aguenta duas colunas), 1024 px (a lateral de filtros cabe ao lado do conteúdo) e, opcionalmente, 1440 px (mais respiro entre as seções).
Media queries não são só sobre largura. Estas são as que você vai usar com frequência:
CSS
/* Só até 767px (raro em mobile first, mas útil para esconder algo só no celular) */@media(max-width:767px){.so-desktop{display:none;}}/* Faixa fechada: só entre 768px e 1023px */@media(min-width:768px)and(max-width:1023px){.lateral{display:none;}}/* Orientação: celular deitado */@media(orientation:landscape){.hero{padding-block:2rem;}}/* Impressão: tira o que não faz sentido no papel */@mediaprint{nav,footer,.sem-impressao{display:none;}}/* Tema escuro escolhido no sistema operacional */@media(prefers-color-scheme:dark){:root{--cor-fundo:#0f1720;--cor-texto:#e6edf3;}}/* Usuário pediu menos movimento */@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;}}
As três últimas — impressão, tema escuro e redução de movimento — são preferências do usuário, e a seção 6 volta a elas com calma.
💡 Dica
Existe uma sintaxe mais nova, de intervalo: @media (width >= 768px) e @media (768px <= width < 1024px). Ela já funciona em todos os navegadores atuais e evita os limites "menos um". Nesta trilha usamos min-width porque é a forma que você vai encontrar em 99 % do código existente — mas reconheça as duas.
Na Aula 06 você reservou a seção 7 de estilo.css para elas. Duas escolas convivem no mercado: agrupar todas as queries no fim do arquivo, em ordem crescente de largura, ou manter cada query logo abaixo do componente que ela ajusta. As duas funcionam; o que não pode é misturar. Nesta trilha, para facilitar a leitura e a correção, as queries ficam agrupadas no fim, em ordem crescente: primeiro o bloco de 768px, depois o de 1024px, depois o de 1440px.
object-fit e aspect-ratio: fotos que não deformam¶
Os cartões de palestrantes recebem fotos de tamanhos diferentes: uma em 800 × 600, outra em 1200 × 1200, outra em 640 × 960. Com width: 100% cada uma fica com uma altura, e a grade vira um zigue-zague.
aspect-ratio: 16 / 9 reserva a mesma proporção para todas as imagens, qualquer que seja a largura do cartão.
object-fit: cover faz a foto preencher a caixa cortando o que sobra, em vez de esticar (fill, o padrão) ou deixar faixas vazias (contain).
object-position escolhe que parte da foto sobrevive ao corte — center para paisagens, top para retratos, onde o rosto fica na parte de cima.
srcset e <picture>: a imagem certa para cada tela¶
Servir uma foto de 1600 px para um celular de 360 px desperdiça dados e tempo de carregamento. O atributo srcset oferece várias versões e deixa o navegador escolher.
site-evento/index.html (banner da página inicial)
HTML
<imgsrc="img/banner-800.jpg"srcset="img/banner-400.jpg 400w, img/banner-800.jpg 800w, img/banner-1600.jpg 1600w"sizes="(min-width: 1024px) 1100px, 100vw"width="1600"height="900"alt="Auditório lotado na abertura da Semana Acadêmica de Sistemas de Informação">
srcset lista os arquivos e a largura real de cada um (480w = 480 px de largura).
sizes diz quanto da tela a imagem vai ocupar: 1100 px em telas a partir de 1024 px, a tela inteira (100vw) abaixo disso. Com essa informação e o devicePixelRatio, o navegador baixa só o arquivo adequado.
width e height no HTML informam a proporção antes de a imagem carregar. O navegador reserva o espaço e a página não "pula" quando a foto chega — o Lighthouse mede isso como Cumulative Layout Shift, e vai cobrar na seção 7.
Quando a versão de celular precisa ser outra foto (um recorte mais fechado, por exemplo), use <picture>:
HTML
<picture><sourcemedia="(min-width: 768px)"srcset="img/banner-largo.jpg"><imgsrc="img/banner-quadrado.jpg"width="800"height="800"alt="Auditório lotado na abertura da Semana Acadêmica de Sistemas de Informação"></picture>
O <source> com media funciona como uma media query dentro do HTML; o <img> no fim é obrigatório e serve de reserva.
Um <h1> de 3 rem fica ótimo no monitor e ocupa três linhas no celular. Antes de clamp(), a solução era uma media query por tamanho de fonte. Hoje é uma linha:
clamp(mínimo, preferido, máximo) devolve o valor preferido, mas nunca abaixo do mínimo nem acima do máximo. No <h1>:
em 360 px, 1.2rem + 2.5vw dá 1.2 × 16 + 9 = 28,2 px — abaixo do mínimo de 28 px? Não: 28,2 px é ligeiramente acima, então esse é o valor usado;
em 1024 px, dá 19,2 + 25,6 = 44,8 px, entre os limites;
em 1920 px, daria 67,2 px, mas o máximo de 3 rem (48 px) segura.
⚠️ Atenção
O termo preferido mistura rem com vw de propósito. Se fosse só 2.5vw, a fonte deixaria de responder ao zoom do navegador (Ctrl++), porque vw depende da largura da janela e não do tamanho da fonte — e isso viola o critério 1.4.4 da WCAG, que exige que o texto possa crescer até 200 %. O rem no cálculo devolve o controle ao usuário.
💡 Dica
No celular, nunca deixe texto corrido nem campos de formulário abaixo de 16 px. Além de ficar difícil de ler, o Safari do iPhone dá zoom automático em qualquer <input> com fonte menor que 16 px quando ele recebe foco, e o layout inteiro "salta".
Na Aula 07 o menu era uma lista horizontal com cinco links, e em 360 px ela não cabe. A solução clássica é esconder a lista atrás de um botão com três barrinhas — o "hambúrguer" — que só aparece em telas pequenas.
Você vai ver por aí menus abertos por <div class="hamburguer"> ou por <a href="#">. Os dois estão errados, e a razão é acessibilidade:
Um <div> não recebe foco por Tab, não reage a Enter nem a Espaço e é anunciado pelo leitor de tela como "texto", não como algo clicável.
Um <a href="#"> é focável, mas semanticamente é um link: o leitor de tela anuncia "link", o usuário espera navegar para outro lugar, e o # faz a página rolar para o topo.
Um <button> é focável, reage às duas teclas, é anunciado como "botão" e aceita dois atributos que contam o resto da história: aria-expanded (está aberto ou fechado?) e aria-controls (controla qual elemento?).
Este é o bloco final do cabeçalho. Ele substitui o <nav> da Aula 07 nas cinco páginas:
site-evento/index.html (dentro de .cabecalho__interno, depois do logo)
HTML
<navaria-label="Principal"><buttonclass="menu-botao"aria-expanded="false"aria-controls="menu-principal"><spanclass="menu-botao__icone"aria-hidden="true"></span>
Menu
</button><ulid="menu-principal"class="menu"><li><ahref="index.html"aria-current="page">Início</a></li><li><ahref="programacao.html">Programação</a></li><li><ahref="palestrantes.html">Palestrantes</a></li><li><ahref="contato.html">Contato</a></li><li><ahref="inscricao.html"class="menu__cta">Inscreva-se</a></li></ul></nav>
Três detalhes:
O <span> do ícone tem aria-hidden="true" porque é decorativo — o leitor de tela lê a palavra "Menu", que está ali como texto de verdade.
aria-controls="menu-principal" aponta para o id da lista. Alguns leitores de tela oferecem um atalho para "pular para o elemento controlado".
aria-expanded="false" é o estado. É esse atributo que o CSS vai observar para mostrar ou esconder a lista — e é o único que o JavaScript vai trocar.
O CSS: base para o celular, a query acrescenta o desktop¶
site-evento/css/estilo.css (seção 5 — componentes; substitui o .menu da Aula 07)
CSS
/* Cabeçalho: logo à esquerda, botão à direita (celular) */.cabecalho__interno{display:flex;align-items:center;justify-content:space-between;gap:var(--espaco-medio);padding-block:var(--espaco-medio);}/* Botão hambúrguer */.menu-botao{display:inline-flex;align-items:center;gap:var(--espaco-pequeno);padding:var(--espaco-pequeno)var(--espaco-medio);background:transparent;border:1pxsolidvar(--cor-borda);border-radius:var(--raio-borda);color:var(--cor-texto);font:inherit;cursor:pointer;}.menu-botao:focus-visible{outline:3pxsolidvar(--cor-secundaria);outline-offset:2px;}/* As três barrinhas: uma no span, duas nos pseudoelementos */.menu-botao__icone{position:relative;display:block;width:20px;height:2px;background:currentColor;}.menu-botao__icone::before,.menu-botao__icone::after{content:"";position:absolute;left:0;width:100%;height:2px;background:currentColor;}.menu-botao__icone::before{top:-6px;}.menu-botao__icone::after{top:6px;}/* Lista: fechada por padrão, cai abaixo do cabeçalho quando aberta */.menu{display:none;position:absolute;top:100%;left:0;right:0;flex-direction:column;gap:0;margin:0;padding:var(--espaco-pequeno)var(--espaco-medio);list-style:none;background:var(--cor-superficie);border-bottom:1pxsolidvar(--cor-borda);box-shadow:var(--sombra-cartao);}/* O atributo que o JavaScript troca é o que abre a lista */.menu-botao[aria-expanded="true"]+.menu{display:flex;}.menua{display:block;padding:var(--espaco-medio)0;border-bottom:1pxsolidvar(--cor-borda);}.menuli:last-childa{border-bottom:0;}
E, na seção 7, a query que transforma o mesmo HTML no menu horizontal da Aula 07:
site-evento/css/estilo.css (seção 7 — media queries)
A lista aberta usa position: absolute com top: 100%. O ancestral posicionado mais próximo é o .cabecalho, que é sticky — e sticky conta como posicionado, como você viu na Aula 07. A lista cai exatamente abaixo do cabeçalho, por cima do conteúdo.
O seletor .menu-botao[aria-expanded="true"] + .menu combina um seletor de atributo com o combinador de irmão adjacente (+), ambos da Aula 06. Ele só casa quando o botão está com aria-expanded="true"e a lista vem logo depois dele — que é o caso do HTML acima.
A query de 768 px desfaz mais coisas do que o normal (position, background, box-shadow). É a exceção que confirma a regra do mobile first: o menu de celular e o de desktop são componentes genuinamente diferentes, então não há como um ser só "a base mais um pouco" do outro.
O sublinhado animado de .menu a::after da Aula 07 continua valendo nas duas versões.
⚠️ Atençãodisplay: none remove a lista da árvore de acessibilidade — o leitor de tela não a encontra. Aqui isso é correto: o menu está fechado e o usuário não deveria alcançar links invisíveis com Tab. Na Aula 09 você vai animar a abertura, e vai aprender que display não anima; a técnica passa a ser opacity + visibility + transform, que preserva o mesmo comportamento acessível.
Oito linhas de JavaScript — antecipação da Unidade 3¶
O CSS já sabe abrir e fechar. Falta alguém trocar o atributo quando o botão for clicado. Isso é JavaScript, assunto da Unidade 3 — mas são poucas linhas, e vale entendê-las agora:
site-evento/js/menu.js
JavaScript
// Seleciona o botão e a lista pelo seletor CSSconstbotao=document.querySelector('.menu-botao');constmenu=document.querySelector('#menu-principal');// A cada clique, inverte o valor de aria-expandedbotao.addEventListener('click',()=>{constaberto=botao.getAttribute('aria-expanded')==='true';botao.setAttribute('aria-expanded',String(!aberto));});
E a chamada, no <head> das cinco páginas:
site-evento/index.html
HTML
<scriptsrc="js/menu.js"defer></script>
O atributo defer faz o navegador baixar o script em paralelo e só executá-lo depois de terminar de ler o HTML — sem ele, document.querySelector('.menu-botao') rodaria antes de o botão existir e devolveria null.
O que importa aqui é a divisão de trabalho: o JavaScript só troca um atributo; o CSS decide o que aparece. Nada de menu.style.display = 'flex' — quem manda na apresentação é a folha de estilo, e o dia em que você quiser animar a abertura vai mexer só nela.
🔎 Por baixo do capô
Com o menu fechado, o leitor de tela anuncia algo como "Menu, botão, recolhido". Depois do clique, "Menu, botão, expandido" — e a pessoa sabe que apareceu algo, mesmo sem enxergar a tela. É o aria-expanded que produz essa frase. Um <div> com uma classe .aberto não produz nada.
💡 Dica
O menu ainda não fecha com Esc, nem com um toque fora dele, nem devolve o foco ao botão. Essas três coisas são o exercício B3 de hoje — com dicas — e vão ficar naturais na Unidade 3. Faça o básico funcionar primeiro.
O sistema operacional guarda escolhas que a pessoa fez uma vez e espera ver respeitadas em todo lugar: tema escuro, menos animações, tamanho de fonte. O CSS lê essas escolhas por media queries — sem JavaScript e sem botão no seu site.
Como todas as cores do site vivem em variáveis desde a Aula 06, um tema escuro é redefinir as variáveis dentro de uma media query. Nenhum seletor de componente precisa mudar.
site-evento/css/estilo.css (seção 2 — variáveis; acrescente a variável nova e o bloco)
color-scheme: light dark avisa o navegador que a página suporta os dois temas. Ele passa a desenhar barras de rolagem, campos de formulário e a cor de fundo padrão de acordo com o tema — de graça.
A variável nova, --cor-sobre-primaria, existe porque no tema claro a primária é escura e o texto sobre ela é branco; no escuro a primária vira azul-claro e o texto sobre ela precisa ser escuro. Toda cor que "fica em cima" de outra cor precisa de variável própria. Na Mão na massa você troca o color: #fff do .menu__cta da Aula 07 por var(--cor-sobre-primaria).
⚠️ Atenção
Tema escuro não é "inverter as cores". Verifique o contraste de novo no WebAIM para cada par texto/fundo do tema escuro — o mínimo continua sendo 4.5:1. Fundos escuros costumam pedir texto um pouco menos branco (#e6edf3 em vez de #ffffff) para não "vibrar", e sombras mais fortes para continuarem visíveis.
Pessoas com distúrbios vestibulares configuram o sistema para reduzir animações, e o site precisa obedecer. O bloco abaixo desliga transições e animações quando essa preferência está ativa. Ele vai para o fim da folha de estilo, porque precisa vencer tudo:
site-evento/css/estilo.css (última regra do arquivo)
Hoje o único movimento do site é o sublinhado do menu; a Aula 09 acrescenta transições, transformações e animações, e volta a este bloco com calma — inclusive para explicar por que é uma das poucas situações em que !important se justifica.
A página de programação vai ser impressa por gente que quer levar a grade de horários no bolso. No papel, cabeçalho fixo, menu, rodapé e link de salto não fazem sentido:
site-evento/css/estilo.css (seção 7 — media queries)
A última regra imprime a URL ao lado de cada link externo — no papel ninguém clica. O attr(href) lê o atributo do próprio elemento, uma função que você já viu na Aula 06 com ::after.
🔬 Investigue
No DevTools, abra o menu de três pontos → More tools → Rendering. Role até Emulate CSS media feature prefers-color-scheme e alterne entre light e dark: o site troca de tema sem você tocar no sistema operacional. Logo abaixo, Emulate CSS media feature prefers-reduced-motion e, mais acima, Emulate CSS media type → print mostra a página como ela sairia na impressora. Deixe esse painel aberto durante a Mão na massa.
Ctrl+Shift+M (ou o ícone de celular no canto do DevTools). Escolha um aparelho na lista ou digite a largura à mão. Use o modo Responsive e arraste a borda devagar para achar os breakpoints. Ative Throttling → Slow 4G para sentir o carregamento em rede ruim. É rápido e ótimo para layout — mas simula: o toque vira clique de mouse, a barra de endereço não existe e as fontes do sistema são as do seu computador.
O Live Server serve a pasta do projeto na porta 5500. Do celular, na mesma rede Wi-Fi, acesse o endereço IP do computador nessa porta:
Descubra o IP do computador: ip addr (Linux), ipconfig (Windows) ou ifconfig (macOS). Procure algo como 192.168.0.15.
No navegador do celular, abra http://192.168.0.15:5500/site-evento/ (troque pelo seu IP).
Se não abrir, o Live Server pode estar ouvindo só em 127.0.0.1. Nas configurações do VS Code, defina liveServer.settings.host como 0.0.0.0, reinicie o Live Server e tente de novo. Se ainda não abrir, o firewall do computador está bloqueando a porta 5500.
No aparelho de verdade aparecem os problemas que o simulador esconde: o menu que não abre porque o botão tem 24 px e o dedo tem 40, a fonte que fica pequena demais, o campo de formulário que dá zoom sozinho, a altura de 100vh que fica escondida atrás da barra de endereço. É sobre isso a atividade assíncrona de hoje.
No DevTools, aba Lighthouse → marque Mobile → Analyze page load. Ele simula um celular de entrada em rede lenta e devolve notas de 0 a 100 em Performance, Acessibilidade, Boas práticas e SEO, com a lista do que corrigir. Problemas que ele pega e que você já sabe resolver: meta viewport ausente, texto menor que 12 px, imagens sem width/height, contraste abaixo de 4.5:1, alvos de toque menores que 48 × 48 px, user-scalable=no.
💡 Dica
Rode o Lighthouse em uma janela anônima. Extensões do navegador (bloqueadores de anúncio, tradutores) injetam scripts na página e derrubam a nota sem culpa sua.
Você vai transformar o site-evento/ da Aula 07 em um site mobile first completo. Trabalhe com o Live Server aberto, o DevTools no modo dispositivo em 360 px e uma segunda janela em 1440 px.
Antes de corrigir, meça. Abra index.html e programacao.html em 360 px e anote o que você vê. A lista deve parecer com esta:
Sintoma em 360 px
Causa provável
Menu com cinco links estoura para a direita
.menu é uma linha flex sem quebra
Lateral de filtros ocupa 240 px e esmaga o conteúdo
.pagina tem duas colunas fixas
Título do banner em quatro linhas
font-size fixo em rem
Rodapé com três colunas de 100 px cada
.rodape__grade tem três colunas fixas
Foto de palestrante achatada
<img> sem object-fit
Cada linha vira um passo abaixo.
Passo 2 — Reorganizar estilo.css para mobile first¶
Percorra a seção 4 (layout) do seu estilo.css e separe o que é base do que é só para telas grandes. A regra é simples: se uma declaração só faz sentido com espaço sobrando (duas ou mais colunas, gap grande, padding generoso), ela vai para uma media query na seção 7. O restante fica como base.
Ao fim da reorganização, a seção 7 do arquivo tem esta estrutura, ainda quase vazia:
site-evento/css/estilo.css (seção 7 — media queries)
CSS
/* 7. Media queries — mobile first, em ordem crescente de largura */@media(min-width:768px){/* tablet: menu em linha, rodapé em duas colunas */}@media(min-width:1024px){/* notebook: lateral ao lado do conteúdo, rodapé em três colunas */}@media(min-width:1440px){/* monitor grande: mais respiro */}@mediaprint{/* impressão */}
Os comentários vão sendo substituídos por regras nos passos seguintes. O bloco prefers-reduced-motion entra no passo 9, depois de tudo isso.
Confirme que a seção 3 (base) tem a regra de mídia e que a seção 4 tem o contêiner fluido; se não tiver, acrescente:
site-evento/css/estilo.css (seções 3 e 4)
CSS
/* 3. Base */img,video{max-width:100%;height:auto;}/* 4. Layout */.container{width:100%;max-width:var(--largura-max);margin-inline:auto;padding-inline:var(--espaco-medio);}.cartoes{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));gap:var(--espaco-medio);}
E, na seção 5 (componentes), a foto do cartão com proporção fixa. Antes de colar, apague a regra .cartao__foto da Aula 07 (a que fixava width: 120px, height: 120px, border-radius: 50% e object-fit: cover): .cartao img tem especificidade 0-1-1 e venceria .cartao__foto (0-1-0) de qualquer jeito, e duas regras disputando a mesma foto é exatamente o tipo de lixo que faz uma folha de estilo parar de ser previsível. Você pode manter a classe cartao__foto no HTML ou removê-la; o que não pode é deixar a regra órfã no CSS.
site-evento/css/estilo.css (seção 5)
CSS
.cartaoimg{width:100%;aspect-ratio:16/9;object-fit:cover;object-position:center;border-radius:var(--raio-borda)var(--raio-borda)00;}/* A foto do palestrante: quadrada, redonda e com o rosto preservado */.cartao--palestranteimg{width:120px;aspect-ratio:1/1;border-radius:50%;object-position:top;align-self:center;}
Os cartões de palestrantes.html já têm a classe cartao--palestrante ao lado de cartao desde a Aula 07 — confira que ela está lá nos seis, porque é ela que devolve a foto redonda de 120 px que a regra genérica acabou de sobrescrever.
Como verificar: em 360 px a grade de programação mostra um cartão por linha; em 768 px, dois; em 1100 px ou mais, três. Nenhuma media query foi escrita para isso.
Passo 4 — Página de programação: uma coluna, depois duas¶
A .pagina da Aula 07 tinha grid-template-columns: 240px 1fr como base. Inverta: a base é uma coluna, e a lateral só vai para o lado a partir de 1024 px.
No celular a lateral de filtros vem antes da lista — ela é curta e é útil filtrar antes de rolar. Se a lateral fosse longa (patrocinadores, links, avisos), bastaria trocar a ordem em grid-template-areas da base para "conteudo" "lateral" — sem tocar no HTML, que continua com <aside> antes de <section>.
Como verificar: em 360 px os filtros aparecem em cima e os cartões embaixo, ambos com a largura toda; a partir de 1024 px a lateral gruda à esquerda com 240 px.
Substitua o <nav> das cinco páginas pela marcação da seção 5, mantendo aria-current="page" no link da página correta em cada arquivo. Depois:
Na seção 5 de estilo.css, substitua o .menu da Aula 07 pelo CSS de celular da seção 5 desta aula (.menu-botao, .menu-botao__icone, .menu com display: none, o seletor .menu-botao[aria-expanded="true"] + .menu).
Na seção 7, dentro do bloco de 768 px, cole a query que esconde o botão e coloca o menu em linha.
Crie a pasta js/ e o arquivo js/menu.js com as oito linhas da seção 5.
Inclua <script src="js/menu.js" defer></script> no <head> das cinco páginas, logo depois do <link> do CSS.
Troque, no .menu__cta, color: #fff por color: var(--cor-sobre-primaria) — a variável entra no passo 8.
site-evento/index.html (<head> completo, como fica)
HTML
<head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metaname="description"content="Semana Acadêmica de Sistemas de Informação: palestras, minicursos e maratona de programação."><metaname="author"content="Semana Acadêmica de Sistemas de Informação"><title>Início — Semana Acadêmica de Sistemas de Informação</title><linkrel="preconnect"href="https://fonts.googleapis.com"><linkrel="preconnect"href="https://fonts.gstatic.com"crossorigin><linkhref="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"rel="stylesheet"><linkrel="stylesheet"href="css/estilo.css"><scriptsrc="js/menu.js"defer></script></head>
Os três <link> da fonte Inter e o <meta name="author"> vêm das Aulas 02 e 06 — eles continuam aqui. Quem copiar este bloco por cima do <head> antigo sem eles perde a tipografia do projeto inteiro.
Como verificar: em 360 px aparece o botão "Menu"; um clique abre a lista abaixo do cabeçalho, outro fecha. No DevTools, aba Elements, observe o atributo aria-expanded mudando a cada clique. Com Tab o botão recebe o anel de foco e Enter abre o menu. A partir de 768 px o botão some e os cinco links ficam em linha, como na Aula 07.
Como verificar: arraste a janela de 360 px a 1440 px e veja o título crescer de forma contínua, sem saltos. Dê zoom com Ctrl++ até 200 %: o texto cresce junto.
Na index.html, a seção de abertura ganha uma classe e uma altura que acompanha a tela:
site-evento/index.html (primeiro bloco dentro de <main>)
HTML
<sectionclass="hero"><divclass="container"><h1>Semana Acadêmica de Sistemas de Informação</h1><p>Três dias de palestras, minicursos e maratona de programação.</p><ahref="inscricao.html"class="botao">Inscreva-se</a></div></section>
site-evento/css/estilo.css (seção 5) — substitua o bloco .hero da Aula 07
⚠️ Atenção
O herói muda de forma: sai o Grid de duas colunas com texto de um lado e <img> do outro (Aula 07, Passo 5) e entra um banner de largura total com a foto como imagem de fundo. Apague as três regras antigas — .hero, .hero img e .hero__acoes — antes de colar as de baixo, e apague também o <div class="hero__texto">, a <img> e o segundo botão da marcação. Se as duas versões coexistirem, o display: grid da antiga briga com o text-align: center da nova e o resultado não é nem um nem outro.
O padding-block com clamp() faz o banner ter 48 px de respiro no celular e até 128 px no monitor. O center / cover posiciona e recorta a foto de fundo do mesmo jeito que object-fit: cover faz em um <img>. Na Aula 09 esse banner ganha um gradiente por cima da foto para garantir o contraste do texto.
Na seção 2 (variáveis), acrescente --cor-sobre-primaria: #ffffff; e color-scheme: light dark; ao :root, e logo abaixo o bloco @media (prefers-color-scheme: dark) da seção 6 desta aula, com os valores escuros de todas as variáveis de cor.
Depois, procure no arquivo inteiro por cores escritas "na mão" (#fff, #333, white) e troque cada uma pela variável correspondente. Duas que certamente existem desde a Aula 07: o color: #fff do .menu__cta (vira var(--cor-sobre-primaria)) e a cor de fundo do .cabecalho (deve ser var(--cor-superficie)).
Como verificar: com o painel Rendering do DevTools em prefers-color-scheme: dark, o site inteiro troca de tema; nenhum texto some, nenhum botão fica branco sobre branco. Confira o contraste de --cor-texto sobre --cor-fundo e de --cor-sobre-primaria sobre --cor-primaria no WebAIM.
Cole o bloco @media (prefers-reduced-motion: reduce) da seção 6 como última regra do arquivo, depois de todas as outras media queries.
Como verificar: no painel Rendering, ative prefers-reduced-motion: reduce. Passe o mouse sobre um link do menu: o sublinhado aparece de imediato, sem deslizar.
Como verificar: as três colunas do rodapé (sobre, links, contato) empilham em 360 px, ficam duas em 768 px (a terceira desce) e três a partir de 1024 px.
Cole a query @media print da seção 6 no fim da seção 7 (antes do bloco de prefers-reduced-motion). Abra programacao.html, pressione Ctrl+P e olhe a pré-visualização: só a grade de horários, em preto sobre branco, com as URLs dos links externos entre parênteses.
Abra cada uma das cinco páginas em três larguras e confira:
Largura
O que você deve ver
360 px
Botão "Menu" no cabeçalho; uma coluna em tudo (cartões, filtros, rodapé); título do banner em no máximo duas linhas; sem rolagem horizontal
768 px
Menu horizontal com cinco links; cartões em duas colunas; rodapé em duas colunas; filtros ainda acima do conteúdo
1440 px
Layout completo: lateral à esquerda com 240 px, cartões em três colunas, rodapé em três colunas, contêiner parado em 1100 px
Depois, os três testes da seção 7: modo dispositivo em "Pixel 7" e "iPad", o celular real pelo IP do computador, e o Lighthouse Mobile em index.html — a meta é nenhum alerta de viewport, fonte ou alvo de toque. Guarde a captura do relatório: ela entra no checkpoint.
A1. Por que a meta viewport é indispensável para responsividade? Descreva, em duas frases, o que o celular faz com uma página que não a declara.
A2. Qual a diferença entre a abordagem mobile first e desktop first na escrita das media queries? Qual delas usa min-width e qual usa max-width, e por que a primeira gera CSS "aditivo"?
A3. Escreva uma media query que aplique regras apenas entre 768 px e 1023 px de largura.
A4. O que faz img { max-width: 100%; height: auto; } e por que é uma regra praticamente obrigatória? O que aconteceria com a proporção de uma imagem que tem width="800" height="600" no HTML se faltasse o height: auto?
A5. Escreva a meta viewport correta e explique cada um de seus dois valores.
A6. Por que os breakpoints devem ser definidos pelo conteúdo e não por modelos de aparelho? Descreva o método prático para encontrá-los.
A7. O que faz clamp(1rem, 2.5vw, 1.5rem) aplicado a font-size? Calcule o valor resultante em uma tela de 320 px, de 800 px e de 1200 px.
A8. Explique o que prefers-color-scheme permite fazer e escreva um exemplo que troque as cores de fundo e de texto de um site para o tema escuro.
A9. Por que um menu hambúrguer deve ser um <button> com aria-expanded, e não um <div> ou um <a href="#">? Liste três diferenças práticas.
A10. Dada a grade grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)) dentro de um contêiner de 1000 px com gap: 20px, quantas colunas o navegador cria e qual a largura de cada uma? E se houver só dois itens na grade — o que muda entre auto-fit e auto-fill?
A11. O CSS abaixo foi escrito mobile first, mas o estudante reclama que "a media query não funciona". Encontre o erro sem rodar o código:
B1. Crie uma galeria de 12 imagens com CSS Grid usando auto-fit / minmax, gap de 16 px, imagens com aspect-ratio: 4 / 3 e object-fit: cover, e efeito de scale no :hover com transição. Use fotos livres (Unsplash, Pexels) ou o serviço https://picsum.photos/600/450?random=1 mudando o número final.
Resultado esperado: em 360 px uma coluna; em 768 px duas; em 1200 px três ou quatro, sem nenhuma media query. Todas as imagens têm a mesma proporção, nenhuma deforma, e passar o mouse amplia a foto dentro da moldura sem estourar o cartão.
Dica
Para o zoom não vazar da moldura, o elemento que envolve a imagem precisa de overflow: hidden. A transição vai no img (estado normal), não no :hover, senão a volta é abrupta — a Aula 09 explica por quê. Replique o efeito em :focus-within se cada imagem for um link.
B2. Pegue o site institucional que você criou no exercício C1 da Aula 02 (exercicios/aula02/curso/) e torne-o totalmente responsivo, com pelo menos 3 breakpoints. Documente em comentários no CSS por que escolheu cada breakpoint — qual elemento quebrou e em que largura.
Resultado esperado: nenhuma rolagem horizontal entre 320 px e 1920 px; menu, tabela, formulário e imagens legíveis e usáveis no celular; três blocos @media (min-width) em ordem crescente, cada um com um comentário de uma linha justificando a largura.
Dica
Comece pela meta viewport e pela regra de mídia; só então redimensione. A tabela é o elemento que mais resiste: se ela não couber, envolva-a em um <div> com overflow-x: auto como solução mínima — o B4 mostra a solução completa.
B3. Implemente um menu responsivo completo: horizontal acima de 768 px, hambúrguer abaixo. Requisitos: <button> com aria-expanded e aria-controls, abertura animada por transform, fechamento pelo Esc e pelo clique fora, foco preso dentro do menu enquanto aberto e devolvido ao botão ao fechar.
Resultado esperado: o menu abre e fecha pelo botão, por Esc e por um toque fora dele; com o menu aberto, Tab circula só entre o botão e os links do menu; ao fechar, o foco volta ao botão; a abertura desliza em vez de aparecer de repente.
Dica
Este é um exercício-ponte para a Unidade 3; faça uma exigência de cada vez, sobre o menu.js da aula. Fechar com Esc: document.addEventListener('keydown', (evento) => { if (evento.key === 'Escape') fechar(); }). Clique fora: um click no document que só fecha se !nav.contains(evento.target). Foco preso: pegue todos os links do menu com menu.querySelectorAll('a'), e em um keydown de Tab no último link (ou Shift+Tab no primeiro) chame evento.preventDefault() e .focus() no outro extremo. Devolver o foco: botao.focus() dentro de fechar(). Para animar, troque display: none por opacity: 0; visibility: hidden; transform: translateY(-8px) e a Aula 09 explica o resto.
B4. Construa uma tabela responsiva: em telas largas, tabela normal; abaixo de 640 px, cada linha vira um cartão com os rótulos das colunas via data-* e ::before. Mantenha a marcação semântica de tabela. Use a grade de horários da programação do evento (dia, horário, atividade, local).
Resultado esperado: acima de 640 px, uma <table> comum com <thead>; abaixo, cada <tr> aparece como um bloco com borda, e cada célula mostra "Horário: 19h00", "Local: Auditório", com o rótulo em negrito à esquerda do valor.
Dica
Cada <td> recebe data-rotulo="Horário" no HTML. Na media query de max-width: 639px: thead { position: absolute; left: -9999px; } (esconde sem tirar do leitor de tela), tr { display: block; margin-bottom: 1rem; border: 1px solid var(--cor-borda); }, td { display: flex; gap: 1rem; } e td::before { content: attr(data-rotulo); font-weight: 600; min-width: 6rem; }. Não use display: none no <thead> — o leitor de tela perderia os cabeçalhos.
B5. Torne o site do seu projeto autoral totalmente responsivo em três breakpoints e registre capturas de tela em 360 px, 768 px e 1440 px de cada página.
Resultado esperado: para cada uma das cinco páginas, três capturas nomeadas pagina-360.png, pagina-768.png e pagina-1440.png em uma pasta capturas/; nenhuma mostra rolagem horizontal; o menu está aberto em pelo menos uma captura de 360 px.
Dica
No modo dispositivo do DevTools, o menu de três pontos ao lado da largura tem Capture full size screenshot, que salva a página inteira de uma vez na largura escolhida. Faça primeiro o index.html, valide os três tamanhos, e só então replique o padrão nas outras páginas — o CSS é o mesmo.
C1. Construa portal/index.html + portal/css/portal.css, um portal de notícias acadêmicas responsivo, com:
Estrutura (Grid, com áreas nomeadas): cabeçalho, barra lateral, conteúdo principal e rodapé.
Cabeçalho (Flexbox): logo à esquerda, menu horizontal à direita, position: sticky; top: 0 com sombra.
Conteúdo principal (Grid): um destaque ocupando 2 colunas, seguido de 6 cartões em repeat(auto-fit, minmax(280px, 1fr)). Cada cartão: imagem com object-fit: cover, categoria, título, resumo, data e link.
Barra lateral (Flexbox em coluna): caixa de busca, lista de mais lidos, lista de tags.
Responsividade: até 767 px, uma coluna, lateral depois do conteúdo, menu vira lista vertical; de 768 px a 1023 px, cartões em 2 colunas, lateral ainda abaixo; a partir de 1024 px, layout completo com lateral à esquerda.
Requisitos obrigatórios: mobile first; variáveis CSS; gap no lugar de margens entre itens; :focus visível; contraste AA; nenhuma media query para a grade de cartões (use auto-fit / minmax).
Dica
Comece pelo esqueleto de áreas na base: grid-template-areas: "cabecalho" "conteudo" "lateral" "rodape" — a lateral já fica depois do conteúdo no celular sem mexer no HTML. Em 1024 px redesenhe: "cabecalho cabecalho" "lateral conteudo" "rodape rodape" com grid-template-columns: 260px 1fr. O destaque de 2 colunas é grid-column: span 2 dentro da grade de cartões — em 360 px só há uma coluna, e o span 2 estoura; proteja com grid-column: 1 / -1 (da primeira à última linha-guia, quantas houver). O rodapé de 4 colunas segue o mesmo repeat(auto-fit, minmax(200px, 1fr)).
Você escreveu repeat(auto-fit, minmax()) hoje e talvez ainda não tenha certeza de por que funciona — porque decorar a linha não é o mesmo que enxergar as linhas-guia da grade. O Grid Garden é um jogo de 28 fases em que cada regra CSS rega uma horta; ele obriga a pensar em linhas-guia, span, valores negativos e grid-template-areas sem nenhum atalho.
Critérios de pronto
Captura de tela da fase 28 concluída, com o seu nome visível na página (escreva-o em um comentário no editor do jogo antes do print).
Um arquivo grid-garden.md com três propriedades ou valores que você não conhecia antes do jogo, cada um com uma linha explicando o que faz.
Um exemplo real: aplique grid-column: 1 / -1 em algum elemento do seu projeto autoral (o destaque da página inicial, por exemplo) e explique em um comentário por que -1 é melhor que span 3.
Pistas
O jogo está em https://cssgridgarden.com/#pt-br, em português.
As fases 14 a 18 usam números de linha negativos: -1 é sempre a última linha-guia, não importa quantas colunas a grade tenha.
As fases 22 em diante mostram grid-template e grid-area juntos — releia a seção "Áreas nomeadas" da Aula 07 antes de tentar.
⭐
⭐ Caça ao bug: o site que só funciona no monitor de quem escreveu¶
bugresponsivocssdevtools
Alguém jura que "o site está responsivo, testei no meu monitor". No celular ele aparece minúsculo, com rolagem horizontal, imagem estourando e um menu que nunca vira hambúrguer. Há seis erros entre o HTML e o CSS abaixo. Encontre todos sem "reescrever do zero" — o desafio é diagnosticar, não refazer.
Um arquivo diagnostico.md com uma tabela Erro | Sintoma que ele causa | Correção, com as seis linhas.
O CSS corrigido, mobile first, sem rolagem horizontal em 320 px e com o menu virando coluna no celular.
Uma captura do DevTools em 360 px antes e outra depois da correção.
Pistas
Um dos erros não está no CSS. Sem ele, o celular finge ter 980 px e nenhuma query de max-width: 768px dispara.
Duas larguras fixas em pixels empurram o contêiner para além da tela: uma em um bloco, outra em uma imagem.
A media query do menu está escrita desktop first e "funcionaria" — mas pense no que acontece se, em vez de max-width, o projeto inteiro for migrado para min-width: o que precisa virar base e o que precisa ir para a query?
auto-fill com apenas três cartões deixa colunas vazias à direita em telas largas; e 64px num <h1> de 360 px ocupa quatro linhas. clamp() resolve o segundo.
O menu.js de hoje tem oito linhas — mas dá para ter zero. Um <input type="checkbox"> escondido, um <label> que faz papel de botão e a pseudoclasse :checked da Aula 06 conseguem abrir e fechar o menu só com CSS. Construa essa versão, faça funcionar, e então responda à pergunta que importa: o que ela perde em relação à versão com <button> e aria-expanded? Descubra testando com o teclado e com um leitor de tela (NVDA no Windows, VoiceOver no macOS, TalkBack no Android).
Critérios de pronto
Menu que abre e fecha em 360 px sem nenhum <script>, e vira horizontal a partir de 768 px.
O <label> é alcançável por Tab e o menu abre com Espaço (dica: isso exige que o checkbox continue focável — não use display: none nele).
Um arquivo comparacao.md com uma tabela Critério | Versão com button | Versão com checkbox cobrindo: o que o leitor de tela anuncia ao focar o controle, se o estado aberto/fechado é anunciado, se Enter abre o menu, e quanto código cada versão tem.
Uma conclusão de três linhas: qual versão você usaria no projeto autoral, e por quê.
Pistas
O seletor é o mesmo raciocínio de hoje, trocando o atributo pela pseudoclasse: #abrir-menu:checked ~ .menu { display: flex; } — o combinador ~ (irmão geral) alcança a lista mesmo com o <label> no meio.
Para esconder o checkbox sem tirá-lo do teclado, use a mesma técnica do link de salto da Aula 07: position: absolute; left: -9999px, ou a classe utilitária "visualmente oculto" (clip, width: 1px, height: 1px, overflow: hidden).
O anel de foco precisa aparecer no <label>, não no checkbox invisível: #abrir-menu:focus-visible + label { outline: 3px solid var(--cor-secundaria); }.
No leitor de tela, um checkbox é anunciado como "caixa de seleção, não marcada" — compare com "botão, recolhido" e pense em qual frase ajuda mais quem não vê a tela.
Para ir além: procure o elemento <details>/<summary> do HTML e tente uma terceira versão do menu com ele — sem JavaScript e com semântica de "expansível" de graça.
Rode o Lighthouse Mobile no index.html do seu projeto autoral agora, antes de mexer em qualquer coisa, e guarde o relatório. É comum a primeira nota de Performance ficar abaixo de 60 em um site com fotos grandes, e a de Acessibilidade tropeçar em contraste e alvos de toque. O desafio é chegar a 90 ou mais nas duas — e cada ponto ganho vai ter um motivo técnico que você consegue explicar.
Critérios de pronto
Relatório do Lighthouse Mobile "antes" e "depois" salvos como lighthouse-antes.html e lighthouse-depois.html (o botão de três pontos do relatório exporta), com Performance ≥ 90 e Acessibilidade ≥ 90 no "depois", em todas as cinco páginas.
Imagens servidas com srcset em pelo menos três larguras, com width e height declarados, e a maior versão com no máximo 200 KB.
Tabela de dados responsiva (a técnica do B4) em pelo menos uma página.
Tema escuro via prefers-color-scheme com todos os pares de contraste ≥ 4.5:1, verificados no WebAIM e listados em contraste.md.
Um melhorias.md com, para cada alerta do "antes", a correção aplicada e a seção da aula que a explica.
Isso entra bem no Marco 2 da unidade.
Pistas
Abra o relatório "antes" e clique em cada alerta: o Lighthouse mostra o elemento exato e um link "Learn more" para a explicação oficial — comece pelos itens marcados em vermelho.
Para gerar as três larguras de cada foto sem software pago, o Squoosh (https://squoosh.app) redimensiona e converte para WebP no navegador; guarde a versão JPG como reserva no src.
Fontes do Google carregadas com <link> custam requisições e podem derrubar Performance. Meça antes de decidir: rode o Lighthouse com a Inter do projeto (que já vem com display=swap e preconnect desde a Aula 06) e depois com a font-family trocada por Arial, sans-serif, e registre a diferença em pontos e em milissegundos de LCP. Se a perda for pequena, a identidade visual vale o preço — a decisão é sua, mas com número.
Alvos de toque menores que 48 × 48 px aparecem em "Tap targets": aumente o padding dos links do menu e do rodapé em vez de aumentar a fonte.
Parte 1 — Leitura (15 min). SILVA, Maurício Samy. Criando sites com HTML, capítulo de design responsivo. MDN: Media queries (https://developer.mozilla.org/pt-BR/docs/Web/CSS/CSS_media_queries) — leia a página de introdução e a de "Usando media queries"; anote a sintaxe de intervalo e as três preferências do usuário que você viu hoje.
Parte 2 — Produção (40 min). No seu projeto autoral:
O exercício B5 completo: o site responsivo em três breakpoints (mobile first, queries agrupadas no fim do estilo.css em ordem crescente), com as capturas de tela de cada página em 360 px, 768 px e 1440 px na pasta capturas/.
O menu responsivo do exercício B3 funcionando em todas as páginas — no mínimo o básico da aula (<button>, aria-expanded, CSS mobile first, menu.js com defer); os fechamentos por Esc e por clique fora valem como extra.
Tema escuro via prefers-color-scheme e o bloco prefers-reduced-motion no fim do arquivo.
Parte 3 — Teste no celular (5 min). Abra o seu site em um celular real pelo IP do computador (ou peça o de outra pessoa). Registre (no fórum da turma, se você está cursando esta trilha em grupo, ou nas suas anotações) um problema que só apareceu no aparelho de verdade e não no simulador do DevTools — botão pequeno demais para o dedo, fonte ilegível, 100vh escondido pela barra de endereço, campo que dá zoom sozinho — e como você o resolveu.
Critério de pronto: nenhuma rolagem horizontal em 320 px em nenhuma página; menu hambúrguer abre e fecha por toque, por clique e por Enter no teclado, com aria-expanded mudando no DevTools; capturas das cinco páginas nas três larguras; tema escuro sem nenhum par de contraste abaixo de 4.5:1; Lighthouse Mobile sem alerta de viewport, fonte pequena ou alvo de toque.
Guarde no seu repositório: commit + push (ou a pasta do projeto, se ainda não usa Git).
Ao fim desta aula, o repositório do seu projeto autoral deve ter:
[ ] Meta viewport correta no <head> das cinco páginas.
[ ] css/estilo.css escrito mobile first: regras base para o celular, blocos @media (min-width: 768px), (min-width: 1024px) e, se necessário, (min-width: 1440px) agrupados na seção 7, em ordem crescente.
[ ] img, video { max-width: 100%; height: auto; } na seção base e nenhuma largura fixa em pixels em contêineres.
[ ] Grade de cartões com repeat(auto-fit, minmax(min(280px, 100%), 1fr)) e sem media query própria.
[ ] Fotos dos cartões com aspect-ratio e object-fit: cover; imagem principal com srcset, sizes, width e height.
[ ] Tipografia fluida com clamp() em body, h1 e h2; texto corrido limitado a 65ch.
[ ] Menu hambúrguer com <button aria-expanded aria-controls>, js/menu.js carregado com defer, e menu horizontal a partir de 768 px.
[ ] Layout de duas colunas (lateral + conteúdo) só a partir de 1024 px, via grid-template-areas.
[ ] Tema escuro com prefers-color-scheme, variável --cor-sobre-primaria e color-scheme: light dark.
[ ] Bloco prefers-reduced-motion como última regra do arquivo.
[ ] @media print escondendo cabeçalho, menu e rodapé.
[ ] Pasta capturas/ com as quinze imagens (cinco páginas × três larguras).
web.dev — Learn CSS: https://web.dev/learn/css — os módulos Sizing units, Layout e Logical properties explicam clamp(), dvh e margin-inline em profundidade.
web.dev — Learn Responsive Design: https://web.dev/learn/design — um curso inteiro sobre o assunto de hoje, incluindo imagens responsivas, tipografia e container queries, o próximo passo depois das media queries.
SILVA, Maurício Samy. Criando sites com HTML: sites de alta qualidade com HTML e CSS. Novatec, 2008 — capítulo de design responsivo.
TERUEL, Evandro C. HTML 5 — Guia Prático. Saraiva, 2014 — seções sobre viewport e mídias.
Na próxima aula o site ganha movimento: transições que suavizam o :hover dos cartões e do menu, transformações 2D e 3D, animações com @keyframes — e, principalmente, como medir no painel Performance do DevTools por que animar transform é barato e animar left trava o celular. O bloco prefers-reduced-motion que você colou hoje no fim do arquivo vai fazer todo o sentido.
Aplicar transições para suavizar mudanças de estado em botões, links, cartões e menus, escolhendo duração e curva adequadas.
Usar transformações 2D e 3D (translate, scale, rotate, skew, perspective) para mover, girar e escalar elementos sem afetar o layout dos vizinhos.
Criar animações com @keyframes e controlá-las com as propriedades de animation, incluindo animações escalonadas e múltiplas.
Explicar o pipeline de renderização (layout → paint → composite) e escolher propriedades baratas (transform e opacity) para animar sem travar a interface.
Respeitar prefers-reduced-motion e os critérios de acessibilidade da WCAG sobre movimento, piscadas e pausa.
Construir efeitos visuais comuns — sombras, gradientes, filtros, backdrop-filter, efeitos de hover e revelação ao rolar — com feedback também para quem navega por teclado.
Medir o desempenho de uma animação no painel Performance do DevTools e provar, com números, que transform é mais barato que left.
[ ] Site do evento (site-evento/) com as cinco páginas estilizadas pelo css/estilo.css da Aula 06, layout Grid + Flexbox e menu acessível da Aula 07, e responsivo com menu hambúrguer, tema escuro e js/menu.js da Aula 08.
[ ] Seu projeto autoral no mesmo estágio (responsivo em três larguras).
[ ] VS Code com Live Server; Chrome ou Firefox com DevTools — hoje você vai usar o painel Performance e a aba Rendering.
[ ] Um celular na mesma rede Wi-Fi do computador (para sentir a diferença de desempenho de verdade).
Na aula passada você fez o site do evento se adaptar a qualquer tela: mobile first, três breakpoints, menu hambúrguer com aria-expanded, imagens fluidas e tema escuro com prefers-color-scheme. O site funciona em qualquer lugar — mas tudo nele acontece de repente: o menu aparece num estalo, o botão muda de cor sem aviso, os cartões surgem prontos. Hoje você dá movimento ao site, com transições, transformações e animações que comunicam algo, custam pouco para o navegador e respeitam quem prefere menos movimento. Esta aula fecha a Unidade 2.
Animação em interface não é enfeite. Ela serve a três funções concretas:
Função
Exemplo
Feedback
O botão escurece e afunda ao ser pressionado — o clique foi registrado
Continuidade
Um painel desliza para dentro — o usuário entende de onde ele veio e para onde vai voltar
Atenção
Uma mensagem de erro treme levemente — o olho é direcionado ao campo com problema
Pense no menu hambúrguer da Aula 08. Quando ele aparece de repente, o cérebro precisa de um instante para entender "de onde veio isso?". Quando ele desliza de cima para baixo em 250 ms, a origem é óbvia: saiu do botão. É a mesma quantidade de informação, entregue com menos esforço.
⚠️ Atenção
Regra de ouro: se a animação não comunica nada, remova-a. Movimento gratuito cansa, atrasa a interação (o usuário espera a animação acabar para clicar) e prejudica pessoas com sensibilidade vestibular. Toda animação desta aula precisa responder à pergunta "o que isso está dizendo ao usuário?".
🧠 Você sabia?
Os "12 princípios da animação" foram formulados por dois animadores da Disney, Frank Thomas e Ollie Johnston, no livro The Illusion of Life — décadas antes de existir CSS. Vários deles migraram direto para as interfaces: ease in / ease out (nada no mundo físico começa nem para de repente), anticipation (um botão que "afunda" antes de abrir um painel) e follow through (o painel que passa um pouquinho do ponto e volta — o efeito de ricochete do cubic-bezier). As diretrizes de movimento do Material Design, da Apple e da Microsoft citam esses princípios explicitamente.
Uma transição interpola automaticamente a mudança de um valor entre dois estados. Você declara os dois estados (normal e :hover, por exemplo) e diz ao navegador "em vez de trocar de repente, leve 200 ms para ir de um ao outro". O navegador calcula todos os valores intermediários, quadro a quadro.
Passe o mouse sobre o botão: a cor muda suavemente e ele sobe 2 px. Tire o mouse: ele desce e volta à cor original, também suavemente — porque a transition está declarada no estado normal, e vale para os dois sentidos.
.botao{transition-property:background-color,transform;/* o que anima */transition-duration:200ms;/* quanto tempo */transition-timing-function:ease;/* a curva de velocidade */transition-delay:0s;/* espera antes de começar */}/* Atalho: propriedade duração curva atraso */.painel{transition:transform300msease-out100ms;}
O atalho transition aceita várias transições separadas por vírgula, cada uma com sua duração e curva. Na prática você quase sempre usa o atalho.
📌 Vale gravar
A transition deve ser declarada no estado base do elemento (.botao), não no estado de interação (.botao:hover). Se ficar no :hover, a entrada é suave e a saída é abrupta — porque ao tirar o mouse a regra :hover deixa de valer, e com ela a transition.
A curva define como a velocidade varia ao longo da transição. É o que separa um movimento "mecânico" de um movimento natural.
Valor
Comportamento
Uso típico
linear
Velocidade constante
Rotação contínua, barras de progresso
ease
Acelera e desacelera (padrão)
Uso geral
ease-in
Começa devagar
Elemento saindo da tela
ease-out
Termina devagar
Elemento entrando na tela
ease-in-out
Devagar nas duas pontas
Movimentos longos
cubic-bezier(.34, 1.56, .64, 1)
Curva personalizada
Efeito de "ricochete" (passa do ponto e volta)
steps(5, end)
Saltos discretos
Sprites, efeito de digitação
A regra de bolso: o que entra usa ease-out (chega rápido e assenta devagar, como um carro estacionando); o que sai usa ease-in (parte devagar e some rápido). Use o site cubic-bezier.com para desenhar curvas visualmente e comparar duas lado a lado.
Painéis, modais, elementos grandes que percorrem distância
acima de 500 ms
Percebido como lento e irritante
Elementos pequenos que se movem pouco precisam de menos tempo; elementos grandes que atravessam a tela precisam de mais. Na dúvida, comece em 200 ms e ajuste olhando.
⚠️ Atençãotransition: all é uma armadilha. Ele anima todas as propriedades que mudarem, inclusive as que você não pretendia — e as caras (width, height, margin). Pior: quando você acrescentar CSS meses depois, coisas passam a animar sem você entender por quê. Liste as propriedades explicitamente, sempre.
Só propriedades com valores numericamente interpoláveis transicionam. O navegador precisa conseguir calcular "o valor no meio do caminho" entre o estado inicial e o final.
Não funcionam:display, font-family, position, flex-direction, grid-template-columns (em alguns navegadores), visibility (tem regra especial, veja abaixo).
Não existe "meio caminho" entre display: none e display: block. Por isso o exemplo abaixo simplesmente não anima:
CSS
/* Errado: display não interpola — o painel aparece de repente */.painel{display:none;transition:display300ms;}
A solução clássica combina opacity (que interpola) com visibility (que tira o elemento da navegação por teclado e do leitor de tela):
CSS
/* Certo: opacity anima; visibility esconde do teclado e do leitor de tela */.painel{opacity:0;visibility:hidden;transition:opacity300msease,visibility0s300ms;}.painel.ativo{opacity:1;visibility:visible;transition:opacity300msease,visibility0s;}
Repare no truque do visibility. Ao esconder, visibility 0s 300ms espera os 300 ms do fade terminar e só então vira hidden — senão o painel sumiria instantaneamente antes de desvanecer. Ao mostrar, visibility 0s (sem atraso) torna o elemento visível na hora, e o opacity faz o fade de entrada.
🔎 Por baixo do capô
Por que opacity: 0 sozinho não basta? Porque um elemento transparente continua no fluxo e continua focável: quem navega por Tab cai em links invisíveis, e o leitor de tela lê o conteúdo de um painel que "não está lá". visibility: hidden resolve os dois problemas. Navegadores recentes começam a aceitar transition-behavior: allow-discrete junto com @starting-style para animar até display, mas a dupla opacity + visibility funciona em todos e é o que você vai usar nesta trilha.
transform altera a renderização do elemento sem afetar o layout dos vizinhos. Um elemento com transform: translateX(50px) é desenhado 50 px à direita, mas para o resto da página ele continua exatamente onde estava — ninguém é empurrado, nada é recalculado. É por isso que transform é barato (a §6 mostra o quanto).
CSS
/* Translação: move sem tirar do fluxo */.a{transform:translateX(20px);}.b{transform:translateY(-10px);}.c{transform:translate(20px,-10px);}/* Escala: 1 = tamanho original */.d{transform:scale(1.05);}/* 5% maior nos dois eixos */.e{transform:scaleX(2)scaleY(0.5);}/* estica na horizontal, achata na vertical *//* Rotação: graus ou voltas */.f{transform:rotate(45deg);}.g{transform:rotate(-0.25turn);}/* um quarto de volta no sentido anti-horário *//* Inclinação */.h{transform:skewX(15deg);}/* Combinadas — a ORDEM importa */.i{transform:translateX(50px)rotate(45deg);}/* move, depois gira no lugar */.j{transform:rotate(45deg)translateX(50px);}/* gira o eixo, depois move ao longo do eixo girado */
Sobre a ordem: as funções são aplicadas da esquerda para a direita, sobre o sistema de coordenadas do elemento. Em .i, o elemento desliza 50 px para a direita e então gira em torno do próprio centro. Em .j, o elemento primeiro gira — e com ele giram os eixos X e Y — e só então anda 50 px "para a direita", que agora aponta para a diagonal. Os dois terminam em lugares diferentes. Teste no DevTools trocando a ordem e veja.
Toda transformação acontece em torno de um ponto. O padrão é o centro do elemento; transform-origin muda isso.
CSS
.padrao{transform-origin:center;}/* padrão: gira/escala em torno do centro */.canto{transform-origin:topleft;}/* gira em torno do canto superior esquerdo */.base{transform-origin:50%100%;}/* meio da borda inferior — um pêndulo */
Um ponteiro de relógio gira em torno da base, não do centro; um menu suspenso "cresce" a partir do topo; o sublinhado do menu do site do evento cresce a partir da esquerda. Sempre que a origem padrão parecer estranha, é transform-origin que resolve.
Com perspective no contêiner pai, o navegador passa a projetar as transformações 3D dos filhos com profundidade. O exemplo clássico é o cartão que vira para mostrar o verso — a estrutura tem três camadas: o palco (com a perspectiva), o virador (que gira) e as duas faces.
exemplos/cartao-3d.html
HTML
<divclass="virador"><divclass="virador__faces"><divclass="virador__face virador__frente"><imgsrc="img/palestrante-ana.jpg"alt=""><h3>Ana Souza</h3></div><divclass="virador__face virador__verso"><p>Engenheira de software, trabalha com sistemas distribuídos e ensina Go.</p><ahref="palestrantes.html#ana">Ver palestra</a></div></div></div>
exemplos/cartao-3d.css
CSS
.virador{perspective:1000px;/* no CONTÊINER pai — nunca no elemento que gira */width:280px;height:360px;}.virador__faces{position:relative;width:100%;height:100%;transform-style:preserve-3d;/* os filhos continuam em 3D, e não achatados */transition:transform600msease;}.virador__face{position:absolute;inset:0;backface-visibility:hidden;/* esconde a face que está de costas para você */border-radius:var(--raio-borda);background:var(--cor-superficie);box-shadow:var(--sombra-cartao);}.virador__verso{transform:rotateY(180deg);/* começa virado para trás */}.virador:hover.virador__faces,.virador:focus-within.virador__faces{transform:rotateY(180deg);}
Quanto menor o valor de perspective, mais "perto" está o observador e mais exagerada fica a distorção. Valores entre 800 px e 1200 px parecem naturais. O :focus-within faz o cartão virar também quando o link do verso recebe foco por teclado — sem isso, quem não usa mouse jamais veria o verso.
Transições precisam de uma mudança de estado (hover, classe adicionada, foco). Animações rodam sozinhas, podem ter vários passos intermediários e podem se repetir.
Uma animação tem duas partes: a definição dos quadros-chave (@keyframes) e a aplicação a um elemento (animation).
O navegador interpola entre cada par de quadros consecutivos: de 0 % a 50 % o elemento cresce e clareia; de 50 % a 100 % volta. Com infinite, recomeça para sempre.
Quando há só dois passos, from e to são mais legíveis:
.cartao{animation-name:surgir;animation-duration:400ms;animation-timing-function:ease-out;animation-delay:100ms;animation-iteration-count:1;/* ou infinite, ou um número */animation-direction:normal;/* reverse | alternate | alternate-reverse */animation-fill-mode:forwards;/* none | forwards | backwards | both */animation-play-state:running;/* paused */}/* Atalho: nome duração curva atraso repetições direção preenchimento */.cartao{animation:surgir400msease-out100ms1normalforwards;}
Um detalhe do atalho: quando há dois valores de tempo, o primeiro é a duração e o segundo é o atraso. animation: surgir 400ms 100ms significa "dura 400 ms, começa depois de 100 ms".
⚠️ Atençãoanimation-fill-mode é o que quase todo mundo esquece. Sem ele, quando a animação termina o elemento volta ao estado original — o cartão que surgiu do nada desaparece de novo. Com forwards, o último quadro permanece aplicado. Com backwards, o primeiro quadro é aplicado durante o animation-delay (útil em animações escalonadas: o elemento fica invisível enquanto espera a sua vez). both faz as duas coisas.
animation-direction: alternate inverte o sentido a cada repetição (vai e volta, como um pêndulo), o que evita o "salto" no fim de cada ciclo de uma animação infinite. reverse roda a animação de trás para frente, sempre.
O var(--i, 0) tem um valor de reserva: se algum cartão vier sem --i, ele anima sem atraso, em vez de quebrar. Repare no backwards: sem ele, os cartões que ainda estão esperando o atraso ficariam visíveis no estado final e só "piscariam" quando a animação começasse.
O balão surge em 400 ms e, quando termina de surgir, passa a flutuar para sempre. Cuidado: se duas animações mexem na mesma propriedade (aqui, as duas usam transform), a última declarada vence enquanto estiver rodando.
🔎 Por baixo do capô
Quando você aplica animation a um elemento, o navegador cria uma linha do tempo interna para ele e, a cada quadro (idealmente 60 por segundo), calcula em que ponto da linha ele está, interpola os valores dos dois quadros-chave vizinhos e reaplica o estilo. Tudo isso acontece fora do fluxo de JavaScript da página — se a animação usa só transform e opacity, ela roda até com a thread principal ocupada. É a diferença entre "o botão continua girando enquanto a página carrega" e "o botão trava".
Cinco animações que aparecem em quase todo projeto. Copie para o seu css/estilo.css conforme precisar — todas usam só transform, opacity ou background-position.
css/estilo.css (seção 5 — componentes)
CSS
/* Carregando — giro contínuo */@keyframesgirar{to{transform:rotate(360deg);}}.spinner{width:32px;height:32px;border:3pxsolidvar(--cor-borda);border-top-color:var(--cor-secundaria);border-radius:50%;animation:girar700mslinearinfinite;}/* Esqueleto de carregamento — brilho que percorre a caixa */@keyframesbrilho{to{background-position-x:-200%;}}.esqueleto{background:linear-gradient(90deg,var(--cor-esqueleto)40%,var(--cor-esqueleto-brilho)50%,var(--cor-esqueleto)60%);background-size:200%100%;border-radius:var(--raio-borda);animation:brilho1.4slinearinfinite;}/* Balançar — campo inválido */@keyframestremer{0%,100%{transform:translateX(0);}20%,60%{transform:translateX(-6px);}40%,80%{transform:translateX(6px);}}.campo.invalido{animation:tremer400msease;}/* Entrada lateral */@keyframesentrarEsquerda{from{opacity:0;transform:translateX(-24px);}to{opacity:1;transform:translateX(0);}}/* Flutuação sutil — para um ícone ou ilustração de destaque */@keyframesflutuar{0%,100%{transform:translateY(0);}50%{transform:translateY(-8px);}}
O @keyframes girar só tem to: quando falta o from, o navegador usa o estado atual do elemento como ponto de partida (rotate(0deg)). O esqueleto usa um gradiente com o dobro da largura da caixa e desloca a posição do fundo — o "brilho" do meio parece passar de um lado ao outro.
💡 Dica
Dê nomes de @keyframes que descrevam o movimento (surgir, tremer, girar), não o uso (animacaoDoCartao). Assim a mesma animação serve para cartões, alertas e imagens, e você constrói aos poucos uma biblioteca reutilizável — o desafio ⭐⭐⭐ desta aula é exatamente isso.
O navegador desenha uma página em etapas. Entender essas etapas é o que separa uma animação que roda lisa no celular de uma que trava.
Layout (também chamado de reflow): o navegador calcula a posição e o tamanho de cada elemento. Se um elemento muda de largura, todos os vizinhos, os filhos e às vezes a página inteira precisam ser recalculados.
Paint: o navegador pinta os pixels de cada elemento — cores, bordas, sombras, texto.
Composite: o navegador junta as camadas já pintadas na tela, com posição, escala e opacidade. Essa etapa roda na GPU e é muito barata.
Cada propriedade CSS, quando animada, dispara a partir de uma dessas etapas — e tudo que vem depois:
A conclusão prática cabe numa frase: sempre que possível, anime apenas transform e opacity. Um painel que desliza com left obriga o navegador a refazer o layout da página a cada quadro; o mesmo painel com translateX só muda a posição de uma camada já pintada.
CSS
/* Errado: recalcula o layout a cada quadro — trava em celular */.menu-lateral{left:-300px;transition:left300ms;}.menu-lateral.aberto{left:0;}/* Certo: só composição — 60 fps mesmo em aparelhos modestos */.menu-lateral{transform:translateX(-300px);transition:transform300ms;}.menu-lateral.aberto{transform:translateX(0);}
O resultado visual é idêntico. O custo, não.
🧠 Você sabia?
A maioria das telas atualiza 60 vezes por segundo. Isso dá ao navegador 16,7 ms para produzir cada quadro — calcular layout, pintar e compor. Se um quadro demora mais que isso, ele é pulado, e o olho percebe o "engasgo": é o que os desenvolvedores chamam de jank. Animar left numa página grande pode custar 30 ms por quadro só de layout; animar transform custa uma fração de milissegundo, porque a GPU só desloca uma textura já pronta. Telas de 120 Hz (comuns em celulares atuais) dão apenas 8,3 ms por quadro — a margem só diminui.
will-change avisa o navegador com antecedência: "este elemento vai animar transform, prepare uma camada dedicada para ele". Isso evita o pequeno atraso da criação da camada no início da animação. Mas cada camada consome memória da GPU. Aplique apenas em elementos que realmente vão animar e que mostraram problema na medição; nunca em * ou em dezenas de elementos. Deixar will-change espalhado piora o desempenho — exatamente o oposto do pretendido.
🔬 Investigue
Abra o site do evento, pressione F12 e vá ao painel Performance. No ícone de engrenagem, marque CPU: 4× slowdown (simula um celular barato). Clique em gravar, passe o mouse sobre alguns cartões e abra o menu hambúrguer, pare a gravação. Na linha do tempo, procure as barras roxas (Layout) e verdes (Paint) e o gráfico de FPS no topo. Agora troque temporariamente uma transição de transform por left (ou margin-left) e repita. Compare: quantos quadros caíram abaixo de 60 fps? Quanto tempo total foi gasto em Layout? Guarde as duas capturas — o desafio ⭐⭐ pede exatamente esse relatório. Bônus: na aba Rendering (menu ⋮ → More tools), ative Paint flashing: tudo que é repintado pisca em verde. Uma animação boa quase não pisca.
Movimento excessivo causa náusea, tontura e desorientação em pessoas com distúrbios vestibulares — e o problema não é raro. Todo sistema operacional moderno tem uma opção "reduzir movimento" (no Android, em Acessibilidade; no iOS, em Acessibilidade → Movimento; no Windows, em Configurações de exibição). O CSS consegue ler essa preferência:
css/estilo.css (última seção do arquivo, depois de todas as media queries)
CSS
/* Respeita a preferência do sistema por menos movimento. Precisa ficar no FIM da folha para vencer qualquer regra anterior. */@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:0.01ms!important;animation-iteration-count:1!important;transition-duration:0.01ms!important;scroll-behavior:auto!important;}}
Este bloco é uma das poucas situações em que !important é justificado: ele precisa vencer qualquer regra do projeto, inclusive as mais específicas, e ninguém deve conseguir sobrescrevê-lo por acidente. Cole-o no fim de toda folha de estilo desta trilha.
Dois detalhes que costumam gerar dúvida:
Por que 0.01ms e não 0? Na Unidade 3 você vai escrever JavaScript que espera o evento transitionend ou animationend para fazer algo (fechar um painel, por exemplo). Com duração zero, alguns navegadores não disparam o evento e o código trava. Com 0,01 ms, a animação "acontece" instantaneamente e o evento dispara.
Por que a Aula 08 já tinha uma versão curta disso? Lá o bloco só existia para você conhecer a media query. Esta versão cobre pseudo-elementos, repetições infinitas e rolagem suave — é a definitiva.
Reduzir movimento não significa remover feedback. A melhor prática é trocar o movimento por um fade: onde o cartão deslizava 24 px para cima, ele apenas aparece. O desafio ⭐ desta aula trabalha isso.
Outras regras, todas com base na WCAG 2.1:
Nada que pisque mais de 3 vezes por segundo (critério 2.3.1 — risco de convulsão fotossensível).
Animações infinitas que chamam atenção devem poder ser pausadas (critério 2.2.2 — pausar, parar, ocultar). Carrosséis automáticos precisam de botão de pausa visível.
Nunca use animação como única forma de comunicar informação: o campo inválido treme e fica com borda vermelha e mostra uma mensagem de texto.
Todo efeito de :hover precisa de um equivalente em :focus-visible — quem navega por teclado também merece feedback.
🔬 Investigue
No DevTools, aba Rendering, procure Emulate CSS media feature prefers-reduced-motion e escolha reduce. Recarregue o site do evento. Tudo que ainda se mexe é uma regra que escapou do bloco acima — geralmente uma animação declarada em um arquivo carregado depois de estilo.css, ou uma transition inline. Faça o mesmo com prefers-color-scheme: dark para conferir o tema escuro da Aula 08.
Nem todo efeito envolve movimento. Sombras, gradientes e filtros dão profundidade e hierarquia à página — e, combinados com transições, produzem os efeitos de hover que você vê nos sites profissionais.
/* Elevação sutil — cartões em repouso */.cartao{box-shadow:01px3pxrgba(0,0,0,0.12);}/* Elevação média — cartão em hover, menu suspenso */.cartao:hover{box-shadow:04px12pxrgba(0,0,0,0.15);}/* Sombra interna — campo de formulário "afundado" */.campo{box-shadow:inset02px4pxrgba(0,0,0,0.1);}/* Anel de foco — alternativa ao outline que respeita o border-radius */.botao:focus-visible{box-shadow:0003pxrgba(26,127,181,0.4);}/* Múltiplas sombras — uma curta e densa, outra longa e difusa */.modal{box-shadow:01px2pxrgba(0,0,0,0.1),08px24pxrgba(0,0,0,0.12);}/* Sombra que acompanha o formato (útil em PNG com transparência e SVG) */.logo{filter:drop-shadow(04px6pxrgba(0,0,0,0.2));}
Os quatro valores de box-shadow são: deslocamento horizontal, deslocamento vertical, desfoque e cor. Sombras realistas têm deslocamento vertical maior que o horizontal (a luz vem de cima) e cor preta com pouca opacidade, nunca cinza sólido. box-shadow desenha um retângulo (ou o border-radius da caixa); filter: drop-shadow() segue o contorno real dos pixels — é o que você quer numa logo com fundo transparente.
/* Linear: direção por palavra-chave ou ângulo */.faixa{background:linear-gradient(toright,#0b3d5c,#1a7fb5);}.faixa-2{background:linear-gradient(135deg,#0b3d5c0%,#1a7fb560%,#7ec8e3100%);}/* Radial: a partir de um ponto */.bolha{background:radial-gradient(circleat30%30%,#1a7fb5,#0b3d5c);}/* Cônico: gira em torno do centro — gráficos de pizza sem JavaScript */.pizza{background:conic-gradient(#0b3d5c0%25%,#1a7fb525%100%);}/* Texto com gradiente */.titulo-gradiente{background:linear-gradient(90deg,#0b3d5c,#1a7fb5);-webkit-background-clip:text;background-clip:text;color:transparent;}/* Sobreposição escura em imagem, para garantir contraste do texto */.hero{background:linear-gradient(rgba(0,0,0,0.55),rgba(0,0,0,0.55)),url("../img/banner.jpg")center/coverno-repeat;color:#ffffff;}
O último exemplo é o mais importante para o projeto: texto branco sobre uma foto nunca tem contraste garantido — em alguma parte da imagem haverá um trecho claro. Um gradiente de duas cores iguais funciona como um "vidro escuro" sobre a foto e garante o contraste AA em toda a área. Repare no caminho ../img/banner.jpg: dentro de css/estilo.css, os caminhos são relativos ao arquivo CSS, não à página HTML.
.a{filter:blur(4px);}.b{filter:brightness(1.2);}.c{filter:contrast(1.1);}.d{filter:grayscale(100%);}.e{filter:saturate(1.4);}.f{filter:sepia(60%);}.g{filter:hue-rotate(90deg);}.h{filter:invert(100%);}.i{filter:grayscale(100%)brightness(1.1);}/* combináveis, aplicados em ordem *//* Desfoque do que está ATRÁS do elemento — o "vidro fosco" */.modal-fundo{background:rgba(255,255,255,0.6);backdrop-filter:blur(8px);}
filter altera o próprio elemento; backdrop-filter altera o que está atrás dele — é o efeito de vidro fosco dos menus e modais modernos. Como a foto de um palestrante em preto e branco que ganha cor no hover: filter: grayscale(100%) no estado normal, grayscale(0) no :hover, com transition: filter 300ms.
/* 1. Sublinhado que cresce da esquerda e recolhe pela direita */.link{position:relative;text-decoration:none;}.link::after{content:"";position:absolute;left:0;bottom:-2px;width:100%;height:2px;background:currentColor;transform:scaleX(0);transform-origin:right;transition:transform250msease;}.link:hover::after,.link:focus-visible::after{transform:scaleX(1);transform-origin:left;}/* 2. Cartão que se eleva */.cartao{transition:transform200msease,box-shadow200msease;}.cartao:hover,.cartao:focus-within{transform:translateY(-4px);box-shadow:012px24pxrgba(0,0,0,0.15);}/* 3. Zoom da imagem dentro da moldura */.moldura{overflow:hidden;border-radius:var(--raio-borda);}.molduraimg{display:block;transition:transform400msease;}.moldura:hoverimg,.moldura:focus-withinimg{transform:scale(1.06);}
O sublinhado (efeito 1) é o mesmo que você usou "de bônus" no menu da Aula 07 — agora com um refinamento: a troca de transform-origin faz a linha crescer da esquerda ao entrar e recolher pela direita ao sair, como se deslizasse por baixo do texto. No zoom (efeito 3), o overflow: hidden da moldura corta o excesso da imagem ampliada; sem ele, a imagem invadiria os vizinhos.
⚠️ Atenção
Sempre replique o efeito de :hover também em :focus-visible (ou :focus-within, quando o elemento interativo está dentro do cartão). Caso contrário, quem navega por teclado não recebe nenhum retorno visual — e não tem como saber onde está.
O efeito de seções que "surgem" conforme você rola é a combinação de uma transição CSS com um pedacinho de JavaScript que observa quando o elemento entra na tela. O JavaScript é a Unidade 3; aqui você só precisa saber que ele adiciona uma classe — toda a animação continua no CSS.
css/estilo.css (seção 6 — utilitários)
CSS
/* Só esconde quando o JavaScript está ativo; sem JS, o conteúdo aparece normalmente */.js.revelar{opacity:0;transform:translateY(24px);transition:opacity500msease,transform500msease;}.js.revelar.visivel{opacity:1;transform:translateY(0);}
js/efeitos.js
JavaScript
// Marca que o JavaScript está ativo: o CSS só esconde os .revelar com essa classe.document.documentElement.classList.add("js");// Observa cada elemento .revelar e adiciona "visivel" quando 15% dele entra na tela.constobservador=newIntersectionObserver((entradas)=>{entradas.forEach((entrada)=>{if(entrada.isIntersecting){entrada.target.classList.add("visivel");observador.unobserve(entrada.target);// já revelou: para de observar}});},{threshold:0.15});document.querySelectorAll(".revelar").forEach((elemento)=>observador.observe(elemento));
index.html (antes de </body>)
HTML
<scriptsrc="js/efeitos.js"defer></script>
💡 Dica
Repare na classe .js no <html>. Se o CSS escondesse .revelar incondicionalmente e o JavaScript falhasse (bloqueado, com erro, ou ainda carregando numa conexão lenta), o conteúdo ficaria invisível para sempre. Com o .js adicionado pelo próprio script, sem JavaScript a página é só uma página normal. Conteúdo nunca deve depender de animação para existir — esse é o padrão correto, sem exceção.
Você vai aplicar os dez itens da prática guiada ao site da Semana Acadêmica de Sistemas de Informação. Todos os trechos vão em css/estilo.css, respeitando a ordem da folha da Aula 06 (1 reset · 2 variáveis · 3 base · 4 layout · 5 componentes · 6 utilitários · 7 media queries), mais um arquivo novo, js/efeitos.js. Abra o site com o Live Server e mantenha o DevTools aberto: cada passo tem um resultado visível.
Passo 1 — variáveis de movimento e transições nos elementos interativos¶
Assim como as cores e os espaçamentos, as durações e curvas do projeto viram variáveis. Se um dia o evento quiser um site "mais calmo", você muda três linhas.
css/estilo.css (seção 2 — variáveis; acrescente ao :root existente)
CSS
:root{--duracao-curta:150ms;--duracao-media:250ms;--duracao-longa:400ms;--curva-padrao:ease-out;--cor-esqueleto:#e4e9ee;/* fundo do cartão-esqueleto */--cor-esqueleto-brilho:#f2f5f8;/* faixa clara que percorre o esqueleto */}
E, no bloco @media (prefers-color-scheme: dark) que você escreveu na Aula 08, os dois valores escuros correspondentes — sem eles, o esqueleto vira uma mancha branca no tema escuro:
As duas cores do esqueleto entram como variáveis pelo mesmo motivo de todas as outras: o Boss desta aula exige "nenhuma cor solta na folha", e um valor fixo aqui quebraria o tema escuro exatamente no componente que mais aparece durante o carregamento.
css/estilo.css (seção 5 — componentes; substitua as regras de .botao, links e campos da Aula 06)
CSS
/* Botões: cor, elevação e "afundar" ao pressionar */.botao{display:inline-block;padding:var(--espaco-pequeno)var(--espaco-grande);background-color:var(--cor-primaria);color:#ffffff;border:0;border-radius:999px;font-weight:600;text-decoration:none;cursor:pointer;transition:background-colorvar(--duracao-curta)var(--curva-padrao),transformvar(--duracao-curta)var(--curva-padrao),box-shadowvar(--duracao-curta)var(--curva-padrao);}.botao:hover,.botao:focus-visible{background-color:var(--cor-secundaria);transform:translateY(-2px);box-shadow:04px12pxrgba(0,0,0,0.15);}.botao:active{transform:translateY(0);box-shadow:none;}.botao:focus-visible{outline:3pxsolidvar(--cor-secundaria);outline-offset:3px;}/* Links de conteúdo */maina{color:var(--cor-secundaria);transition:colorvar(--duracao-curta)var(--curva-padrao);}maina:hover,maina:focus-visible{color:var(--cor-primaria);}/* Campos do formulário de inscrição (Aula 03) */.campoinput,.camposelect,.campotextarea{border:1pxsolidvar(--cor-borda);border-radius:var(--raio-borda);padding:var(--espaco-pequeno)var(--espaco-medio);transition:border-colorvar(--duracao-curta)var(--curva-padrao),box-shadowvar(--duracao-curta)var(--curva-padrao);}.campoinput:focus-visible,.camposelect:focus-visible,.campotextarea:focus-visible{outline:none;border-color:var(--cor-secundaria);box-shadow:0003pxrgba(26,127,181,0.35);}
Todas as durações ficam entre 150 ms e 250 ms — micro-interações. Repare que o :activenão tem transição própria: ele herda a do estado base, e o botão "afunda" em 150 ms.
Passo 2 — menu com sublinhado animado e item ativo¶
Substitua o ::after do menu da Aula 07 por esta versão, que cresce da esquerda e recolhe pela direita:
css/estilo.css (seção 4 — layout, regras do .menu)
O item ativo (aria-current="page", que cada página marca no seu próprio link desde a Aula 07) fica com o sublinhado permanente e não anima — ele está lá, não precisa chamar atenção.
Os cartões de programacao.html e palestrantes.html (grade .cartoes da Aula 07) ganham elevação no hover e quando o link interno recebe foco.
css/estilo.css (seção 5 — componentes)
CSS
.cartao{position:relative;/* já existia: ancora o selo absolute */background:var(--cor-superficie);border-radius:var(--raio-borda);box-shadow:var(--sombra-cartao);overflow:hidden;/* corta o zoom da imagem */transition:transformvar(--duracao-media)var(--curva-padrao),box-shadowvar(--duracao-media)var(--curva-padrao);}.cartao:hover,.cartao:focus-within{transform:translateY(-4px);box-shadow:012px24pxrgba(0,0,0,0.15);}.cartao__imagem{display:block;width:100%;aspect-ratio:16/9;object-fit:cover;transition:transformvar(--duracao-longa)var(--curva-padrao);}.cartao:hover.cartao__imagem,.cartao:focus-within.cartao__imagem{transform:scale(1.05);}.cartaoa:focus-visible{outline:3pxsolidvar(--cor-secundaria);outline-offset:2px;}
A página inicial ganha uma seção de destaque com a foto do auditório ao fundo. Sem o gradiente, o título branco some nas partes claras da foto.
index.html (logo depois do <header>, dentro de <main id="a09-conteudo">)
HTML
<sectionclass="hero"aria-labelledby="titulo-hero"><divclass="container hero__conteudo"><pclass="hero__chamada">Semana Acadêmica de Sistemas de Informação</p><h1id="titulo-hero">Três dias de palestras, minicursos e maratona de programação</h1><p>Auditório Central · vagas limitadas</p><ahref="inscricao.html"class="botao">Garanta sua vaga</a></div></section>
Usei a própria cor primária do evento (#0b3d5c) com 78 % de opacidade no lugar do preto: o resultado tem a identidade do site e ainda assim garante o contraste. Confira no WebAIM: branco sobre #0b3d5c passa com folga no nível AAA.
Passo 5 — entrada escalonada dos cartões da programação¶
programacao.html (a lista de cartões existente; acrescente o --i em cada item)
HTML
<ulclass="cartoes"><liclass="cartao"style="--i: 0"><imgsrc="img/palestra-ia.jpg"alt=""class="cartao__imagem"><spanclass="cartao__selo">Palestra</span><h3>Inteligência artificial no dia a dia do desenvolvedor</h3><p>Como usar assistentes de código sem perder o controle do que você entrega.</p><ahref="#ia">Ver detalhes</a></li><liclass="cartao"style="--i: 1"><imgsrc="img/minicurso-git.jpg"alt=""class="cartao__imagem"><spanclass="cartao__selo">Minicurso</span><h3>Git e GitHub do zero</h3><p>Versionamento, branches e o primeiro pull request em três horas.</p><ahref="#git">Ver detalhes</a></li><liclass="cartao"style="--i: 2"><imgsrc="img/maratona.jpg"alt=""class="cartao__imagem"><spanclass="cartao__selo">Competição</span><h3>Maratona de programação</h3><p>Equipes de três, seis problemas, quatro horas. Inscrições por equipe.</p><ahref="#maratona">Ver detalhes</a></li></ul>
⚠️ Atenção
Enquanto a animação surgir está rodando, o transform dela vence o transform do :hover do Passo 3 — é a regra de cascata das animações. Como ela dura 400 ms e acontece só no carregamento, ninguém percebe. Mas se você fizer uma animação infinite com transform num cartão, o hover de elevação para de funcionar. Uma propriedade, um dono.
Passo 6 — spinner no botão e esqueleto de palestrante a confirmar¶
Ainda não há JavaScript para "carregar" nada de verdade, mas o site já pode ter os dois componentes prontos. O spinner vira um estado do botão de inscrição (a Unidade 3 vai ativá-lo ao enviar o formulário); o esqueleto representa os palestrantes ainda não confirmados em palestrantes.html.
css/estilo.css (seção 5 — componentes; as animações girar e brilho da §5 precisam estar no arquivo)
CSS
/* Botão em estado de carregamento: o texto some e um spinner ocupa o centro */.botao--carregando{position:relative;color:transparent;pointer-events:none;}.botao--carregando::after{content:"";position:absolute;inset:0;margin:auto;width:20px;height:20px;border:3pxsolidrgba(255,255,255,0.4);border-top-color:#ffffff;border-radius:50%;animation:girar700mslinearinfinite;}/* Cartão-esqueleto: mesmo tamanho de um cartão real, sem conteúdo */.cartao--esqueleto{padding:var(--espaco-medio);display:grid;gap:var(--espaco-pequeno);}.esqueleto--foto{aspect-ratio:16/9;}.esqueleto--linha{height:1rem;}.esqueleto--curta{width:60%;}/* Texto só para leitores de tela (se ainda não tiver este utilitário, adicione na seção 6) */.visualmente-oculto{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;}
palestrantes.html (último item da grade .cartoes)
HTML
<liclass="cartao cartao--esqueleto"style="--i: 3"><spanclass="visualmente-oculto">Palestrante a confirmar</span><divclass="esqueleto esqueleto--foto"aria-hidden="true"></div><divclass="esqueleto esqueleto--linha"aria-hidden="true"></div><divclass="esqueleto esqueleto--linha esqueleto--curta"aria-hidden="true"></div></li>
inscricao.html (para testar o spinner, adicione a classe temporariamente ao botão de envio)
O aria-hidden="true" nas caixas cinzas e o texto visualmente oculto fazem o leitor de tela anunciar "Palestrante a confirmar" em vez de silêncio ou de três caixas vazias. Depois de testar o spinner, remova a classe botao--carregando do botão.
Na Aula 08 o menu no celular alternava entre display: none e display: flex. Esse é o caso clássico de "não anima". Você vai trocar por opacity + visibility + transform, mantendo o js/menu.jsexatamente como está — ele só inverte o aria-expanded, e é o CSS que reage.
css/estilo.css (seção 4 — layout; remova as regras .menu { display: none; } e .menu-botao[aria-expanded="true"] + .menu { display: flex; } da Aula 08 e coloque estas no lugar)
CSS
/* Celular: o menu fica ancorado logo abaixo do cabeçalho, fora de vista */.menu{position:absolute;/* em relação ao .cabecalho, que é sticky (Aula 07) */top:100%;left:0;right:0;display:flex;flex-direction:column;gap:0;padding:var(--espaco-medio);background:var(--cor-superficie);border-bottom:1pxsolidvar(--cor-borda);box-shadow:08px24pxrgba(0,0,0,0.12);list-style:none;opacity:0;visibility:hidden;transform:translateY(-8px);transition:opacityvar(--duracao-media)var(--curva-padrao),transformvar(--duracao-media)var(--curva-padrao),visibility0svar(--duracao-media);}.menu-botao[aria-expanded="true"]+.menu{opacity:1;visibility:visible;transform:translateY(0);transition:opacityvar(--duracao-media)var(--curva-padrao),transformvar(--duracao-media)var(--curva-padrao),visibility0s;}/* O ícone do botão gira 90° quando o menu está aberto */.menu-botao__icone{display:inline-block;transition:transformvar(--duracao-media)var(--curva-padrao);}.menu-botao[aria-expanded="true"].menu-botao__icone{transform:rotate(90deg);}
css/estilo.css (seção 7 — media queries; dentro do @media (min-width: 768px) já existente, substitua a regra do .menu)
Este é o "painel lateral" do item 7 da prática guiada: entra com transform, sai suavemente e o visibility garante que, fechado, os links não sejam alcançáveis por Tab.
index.html (as duas seções abaixo do hero recebem a classe revelar)
HTML
<sectionclass="container revelar"id="sobre"aria-labelledby="titulo-sobre"><h2id="titulo-sobre">Sobre o evento</h2><p>A Semana Acadêmica de Sistemas de Informação reúne estudantes, professores e profissionais do mercado em três dias de palestras, minicursos e competições no campus de Sinop.</p></section><sectionclass="container revelar"id="numeros"aria-labelledby="titulo-numeros"><h2id="titulo-numeros">A edição em números</h2><ulclass="numeros"><li><strong>12</strong> palestras</li><li><strong>6</strong> minicursos</li><li><strong>300</strong> vagas</li></ul></section>
Crie o arquivo js/efeitos.js com o código da §8 (o IntersectionObserver que adiciona a classe visivel) e adicione as regras .js .revelar ao css/estilo.css (seção 6 — utilitários). Inclua o script no fim de index.html:
Passo 9 — bloco prefers-reduced-motion no fim da folha¶
Cole o bloco completo da §7 como última regra de css/estilo.css, depois de todas as media queries. Se ele ficar antes de alguma transition declarada mais abaixo, o !important ainda vence — mas a convenção "fica no fim" evita que alguém procure por ele no meio do arquivo.
Abra index.html no Chrome, F12 → painel Performance.
Na engrenagem, marque CPU: 4× slowdown.
Clique em Record, role a página do topo ao rodapé passando o mouse sobre os cartões, abra e feche o menu hambúrguer (redimensione para menos de 768 px antes), pare a gravação.
Observe o gráfico de FPS no topo: ele deve ficar verde e estável. Barras vermelhas indicam quadros perdidos.
Na faixa Main, procure blocos roxos de Layout. Durante as animações não deve haver nenhum — se houver, alguma transição está usando uma propriedade de layout.
Botões e links: passe o mouse e navegue por Tab. Cada elemento interativo reage em até 250 ms, e o retorno do teclado é igual ao do mouse.
Menu: em qualquer página, o sublinhado cresce da esquerda ao passar o mouse e recolhe pela direita ao sair; o item da página atual fica sublinhado o tempo todo.
Cartões: recarregue programacao.html — os cartões surgem um após o outro, com 80 ms de intervalo. Depois disso, o hover eleva o cartão e amplia a imagem sem vazar da moldura.
Hero: o título é legível em qualquer parte da foto; reduza a janela — o título e o espaçamento acompanham (clamp).
Esqueleto e spinner: o último cartão de palestrantes.html "brilha"; o botão com botao--carregando mostra um círculo girando no lugar do texto.
Menu hambúrguer: abaixo de 768 px, o menu desliza de cima em 250 ms ao clicar no botão e desaparece suavemente ao fechar. Com o menu fechado, Tab não entra nos links dele.
Revelação: recarregue index.html e role — as seções "Sobre o evento" e "A edição em números" surgem subindo. Desative o JavaScript (DevTools → Ctrl+Shift+P → "Disable JavaScript") e recarregue: as seções aparecem normalmente, sem animação.
Movimento reduzido: em Rendering, emule prefers-reduced-motion: reduce. Tudo passa a acontecer instantaneamente, mas nada some nem deixa de funcionar.
Performance: a gravação com CPU 4× mostra FPS estável e nenhum bloco de Layout durante as animações.
Resultado esperado: o site do evento continua idêntico em estrutura, mas cada interação agora tem resposta visual suave, os cartões chegam em cascata, o hero é legível, o menu do celular desliza e ninguém que prefira menos movimento é prejudicado. Faça o commit com a mensagem Aula 09: transições, animações e efeitos.
A1. Qual a diferença entre transition e animation? Quando usar cada uma?
A2. Escreva a transição de background-color e transform em 250 ms com curva ease-out, na forma abreviada.
A3. Por que transition: all é desaconselhado? Dê dois motivos.
A4. Explique cada uma das quatro propriedades da transição (transition-property, transition-duration, transition-timing-function, transition-delay).
A5. Diferencie ease-in, ease-out e ease-in-out, indicando quando usar cada uma.
A6. Por que display: none não pode ser transicionado? Qual a alternativa e como ela evita que o elemento escondido receba foco?
A7. Qual a diferença de resultado entre transform: translateX(50px) rotate(45deg) e transform: rotate(45deg) translateX(50px)? Desenhe.
A8. O que faz transform-origin? Dê um exemplo em que alterá-lo é necessário.
A9. Escreva um @keyframes que leve a opacidade de 0 a 1 e desloque o elemento 20 px para cima.
A10. O que faz animation-fill-mode: forwards? O que acontece sem ele?
A11. Qual a diferença entre animation-direction: alternate e reverse?
A12. Por que transform e opacity são as propriedades mais performáticas para animar? Cite as etapas do pipeline que cada grupo de propriedades dispara.
A13. Reescreva de forma performática: .painel { top: -200px; transition: top 300ms; } e o estado aberto .painel.aberto { top: 0; }.
A14. Para que serve will-change? Qual o risco de abusar dela?
A15. Escreva o bloco prefers-reduced-motion completo e explique por que ele usa !important e por que usa 0.01ms em vez de 0.
A16. Qual o limite de piscadas por segundo previsto na WCAG (critério 2.3.1) e por quê?
A17. Escreva um gradiente linear diagonal (135°) de #0b3d5c para #1a7fb5.
A18. Qual a diferença entre box-shadow e filter: drop-shadow()? Em que caso só o segundo funciona?
A19. Por que todo efeito de :hover deve ser replicado em :focus-visible? Qual a diferença entre :focus e :focus-visible?
A20. O que faz backdrop-filter e em que componente ele é tipicamente usado?
B1. Construa um botão completo com estados animados: normal, :hover (elevação), :active (pressionado), :disabled (opaco, sem transição) e :focus-visible (anel). Nenhum estado pode depender só de cor.
Resultado esperado: um botão que sobe no hover, afunda no clique, mostra um anel no foco por teclado e, desabilitado, fica opaco com cursor not-allowed — e cada estado é distinguível mesmo em uma captura de tela em preto e branco.
Dica
Declare a transition no estado base. Para o :disabled, use transition: none e opacity: 0.5 — um botão desabilitado não deve "reagir" ao mouse. O anel de foco pode ser outline com outline-offset ou box-shadow: 0 0 0 3px.
B2. Faça um cartão giratório 3D para um palestrante: a frente mostra a foto e o nome; o verso mostra a mini-biografia e o link da palestra. O giro acontece no hover e também no foco por teclado.
Resultado esperado: o cartão vira em 600 ms com profundidade real; o verso não aparece espelhado; ao pressionar Tab até o link do verso, o cartão vira sozinho.
Dica
perspective no pai, transform-style: preserve-3d no elemento que gira, backface-visibility: hidden nas duas faces, e o verso começa com rotateY(180deg). Use :focus-within no pai para o giro por teclado. O exemplo completo está na §3.
B3. Crie um spinner e um esqueleto de carregamento para uma listagem de 6 cartões. Simule o carregamento com um setTimeout de 2 segundos (o trecho de JavaScript de 4 linhas está na Dica) e faça a transição do esqueleto para o conteúdo real.
Resultado esperado: ao abrir a página, seis cartões cinza "brilham" por 2 segundos e então desvanecem, dando lugar aos cartões reais, que surgem com surgir.
Dica
Tenha os dois conjuntos de cartões no HTML (esqueleto e real), com o real escondido por opacity: 0; visibility: hidden. O JavaScript apenas troca uma classe no contêiner após 2 s: setTimeout(() => lista.classList.add("carregado"), 2000). O CSS faz o resto com .carregado .esqueleto e .carregado .cartao.
B4. Implemente um menu lateral (drawer) que entra da esquerda animando apenas transform, com sobreposição escura em opacity, fechamento pelo Esc, pelo clique fora e por botão, e com o foco preso dentro dele enquanto estiver aberto (o esqueleto de JavaScript para abrir/fechar e prender o foco costuma ser fornecido pronto nesta altura do curso; se você ainda não estudou JavaScript, adie essa parte para depois da Aula 13 e volte para terminá-la — o CSS já pode ser feito agora).
Resultado esperado: o painel desliza em 300 ms; o fundo escurece junto; nenhuma barra de Layout aparece no painel Performance durante a abertura; com o painel fechado, seus links não recebem foco.
Dica
Painel: position: fixed; inset: 0 auto 0 0; width: min(320px, 85vw); transform: translateX(-100%); aberto: translateX(0). Sobreposição: position: fixed; inset: 0; background: rgba(0,0,0,0.5); opacity: 0; visibility: hidden com o truque do visibility 0s 300ms. Use a escala de z-index da Aula 07 (900 para a sobreposição, 1000 para o painel).
B5. Construa um acordeão animado com três itens: a altura anima suavemente, o ícone gira 180°, apenas um item fica aberto por vez, e o conteúdo fechado não é alcançável por teclado.
Resultado esperado: clicar em um título abre o conteúdo com deslizamento suave e fecha o que estava aberto; o "chevron" gira; Tab pula os conteúdos fechados.
Dica
Altura auto não transiciona. Duas saídas: usar grid-template-rows: 0fr → 1fr no contêiner (com overflow: hidden no filho), que anima em navegadores modernos, ou max-height com um valor maior que o conteúdo. Para fechar os outros, use <details name="acordeao"> com o mesmo name em todos, ou um pouco de JavaScript. Para o teclado, visibility: hidden no conteúdo fechado.
B6. Faça uma galeria de 6 fotos com efeito de zoom e legenda: a imagem amplia dentro da moldura, a legenda desliza de baixo para cima e um filtro de saturação é aplicado. Tudo reversível e suave.
Resultado esperado: ao passar o mouse (ou focar por teclado), a foto cresce 6 % sem vazar da moldura, ganha cor mais viva e a legenda sobe do rodapé da moldura; ao sair, tudo volta na mesma velocidade.
Dica
Moldura com position: relative; overflow: hidden. Legenda position: absolute; bottom: 0; transform: translateY(100%), e translateY(0) no hover. Na imagem, filter: saturate(1) → saturate(1.4) e transform: scale(1.06). Use :focus-within na moldura se a foto for um link.
B7. Implemente uma barra de progresso animada que vai de 0 % ao valor real ao entrar na viewport, com o número contando junto, usando IntersectionObserver (adapte o js/efeitos.js).
Resultado esperado: ao rolar até a seção "vagas preenchidas", a barra cresce de 0 a 72 % em 1,2 s e o texto passa de "0 %" a "72 %" ao mesmo tempo.
Dica
A barra é um transform: scaleX(0) com transform-origin: left que vai a scaleX(var(--progresso)) quando ganha a classe visivel — é transform, portanto barato. O número contando é a única parte que precisa de JavaScript (um setInterval curto ou requestAnimationFrame) — se você ainda não chegou na Aula 13, um trecho pronto costuma ser fornecido para focar no CSS; senão, é um bom exercício escrevê-lo você mesmo.
B8. Crie um conjunto de notificações (toasts) que entram deslizando pela direita, empilham-se, somem automaticamente após 4 segundos com animação de saída e podem ser fechadas manualmente.
Resultado esperado: um botão "Testar notificação" adiciona um toast no canto inferior direito; ele entra da direita, fica 4 s, e sai para a direita desvanecendo; três cliques rápidos empilham três toasts sem sobreposição.
Dica
Contêiner position: fixed; bottom: 1rem; right: 1rem; display: flex; flex-direction: column; gap: 0.5rem. Entrada com @keyframes entrarDireita (espelho de entrarEsquerda) e animation-fill-mode: both. A saída é outra classe (.saindo) com uma animação que leva a translateX(120%) e opacity: 0; remova o elemento no animationend.
B9. Anime a validação do formulário de inscrição: campo inválido treme e ganha borda vermelha; campo válido exibe um ícone de confirmação que surge com escala; a mensagem de erro desliza para baixo.
Resultado esperado: ao sair de um campo inválido (:invalid após interação, ou classe .invalido), ele treme 400 ms e fica vermelho com uma mensagem abaixo; um campo válido mostra um "✓" que cresce de scale(0) a scale(1) com ricochete.
Dica
Use a animação tremer da §5. Para não tremer antes de o usuário digitar, prefira :user-invalid (navegadores recentes) ou uma classe adicionada por JavaScript. O ícone pode ser um ::after com content: "✓" e transform: scale(0) → scale(1) com cubic-bezier(.34, 1.56, .64, 1). A mensagem de erro nasce com opacity: 0; transform: translateY(-4px).
B10. Construa um carrossel de depoimentos com transição entre slides, indicadores clicáveis, avanço automático a cada 5 segundos, pausa no hover e no foco, e botão explícito de pausar.
Resultado esperado: três depoimentos alternam a cada 5 s com fade ou deslizamento; os três pontinhos abaixo indicam e escolhem o slide; passar o mouse ou focar qualquer coisa do carrossel interrompe o avanço; o botão "Pausar" vira "Continuar" e realmente para o relógio.
Dica
Faça a versão só com CSS primeiro: uma faixa display: flex com transform: translateX(calc(var(--slide) * -100%)) e transition: transform 500ms. O avanço automático e o botão de pausa precisam de JavaScript (setInterval / clearInterval) — se você ainda não chegou na Aula 13, um trecho pronto costuma ser fornecido para focar no CSS; senão, vale a pena escrevê-lo você mesmo. O botão de pausa é obrigatório pela WCAG 2.2.2 — sem ele, o exercício não está pronto.
C1.Página de apresentação animada. Construa uma landing page de um produto (ou do próprio evento) com: hero com gradiente animado ao fundo e texto que entra escalonado; menu que muda de aparência ao rolar (fica compacto e ganha sombra); seções que se revelam por IntersectionObserver; contadores animados; galeria com zoom; depoimentos em carrossel; formulário com validação animada; rodapé com links de sublinhado animado. Requisitos: todas as animações usam apenas transform e opacity onde for possível, prefers-reduced-motion respeitado, e a página mantém 60 fps na gravação do painel Performance com CPU 4×. Comece pelo hero e o menu; o restante pode ser terminado depois e reaproveita os exercícios B6, B7, B9 e B10.
Dica
O gradiente animado ao fundo é um background-size: 200% 200% com @keyframes deslocando background-position — barato o suficiente para um único elemento grande. O "menu que muda ao rolar" é uma classe .compacto adicionada pelo JavaScript quando window.scrollY > 80, e o CSS transiciona padding e box-shadow — aqui padding é aceitável porque muda uma vez, não a cada quadro. Faça a auditoria de performance no fim de cada seção, não só no fim da página.
Alguém enviou o CSS abaixo dizendo "nada anima, o CSS deve estar bugado". Não está — o navegador está fazendo exatamente o que foi pedido. Há quatro erros conceituais, cada um de um tipo diferente visto nesta aula. Encontre e corrija todos sem reescrever do zero: a intenção de quem escreveu deve ser preservada.
Ative prefers-reduced-motion: reduce no sistema (ou emule no DevTools) e abra qualquer site grande: a maioria simplesmente remove tudo, e junto some o feedback — o painel que deslizava agora aparece do nada, o toast que entrava pela direita simplesmente "está lá". Reduzir movimento não é remover comunicação. Pegue as animações abaixo, que são chamativas demais para quem tem sensibilidade vestibular, e reescreva o CSS para que, com movimento reduzido, cada uma seja substituída por um fade curto — sem depender do bloco !important genérico do fim da folha.
Esta aula afirma que animar transform é mais barato que animar left. Você vai provar isso com números, do jeito que um engenheiro de front-end faz antes de defender a escolha para outra pessoa. Implemente o mesmo painel deslizante de duas formas: uma animando left (ou width), outra animando transform. Meça as duas com o painel Performance do DevTools, capture o número de quadros por segundo e o tempo gasto em Layout, e escreva um relatório de uma página comparando os resultados, com as capturas de tela como evidência. Repita o teste com a CPU limitada em 4× — é aí que a diferença aparece de verdade.
Critérios de pronto
Duas páginas (painel-left.html e painel-transform.html) com o mesmo painel, o mesmo conteúdo (coloque pelo menos 200 elementos na página para o layout ter custo) e o mesmo botão de abrir/fechar.
Quatro gravações do painel Performance: cada versão com CPU normal e com CPU 4×.
O relatório (relatorio.md ou PDF de uma página) traz uma tabela com FPS mínimo e tempo total de Layout para as quatro gravações, as capturas, e uma conclusão de três linhas.
O relatório explica, com suas palavras, por que a diferença existe (pipeline da §6).
Bônus: repita no celular real usando a depuração remota do Chrome (chrome://inspect).
Pistas
No painel Performance, o resumo (aba Summary) mostra o tempo total de Rendering; a aba Bottom-Up mostra quanto foi Layout.
O gráfico de FPS fica no topo da gravação; passe o mouse sobre as barras vermelhas para ver a duração de cada quadro perdido.
Se a diferença não aparecer com CPU normal, não conclua que "é igual": aumente o número de elementos e ligue o 4×.
Grave só o trecho da animação (clique em Record, abra o painel, feche, pare) — gravações longas escondem o pico.
Bibliotecas como a Animate.css têm milhões de downloads por semana — e cabem em um arquivo CSS que você já sabe ler. Crie o seu animacoes.css com no mínimo 20 animações reutilizáveis, organizadas em quatro categorias (entrada, saída, atenção e carregamento), controladas por variáveis CSS (--duracao, --atraso, --curva) e aplicáveis por classe (.anim-surgir, .anim-tremer). Documente cada uma em uma página de demonstração com o código ao lado do exemplo funcionando e um controle deslizante (<input type="range">) que ajusta a duração ao vivo.
Critérios de pronto
animacoes.css com ≥ 20 animações, cada uma em um @keyframes com nome em português descrevendo o movimento, e uma classe .anim-* que a aplica lendo var(--duracao, 400ms), var(--atraso, 0ms) e var(--curva, ease-out).
Pelo menos 5 animações por categoria, e as de atenção têm animation-iteration-count finito por padrão.
Todas usam apenas transform, opacity ou background-position — nenhuma anima propriedade de layout.
O arquivo termina com o bloco prefers-reduced-motion que troca todas as entradas/saídas por fade (não por remoção).
demo.html mostra cada animação com um botão "Reproduzir", o código-fonte ao lado (em <pre><code>) e um <input type="range"> que altera --duracao no :root ao vivo.
Um README.md explica como usar a biblioteca em qualquer projeto em 5 linhas.
Para "reproduzir" uma animação de novo por clique, remova a classe, force um reflow (void elemento.offsetWidth) e adicione a classe de volta — esse é o truque clássico.
Variáveis CSS entram em animation-duration: var(--duracao, 400ms) normalmente; o valor de reserva garante que a classe funcione sem configuração.
Para a saída, animation-fill-mode: forwards é obrigatório — senão o elemento "volta" ao terminar.
Olhe a lista de nomes da Animate.css para não esquecer categorias inteiras (flip, zoom, slide, fade, bounce), mas escreva cada @keyframes você mesmo.
Para ir além: publique a biblioteca no GitHub Pages (Aula 15) e use-a no seu projeto autoral no lugar das animações soltas.
Quatro aulas atrás o seu projeto autoral era HTML puro. Hoje ele tem um sistema de design, um layout de verdade, responde a qualquer tela e se move com propósito. O Boss desta unidade é fechar tudo isso em um site que você mostraria numa entrevista — e que é a base direta do Marco 2. Não há nada novo aqui: há tudo da Unidade 2, junto, funcionando ao mesmo tempo, no seu tema.
Critérios de pronto
Aula 06 — sistema de design:css/estilo.css único, organizado nas 7 seções, com :root de pelo menos 12 variáveis (cores, espaçamentos, fonte, raio, sombra, durações, curva); nenhuma cor ou duração "solta" no meio do arquivo; contraste AA em todos os textos, incluindo o hero.
Aula 07 — layout: estrutura da página em Grid com áreas nomeadas, componentes internos em Flexbox, cabeçalho sticky com <nav aria-label> em lista, aria-current="page" em cada página, link de salto funcionando, rodapé sempre no fim da janela; zero float e zero position: absolute para layout.
Aula 08 — responsividade: mobile first com três breakpoints escolhidos pelo conteúdo (documentados em comentário), grade de cartões sem media query (auto-fit/minmax), imagens fluidas com object-fit, tipografia com clamp(), menu hambúrguer com <button aria-expanded aria-controls>, tema escuro via prefers-color-scheme.
Aula 09 — movimento: transições em todos os elementos interativos (150–250 ms) com :focus-visible equivalente ao :hover; menu hambúrguer que desliza (transform + opacity + visibility); cartões com entrada escalonada e elevação; hero com gradiente; pelo menos duas seções com revelação ao rolar que aparecem normalmente sem JavaScript; bloco prefers-reduced-motion no fim da folha; nenhuma animação de propriedade de layout.
Qualidade: Lighthouse (modo mobile) com Acessibilidade ≥ 90 e Boas práticas ≥ 90 em todas as páginas; gravação do painel Performance com CPU 4× sem quadros perdidos durante a rolagem e a abertura do menu; validador W3C sem erros (Aula 02).
Entrega: capturas de cada página em 360 px, 768 px e 1440 px, a captura do Lighthouse e a do painel Performance, tudo numa pasta evidencias/ no repositório.
Pistas
Use os checkpoints das Aulas 06, 07, 08 e 09 como lista de verificação, na ordem — a maioria dos itens você já fez; o Boss é o que falta e a integração.
Rode o Lighthouse antes de mexer em qualquer coisa e anote a nota: os itens de acessibilidade que ele aponta (contraste, nomes de botão, alt, ordem de títulos) são os mais rápidos de resolver.
A gravação de Performance sem quadros perdidos é o critério que mais reprova: procure transition: all, box-shadow animado em muitos elementos ao mesmo tempo e will-change espalhado.
Deixe o tema escuro por último e teste-o com o menu aberto, no hero e nos cartões — é onde as cores "fixas" que escaparam das variáveis aparecem.
Para ir além: peça a alguém que use o seu site só pelo teclado por dois minutos e anote onde essa pessoa se perdeu. Cada ponto anotado é um item do checklist de qualidade do Marco 2 que você acabou de garantir.
Parte 1 — Leitura (15 min). SILVA, Maurício Samy. Criando sites com HTML, capítulo sobre efeitos e transformações. MDN: Using CSS transitions e Using CSS animations (links em "Para aprofundar"). Anote uma coisa que a MDN explica e esta aula não.
Parte 2 — Entrega (40 min). Aplique ao seu projeto autoral a Mão na massa completa, com os dez requisitos:
Variáveis de movimento e transições em todos os elementos interativos (150–250 ms).
Menu com sublinhado animado e item ativo.
Cartões com elevação no hover, replicada em :focus-within.
Hero com gradiente sobre a imagem garantindo contraste AA.
Entrada escalonada dos cartões com --i.
Spinner e esqueleto de carregamento.
Menu hambúrguer (ou painel lateral) que abre com transform e sai suavemente.
Revelação ao rolar em duas seções, com js/efeitos.js.
Bloco prefers-reduced-motion no fim da folha.
Captura de tela do painel Performance (CPU 4×) mostrando a rolagem sem quedas de quadro, salva em evidencias/performance.png.
Critério de pronto: todos os dez itens presentes; emular prefers-reduced-motion: reduce no DevTools não esconde nem quebra nada; desativar o JavaScript não esconde nenhuma seção; a captura do painel Performance está no repositório.
Parte 3 — Discussão (5 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: traga um site com movimento bem empregado e outro com movimento excessivo, explicando tecnicamente a diferença (o que cada animação comunica, duração, propriedade animada).
Guarde no seu repositório: commit + push (ou a pasta do projeto).
Ao fim desta aula — e da Unidade 2 — o repositório do seu projeto autoral precisa ter:
[ ] css/estilo.css único, organizado nas 7 seções, com :root incluindo as variáveis de movimento (--duracao-*, --curva-padrao).
[ ] Todos os botões, links, campos e cartões com transição entre 150 ms e 250 ms, e cada :hover com seu :focus-visible.
[ ] Menu com sublinhado animado (transform: scaleX + transform-origin) e item ativo via aria-current.
[ ] Hero com gradiente sobre imagem, título legível em qualquer ponto da foto, tamanhos com clamp().
[ ] Cartões com entrada escalonada (--i + animation-delay + backwards) e elevação no hover.
[ ] Spinner (@keyframes girar) e esqueleto (@keyframes brilho) prontos no CSS, com texto para leitor de tela.
[ ] Menu hambúrguer da Aula 08 animado com opacity + visibility + transform, sem display: none, e js/menu.js inalterado.
[ ] js/efeitos.js com IntersectionObserver e pelo menos duas seções .revelar que aparecem mesmo sem JavaScript.
[ ] Bloco prefers-reduced-motion completo como última regra do CSS.
[ ] Nenhuma animação de width, height, top, left ou margin; nenhum transition: all; nenhum will-change sem justificativa.
[ ] evidencias/performance.png com a gravação sem quedas de quadro.
[ ] Tudo o que os checkpoints das Aulas 06, 07 e 08 pediam continua funcionando (design system, layout Grid/Flexbox, responsividade em três larguras, tema escuro).
web.dev — Learn CSS: https://web.dev/learn/css — os módulos Transitions, Animations e Shadows têm demonstrações interativas de cada curva.
cubic-bezier.com: https://cubic-bezier.com — desenhe curvas e compare duas ao vivo antes de colar no projeto.
W3C — WCAG 2.1, critérios 2.2.2 (Pausar, parar, ocultar) e 2.3.1 (Três flashes ou abaixo do limite): https://www.w3.org/WAI/WCAG21/quickref/ — filtre por "2.2.2" e "2.3.1".
Especificações do CSS Working Group: CSS Transitions e CSS Animations — para quando a MDN não responder um detalhe de comportamento.
SILVA, Maurício Samy. Criando sites com HTML: sites de alta qualidade com HTML e CSS. Novatec, 2008 — capítulo de efeitos e transformações.
TERUEL, Evandro C. HTML 5 — Guia Prático. Saraiva, 2014 — capítulo sobre CSS3 e efeitos visuais.
Isso encerra a Unidade 2. Em quatro aulas o seu site saiu de HTML puro para um sistema de design em variáveis, um layout de verdade com Grid e Flexbox, responsividade em qualquer tela e movimento que comunica sem atrapalhar. O Marco 2 é exatamente esse site — o Boss desta aula é o roteiro para fechá-lo, e ele fecha na próxima aula. Na próxima aula você abre a Unidade 3 com a Introdução ao JavaScript: um js/app.js novo entra na pasta js/ ao lado do menu.js (Aula 08) e do efeitos.js (hoje), e você começa a ler linha a linha o código que até agora colou "sem entender" — a leitura completa dos dois arquivos se fecha na Aula 13, com funções e eventos. O site deixa de ser só apresentação e começa a reagir ao usuário.
[ ] Site do evento com as cinco páginas (início, programação, inscrição, palestrantes, contato) estilizadas, responsivas e com animações, abrindo no Live Server.
[ ] VS Code com a extensão Live Server; Chrome ou Firefox com o DevTools acessível pelo F12.
[ ] Marco 2 pronto para fechar — as instruções completas estão no fim desta aula.
Na aula passada você fechou a Unidade 2 com transições, @keyframes e prefers-reduced-motion: o site do evento está bonito e responsivo, mas continua parado — cada número de vagas foi digitado à mão no HTML e nada reage ao visitante. Hoje começa a Unidade 3: o JavaScript entra em cena, o site ganha o seu primeiro script e o Console do navegador vira a sua principal ferramenta de trabalho.
Uma página web é feita de três linguagens que dividem responsabilidades. Você já domina duas delas; hoje entra a terceira.
Tecnologia
Responsabilidade
Pergunta que responde
No site do evento
HTML
estrutura e conteúdo
o que está na página?
títulos, tabela da programação, formulário de inscrição
CSS
apresentação
como a página aparece?
cores, grid do layout, menu responsivo, animações
JavaScript
comportamento
o que acontece quando o usuário interage?
calcular vagas, validar o formulário, filtrar a programação
A analogia clássica é a de um corpo: o HTML é o esqueleto, o CSS é a pele e a roupa, e o JavaScript são os músculos e o sistema nervoso — sem ele, nada se move nem reage.
JavaScript é uma linguagem interpretada, de tipagem dinâmica, multiparadigma e padronizada. Cada adjetivo diz algo prático:
Interpretada — você não compila nada antes de rodar. O navegador lê o arquivo .js e executa na hora. (Por baixo do capô, os motores modernos compilam trechos "quentes" para código de máquina em tempo real, a chamada compilação JIT; para você, o efeito é que salvar o arquivo e recarregar a página já basta.)
Tipagem dinâmica — uma variável não tem tipo fixo; o valor tem tipo. A mesma variável pode guardar um número agora e um texto depois. Isso dá agilidade e também é a origem de boa parte dos bugs que você vai caçar nesta unidade.
Multiparadigma — dá para programar de forma imperativa (sequência de comandos), funcional (funções que recebem e devolvem funções, Aula 13) e orientada a objetos (classes, que você verá no Nível 2).
Padronizada — a especificação da linguagem chama-se ECMAScript e é mantida pelo comitê TC39 da Ecma International. "JavaScript" é o nome popular; "ECMAScript" é o nome do padrão. Na prática, são sinônimos.
É a única linguagem executada nativamente por todos os navegadores: Chrome, Firefox, Safari e Edge entendem JavaScript sem instalar nada.
A confusão é antiga e o nome foi a causa. Em 1995, a Netscape lançou a linguagem como "LiveScript" e, semanas depois, renomeou para "JavaScript" em um acordo de marketing com a Sun Microsystems, dona do Java — que estava na moda. As linguagens são diferentes em sintaxe, tipagem, forma de execução e propósito. Dizer que JavaScript é parecido com Java é como dizer que "hamster" é parecido com "ham" — compartilham letras, não natureza.
let, const, arrow functions, classes, template literals, módulos, Promises
ES2016 em diante
anual
pequenas adições por ano: **, includes, async/await, ?., ??, at()
Desde 2015 sai uma versão por ano, com o nome do ano. Nesta trilha você escreve JavaScript moderno (ES2015+): const e let em vez de var, template literals em vez de concatenação, e os métodos de array que você vai conhecer na Aula 12.
🧠 Você sabia?
A primeira versão do JavaScript foi escrita por Brendan Eich em dez dias, em maio de 1995, sob pressão para que o Netscape Navigator 2 saísse com uma linguagem de script. O nome interno era "Mocha". Várias decisões tomadas às pressas naquelas semanas — como o comportamento estranho de == e o resultado de typeof null — continuam na linguagem até hoje, porque corrigi-las quebraria milhões de sites.
O código JavaScript é executado por um motor (engine). Cada navegador tem o seu: o Chrome e o Edge usam o V8, o Firefox usa o SpiderMonkey, o Safari usa o JavaScriptCore. Em 2009, o V8 foi retirado do navegador e embrulhado em um programa de linha de comando chamado Node.js — desde então, JavaScript também roda em servidores, e é isso que você vai usar no Nível 2 para construir APIs. Nesta trilha, porém, o único lugar onde o seu JavaScript roda é o navegador.
Existem três maneiras de colocar um script em uma página HTML. Só uma delas é a forma correta para o dia a dia.
Arquivo:exemplo-inclusao.html
HTML
<!DOCTYPE html><htmllang="pt-BR"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Três formas de incluir JavaScript</title><!-- 3. Externo, com defer — a forma correta --><scriptsrc="js/script.js"defer></script></head><body><!-- 1. Inline, dentro de um atributo — evite --><buttononclick="alert('Oi')">Clique</button><!-- 2. Interno, dentro da própria página — só para testes rápidos --><script>console.log("Executando o script interno");</script></body></html>
Inline (onclick="…"): mistura comportamento com estrutura, não pode ser reaproveitado entre páginas e dificulta a leitura. É o equivalente ao style="…" que você aprendeu a evitar na Aula 06.
Interno (<script> com código dentro): útil para um experimento de dois minutos, mas o código fica preso a uma única página.
Externo (<script src="…">): um arquivo .js separado, incluído por todas as páginas que precisam dele, com cache do navegador e leitura limpa. É o que você vai usar no site do evento.
O navegador lê o HTML de cima para baixo e vai construindo a árvore de elementos (o DOM, que você conhecerá a fundo na Aula 13). Quando encontra um <script> sem atributos, ele para de ler o HTML, baixa o script, executa e só então continua. Isso tem duas consequências: a página demora mais para aparecer e, pior, o script roda antes de o restante do HTML existir — qualquer tentativa de acessar um elemento que ainda não foi lido falha.
Forma
Quando baixa
Quando executa
Use quando
<script> no <head> sem atributo
interrompe a leitura do HTML
imediatamente — o DOM ainda não existe
nunca, para scripts que tocam a página
<script> antes de </body>
quando o HTML já foi lido
imediatamente — o DOM já existe
solução clássica; funciona, mas atrasa o download
defer
em paralelo com a leitura do HTML
depois que todo o HTML foi lido, na ordem em que foram declarados
sempre — é a melhor opção
async
em paralelo com a leitura do HTML
assim que terminar de baixar, fora de ordem
só para scripts independentes, como analytics
A regra desta trilha: <script src="…" defer> no <head>. Você ganha o download em paralelo, a garantia de que o HTML inteiro já foi lido e a ordem previsível entre vários scripts — o que vai importar na Aula 12, quando um arquivo de dados precisar ser carregado antes do arquivo que o usa.
🔎 Por baixo do capô
O defer só faz sentido com src. Em um script interno (sem src), o atributo é ignorado e o código roda na hora. Se você precisa de um script interno que espere o HTML, coloque-o antes de </body> — ou, melhor, mova o código para um arquivo externo.
<!DOCTYPE html><htmllang="pt-BR"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Primeiro script</title><scriptsrc="js/primeiro.js"defer></script></head><body><h1>Semana Acadêmica de Sistemas de Informação</h1><p>Vagas restantes: <strongid="vagas">?</strong></p></body></html>
Arquivo:js/primeiro.js
JavaScript
// Tudo depois de // até o fim da linha é comentário: o navegador ignora.console.log("O script carregou!");constvagasTotais=120;constinscritos=87;constvagasRestantes=vagasTotais-inscritos;console.log("Vagas restantes:",vagasRestantes);// Localiza o elemento com id="vagas" e troca o texto dele.// A manipulação da página será aprofundada na Aula 13; por ora,// leia esta linha como "escreva o valor dentro do <strong>".document.querySelector("#vagas").textContent=vagasRestantes;
Abra primeiro.html no Live Server, pressione F12 e olhe a aba Console: a mensagem "O script carregou!" e o número 33 devem aparecer, e o ? da página deve ter virado 33.
🔬 Investigue
Remova o defer da tag <script> em primeiro.html, salve e recarregue. A página mostra ? e o Console exibe Uncaught TypeError: Cannot read properties of null (reading 'textContent') (no Firefox: document.querySelector(...) is null). Leia a mensagem com calma: querySelector devolveu null porque o <strong> ainda não existia quando o script rodou. Agora mova a tag <script> (ainda sem defer) para antes de </body> e recarregue: funciona de novo. Você acabou de ver, na prática, por que a posição importa. Devolva o defer ao <head> antes de continuar.
F12 abre o painel; Ctrl+Shift+J (no macOS, Cmd+Option+J) abre direto na aba Console. Você já usou as abas Elements e Styles na Unidade 2; agora as abas que interessam são Console e Sources.
console.log é a sua principal ferramenta de depuração enquanto aprende. Mas o objeto console tem mais do que log:
Arquivo:js/console-demo.js
JavaScript
console.log("Mensagem comum");console.info("Informação — igual ao log, com ícone em alguns navegadores");console.warn("Aviso — fundo amarelo");console.error("Erro — fundo vermelho e pilha de chamadas");// Vários valores separados por vírgula: o console mostra cada um com o tipo certoconstnome="Ana";constnota=8.5;console.log("Aluna:",nome,"Nota:",nota);// typeof mostra o tipo do valorconsole.log("Valor:",nota,typeofnota);// console.table monta uma tabela a partir de um array de objetosconsole.table([{nome:"Ana",nota:8},{nome:"Bruno",nota:7},]);// console.group agrupa mensagens com recuo (útil para separar etapas)console.group("Cálculo de vagas");console.log("Total: 120");console.log("Inscritos: 87");console.groupEnd();// console.time mede quanto tempo passou entre time e timeEndconsole.time("cálculo");constresultado=120-87;console.timeEnd("cálculo");// cálculo: 0.01ms (o número varia)// console.assert só imprime se a condição for falsaconsole.assert(resultado===33,"O cálculo de vagas está errado!");// console.count conta quantas vezes foi chamado com aquele rótuloconsole.count("carregamento");// console.clear limpa o console
💡 Dica
Use vírgula, não +, para mostrar vários valores: console.log("Nota:", nota) preserva o tipo e a cor de cada valor; console.log("Nota: " + nota) transforma tudo em texto. Isso importa quando o valor é um objeto — com + você vê apenas [object Object].
O Console é um REPL (leia, avalie, imprima, repita): digite uma expressão, pressione Enter e veja o resultado na hora. É o melhor lugar para testar uma dúvida em cinco segundos.
Para escrever mais de uma linha, use Shift+Enter. Para reaproveitar o último resultado, use $_. Para pegar o elemento selecionado na aba Elements, use $0. As setas para cima e para baixo percorrem o histórico.
console.log funciona, mas exige que você adivinhe onde colocar a mensagem, salve, recarregue e leia. A aba Sources oferece algo melhor: o breakpoint, um ponto de parada. Abra js/primeiro.js na árvore de arquivos, clique no número de uma linha e recarregue a página. O navegador pausa exatamente ali e mostra, no painel Scope, o valor de cada variável naquele instante. Os botões no topo permitem avançar linha a linha (F10), entrar em uma função (F11) ou continuar a execução (F8).
Você também pode pausar por código: a instrução debugger; no meio do script tem o mesmo efeito de um breakpoint quando o DevTools está aberto.
JavaScript
constvagasTotais=120;constinscritos=87;debugger;// a execução pausa aqui, com o DevTools abertoconstvagasRestantes=vagasTotais-inscritos;
Nas primeiras semanas, console.log basta. Quando os scripts crescerem (Aula 13 em diante), os breakpoints vão economizar horas.
Os três abrem caixas de diálogo do próprio navegador: alert("texto") mostra uma mensagem; prompt("pergunta") pede um texto e devolve o que o usuário digitou (ou null se cancelar); confirm("pergunta") devolve true ou false. Você vai vê-los em tutoriais antigos, e eles servem para um experimento rápido. Em aplicações reais são inadequados: bloqueiam a página inteira até serem fechados, não podem ser estilizados, ignoram o CSS do site, atrapalham leitores de tela e alguns navegadores os suprimem depois do segundo uso. Na Aula 14 você vai construir mensagens de validação de verdade, dentro da própria página.
Uma variável é um nome que aponta para um valor guardado na memória. Em JavaScript moderno há duas palavras-chave para declarar variáveis — e uma terceira, herdada, que você precisa reconhecer para nunca usar.
JavaScript
letcontador=0;// pode receber outro valor depoisconstPI=3.14159;// NÃO pode receber outro valorvarantigo="evite";// legado: escopo de função e comportamento confuso
Regra prática: declare tudo com const. Só troque por let quando o valor precisar mudar ao longo do programa (um contador, um acumulador, um estado que alterna). Nunca use var em código novo — a Aula 11 mostra em detalhe o que ele faz de estranho.
constlista=[1,2,3];lista.push(4);// permitido: o conteúdo mudou, a referência continua a mesmaconsole.log(lista);// [1, 2, 3, 4]lista=[5,6];// Uncaught TypeError: Assignment to constant variable.
const impede que o nome seja religado a outro valor. Se o valor é um array ou um objeto, o seu interior continua editável. Isso confunde no começo, mas é coerente: const protege a ligação entre nome e valor, não o valor em si.
Podem conter letras, dígitos, _ e $; não podem começar com dígito. Acentos são permitidos, mas evite-os — o teclado do colega pode não ter.
São case-sensitive: nome, Nome e NOME são três variáveis diferentes.
Convenção: camelCase para variáveis e funções (totalInscritos), PascalCase para classes (EventoCard), MAIUSCULO_COM_UNDERSCORE para constantes globais que nunca mudam (VAGAS_TOTAIS).
Nomes devem descrever o conteúdo: totalAlunosAprovados, não x ou tmp2. O código é lido muito mais vezes do que escrito.
Palavras reservadas não podem ser nomes: let, class, for, if, return, new e cerca de quarenta outras. O Console avisa com Uncaught SyntaxError: Unexpected token se você tentar.
JavaScript
// Bons nomesconstnomeDoEvento="Semana Acadêmica de Sistemas de Informação";constVAGAS_TOTAIS=120;letinscritosConfirmados=87;// Nomes ruins (funcionam, mas ninguém entende)constn="Semana Acadêmica de Sistemas de Informação";constv=120;letx=87;
Um valor primitivo é um valor simples, sem partes internas, copiado por valor.
JavaScript
consttexto="Olá";// string — texto entre aspasconstnumero=42;// number — inteiroconstdecimal=3.14;// number — decimal (não existe tipo separado)constverdadeiro=true;// boolean — true ou falseconstnada=null;// null — ausência intencional de valorletindefinido;// undefined — declarada, mas sem valorconstgrande=9007199254740993n;// bigint — inteiros gigantes, com n no fimconstsimbolo=Symbol("id");// symbol — identificador único (raro no Nível 1)
Tudo que não é primitivo é objeto: arrays, funções, datas, o próprio document. Você vai trabalhar com arrays e objetos na Aula 12.
O operador typeof devolve, como string, o tipo de um valor:
JavaScript
typeof"texto";// "string"typeof42;// "number"typeof3.14;// "number"typeoftrue;// "boolean"typeofundefined;// "undefined"typeof10n;// "bigint"typeofSymbol("id");// "symbol"typeofnull;// "object" ← bug histórico, mantido por compatibilidadetypeof[1,2];// "object" ← arrays são objetostypeof{};// "object"typeoffunction(){};// "function" ← funções são objetos, mas typeof as distinguetypeofNaN;// "number" ← sim, "não é um número" é do tipo number
🧠 Você sabia?typeof null === "object" é um bug da primeira implementação de 1995. Os valores eram guardados com uma etiqueta de tipo nos bits mais baixos; a etiqueta 0 significava "objeto", e null era representado pelo ponteiro nulo — que também é 0. Quando o TC39 discutiu corrigir isso (uma proposta chegou a sugerir typeof null === "null"), a resposta foi não: sites demais dependem do comportamento antigo. Para testar se um valor é null, compare diretamente: valor === null.
JavaScript não separa int de float: todo número é um number, armazenado como ponto flutuante de 64 bits (padrão IEEE 754). Alguns valores especiais moram nesse tipo:
JavaScript
1/0;// Infinity-1/0;// -Infinity0/0;// NaN — "Not a Number", resultado de contas impossíveisNumber("abc");// NaNNumber.MAX_SAFE_INTEGER;// 9007199254740991 — maior inteiro representado com exatidão9007199254740992+1;// 9007199254740992 — acima do limite, a conta erra em silêncio9007199254740992n+1n;// 9007199254740993n — bigint resolve0.1+0.2;// 0.30000000000000004 — a Aula 11 explica esse resultado
NaN tem uma propriedade única: é o único valor de JavaScript que não é igual a si mesmo. NaN === NaN é false. Para verificar se algo é NaN, use Number.isNaN(valor).
As duas primeiras são equivalentes; escolha uma e seja consistente (esta apostila usa aspas duplas). A terceira, com crase (acento grave), é o template literal — e faz coisas que as outras não fazem.
constnome="Maria";constidade=20;// Concatenação clássica com + : funciona, mas é fácil esquecer um espaço ou uma aspaconstfrase1="Olá, "+nome+"! Você tem "+idade+" anos.";// Template literal: a expressão entre ${ } é avaliada e inserida no textoconstfrase2=`Olá, ${nome}! Você tem ${idade} anos.`;console.log(frase1===frase2);// true — o resultado é idêntico
Dentro de ${ } cabe qualquer expressão, não só uma variável:
Uma string sabe fazer várias coisas consigo mesma. Os métodos abaixo cobrem 90% do uso diário. Repare que nenhum deles altera a string original — strings são imutáveis; cada método devolve uma string nova.
JavaScript
consts=" Desenvolvimento Web ";// dois espaços em cada pontas.length;// 23 — quantidade de caracteres (é propriedade, sem parênteses)s.trim();// "Desenvolvimento Web" — remove espaços das pontass.trimStart();// "Desenvolvimento Web "s.toUpperCase();// " DESENVOLVIMENTO WEB "s.toLowerCase();// " desenvolvimento web "s.includes("Web");// true — contém?s.startsWith(" Des");// true — começa com?s.trim().endsWith("Web");// true — termina com?s.indexOf("Web");// 18 — posição da primeira ocorrência (ou -1)s.slice(2,17);// "Desenvolvimento" — do índice 2 até antes do 17s.replace("Web","Mobile");// " Desenvolvimento Mobile " — só a primeira ocorrências.replaceAll("e","3");// " D3s3nvolvim3nto W3b " — todass.trim().split(" ");// ["Desenvolvimento", "Web"] — quebra em array"7".padStart(3,"0");// "007" — completa à esquerda até 3 caracteres"ab".repeat(3);// "ababab"s.trim().charAt(0);// "D" — caractere na posição 0s.trim()[0];// "D" — mesma coisa, com colchetess.trim().at(-1);// "b" — índice negativo conta do fim
O índice começa em zero: o primeiro caractere é o [0]. Isso vale para strings hoje e para arrays na Aula 12.
constcomAspas="Ela disse \"oi\" e saiu";// \" escapa a aspa dentro da stringconstcomAspas2='Ela disse "oi" e saiu';// ou troque o tipo de aspaconstquebra="Linha 1\nLinha 2";// \n é quebra de linhaconsttab="Nome:\tAna";// \t é tabulaçãoconstbarra="C:\\pasta\\arquivo";// \\ é uma barra literal
🔬 Investigue
No Console, digite "ação".length e depois "😀".length. A primeira dá 4, como esperado. A segunda dá 2, embora seja um único emoji. JavaScript conta strings em unidades de 16 bits (UTF-16), e emojis modernos ocupam duas. Agora teste [..."😀"].length — o resultado é 1. Guarde esse detalhe: ele explica bugs de "limite de caracteres" em formulários com emoji.
Você pede a conversão chamando a função do tipo de destino:
JavaScript
Number("42");// 42Number("42.5");// 42.5Number(" 42 ");// 42 — espaços nas pontas são ignoradosNumber("");// 0 — string vazia vira zero (armadilha!)Number("abc");// NaNNumber("42px");// NaN — qualquer letra sobrando invalida tudoNumber(null);// 0Number(undefined);// NaNNumber(true);// 1parseInt("42px");// 42 — lê dígitos até encontrar algo que não é dígitoparseInt("3.99");// 3 — descarta a parte decimalparseInt("abc");// NaNparseFloat("3.14m");// 3.14String(42);// "42"String(null);// "null"(42).toFixed(2);// "42.00" — atenção: devolve STRING, com 2 casas decimais(3.14159).toFixed(2);// "3.14"Boolean("");// falseBoolean("0");// true — string com conteúdo é verdadeiraBoolean(0);// falseBoolean([]);// true — array vazio é verdadeiro
Quando o valor vem de um formulário — e todo valor de formulário chega como string —, converter com Number() é o primeiro passo antes de qualquer conta. Isso será o centro da Aula 14.
Existem dois operadores de igualdade. O == (igualdade frouxa) converte os tipos antes de comparar; o === (igualdade estrita) compara valor e tipo, sem converter nada.
JavaScript
5=="5";// true — "5" vira 55==="5";// false — tipos diferentesnull==undefined;// truenull===undefined;// falseNaN===NaN;// false — NaN não é igual a nada, nem a si mesmo0=="";// true — "" vira 00=="0";// true""=="0";// false — o == nem é transitivo![]==false;// truenull==0;// false
Use sempre=== e !==. A coerção do == produz resultados bizarros e elimina uma classe inteira de bugs quando abandonada. Se você precisa comparar um número que chegou como texto, converta primeiro (Number(entrada) === 5), não relaxe a comparação.
Quando JavaScript precisa de um booleano — em um if, em um !, em um Boolean() —, ele converte o valor. Apenas oito valores viram false; todos os outros viram true.
Todo o resto é truthy — inclusive "0", "false", " " (espaço), [] e {}.
JavaScript
Boolean("false");// true — é uma string com conteúdoBoolean("0");// trueBoolean(" ");// true — o espaço é conteúdoBoolean([]);// trueBoolean({});// trueBoolean(0);// falseBoolean("");// falseBoolean(NaN);// false
📌 Vale gravar
A lista dos oito valores falsy e a diferença entre == e === aparecem em toda entrevista técnica de front-end. Decore a lista e saiba explicar por que "0" é truthy (é uma string não vazia) enquanto 0 é falsy.
Todo erro de JavaScript aparece em vermelho no Console, com três partes: o tipo do erro, a mensagem e o local (arquivo e linha). Aprender a ler as três é metade da depuração.
Texto
Uncaught ReferenceError: inscrito is not defined
at app.js:12:13
ReferenceError — você usou um nome que não existe. Quase sempre é um erro de digitação (inscrito em vez de inscritos) ou uma variável usada antes de ser declarada.
TypeError — o valor existe, mas não é do tipo que a operação exige: reatribuir uma const, chamar algo que não é função, ler uma propriedade de null.
SyntaxError — o navegador nem começou a executar; o arquivo tem um erro de escrita. Aspa não fechada, parêntese sobrando, vírgula faltando.
RangeError — um número fora da faixa permitida (por exemplo, (1).toFixed(200)).
O app.js:12:13 diz: arquivo app.js, linha 12, coluna 13. Clique nesse link: o DevTools abre a aba Sources exatamente naquela linha.
JavaScript
// SyntaxError — a aspa nunca fechaconstnome="Ana;// Uncaught SyntaxError: Invalid or unexpected token// ReferenceError — a variável se chama nome, não nomesconsole.log(nomes);// Uncaught ReferenceError: nomes is not defined// TypeError — reatribuição de constconsttotal=10;total=20;// Uncaught TypeError: Assignment to constant variable.// TypeError — chamar algo que não é funçãoconstidade=20;idade();// Uncaught TypeError: idade is not a function
Um SyntaxError impede todo o arquivo de rodar, mesmo as linhas corretas. Se nada acontece e o Console mostra um erro de sintaxe, corrija-o primeiro — o resto do script só será avaliado depois.
💻 Mão na massa — O site do evento ganha um script¶
O site da Semana Acadêmica de Sistemas de Informação tem cinco páginas prontas em HTML e CSS. Hoje ele recebe dois arquivos JavaScript: um compartilhado por todas as páginas e um só para a página de inscrição, que passa a calcular sozinha as vagas restantes.
Passo 1 — criar js/app.js e incluí-lo em todas as páginas¶
A pasta js/ já existe desde a Aula 08, quando você colou ali o menu.js do hambúrguer, e na Aula 09 ela ganhou o efeitos.js do IntersectionObserver. Hoje entra o terceiro arquivo: js/app.js, o script compartilhado por todas as páginas.
Inclua-o no <head> das cinco páginas, sempre com defer, logo após a folha de estilo — e sem apagar os dois <script> que já estavam lá:
Arquivo:index.html (repita o mesmo <head> em programacao.html, inscricao.html, palestrantes.html e contato.html, trocando só o <title>)
HTML
<!DOCTYPE html><htmllang="pt-BR"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metaname="description"content="Semana Acadêmica de Sistemas de Informação: palestras, minicursos e maratona de programação."><metaname="author"content="Semana Acadêmica de Sistemas de Informação"><title>Início — Semana Acadêmica de Sistemas de Informação</title><linkrel="preconnect"href="https://fonts.googleapis.com"><linkrel="preconnect"href="https://fonts.gstatic.com"crossorigin><linkhref="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"rel="stylesheet"><linkrel="stylesheet"href="css/estilo.css"><scriptsrc="js/menu.js"defer></script><scriptsrc="js/efeitos.js"defer></script><scriptsrc="js/app.js"defer></script></head>
O efeitos.js (Aula 09) foi incluído só em index.html; nas outras quatro páginas ficam apenas menu.js e app.js. A ordem entre eles não importa hoje — nenhum depende do outro —, mas a partir da Aula 12 vai importar muito, e é por isso que os três estão declarados com defer no <head>, e não espalhados pelo <body>.
Passo 2 — a mensagem de boas-vindas e os dados do evento¶
Arquivo:js/app.js
JavaScript
// app.js — carregado por todas as páginas do site do evento// Dados gerais do evento. Por enquanto vivem aqui; na Aula 12 viram// arrays e objetos, e no Nível 2 virão de uma API.constNOME_EVENTO="Semana Acadêmica de Sistemas de Informação";constEDICAO=12;constLOCAL_EVENTO="Auditório Central";constTRILHAS="Desenvolvimento Web, Dados, Segurança";console.log(`Bem-vindo à ${EDICAO}ª ${NOME_EVENTO}!`);console.info("Página atual:",document.title);console.group("Dados do evento");console.log("Local:",LOCAL_EVENTO);console.log("Trilhas:",TRILHAS);console.log("Tipo de EDICAO:",typeofEDICAO);console.log("Tipo de NOME_EVENTO:",typeofNOME_EVENTO);console.groupEnd();console.table([{pagina:"Início",arquivo:"index.html"},{pagina:"Programação",arquivo:"programacao.html"},{pagina:"Inscrição",arquivo:"inscricao.html"},{pagina:"Palestrantes",arquivo:"palestrantes.html"},{pagina:"Contato",arquivo:"contato.html"},]);
Salve, abra qualquer página no Live Server e confira o Console: a saudação, o grupo com os dados e a tabela devem aparecer. Navegue para outra página — o script roda de novo, com o document.title correto.
Passo 3 — o cálculo de vagas na página de inscrição¶
A página de inscrição tem, em algum lugar, uma frase como "Restam 33 vagas". Esse número foi digitado à mão. Agora ele será calculado. Localize (ou crie) o parágrafo e dê id aos números:
Arquivo:inscricao.html (trecho dentro do <main>, antes do formulário)
HTML
<sectionclass="vagas"aria-live="polite"><h2>Vagas</h2><p>
Restam <strongid="vagas-restantes">—</strong> de
<spanid="vagas-totais">—</span> vagas
(<spanid="percentual-ocupacao">—</span>% ocupadas).
</p></section>
Os travessões são o conteúdo "enquanto o script não roda" — se o JavaScript falhar, o visitante vê um traço, não um número errado.
Inclua um segundo script só nesta página, depois do app.js:
// inscricao.js — carregado apenas por inscricao.htmlconstVAGAS_TOTAIS=120;letinscritos=87;// let: este número vai mudar quando o formulário funcionar (Aula 14)constvagasRestantes=VAGAS_TOTAIS-inscritos;constpercentualOcupacao=(inscritos/VAGAS_TOTAIS)*100;console.group("Vagas");console.log("Totais:",VAGAS_TOTAIS);console.log("Inscritos:",inscritos);console.log("Restantes:",vagasRestantes);console.log("Ocupação bruta:",percentualOcupacao);// 72.5console.log("Ocupação formatada:",percentualOcupacao.toFixed(1));// "72.5" (string)console.groupEnd();// Sanidade: se alguém digitar mais inscritos do que vagas, o console avisaconsole.assert(vagasRestantes>=0,"Há mais inscritos do que vagas!");// Escreve os valores na página (detalhes na Aula 13)document.querySelector("#vagas-restantes").textContent=vagasRestantes;document.querySelector("#vagas-totais").textContent=VAGAS_TOTAIS;document.querySelector("#percentual-ocupacao").textContent=percentualOcupacao.toFixed(1);console.log(`Resumo: ${inscritos} inscritos, ${vagasRestantes} vagas livres.`);
Os nomes das variáveis são os do sistema de design que você montou na Aula 06 (--espaco-medio, --raio-borda, --cor-primaria). A única nova é --cor-fundo-destaque — declare-a no :root, junto das outras, em vez de escrever a cor solta aqui.
Os exercícios do Nível B pedem funções — a sintaxe completa é assunto da Aula 13. Por ora, copie este esqueleto e preencha o corpo:
JavaScript
functionnomeDaFuncao(parametro){// cálculos aquireturnresultado;// o valor devolvido a quem chamou}console.log(nomeDaFuncao(10));// chamada: o argumento 10 entra em parametro
A1. Qual o resultado e por quê: 5 == "5", 5 === "5", null == undefined, null === undefined, NaN === NaN? Confirme cada resposta no Console antes de escrever a justificativa.
A2. Liste todos os valores falsy do JavaScript. Depois, explique por que "0" e [] não estão na lista.
A3. Reescreva com template literal: "Aluno: " + nome + " - Média: " + media.
A4. Explique a diferença entre defer e async em uma tag <script>. Em qual situação async seria aceitável?
A5. O que retorna typeof null? Por quê? Como testar corretamente se uma variável vale null?
A6. Onde a tag <script> deve ficar e por quê? Cite as duas alternativas que funcionam e diga qual é a preferida.
A7. Qual a diferença entre console.log, console.table e console.error? Dê um exemplo de dado que fica melhor em console.table.
A8. Por que alert e prompt não são adequados em aplicações reais? Cite três motivos.
A9. O que a aba Sources do DevTools permite fazer que o console.log não permite?
A10. Qual a saída de cada linha? Anote sua previsão e só depois teste no Console.
A11. O trecho abaixo tem três erros, um de cada tipo (SyntaxError, ReferenceError, TypeError). Encontre-os sem rodar; depois rode e compare com as mensagens do Console.
B1. Escreva formatarMoeda(valor) que converta 1234.5 em "R$ 1.234,50" usando só toFixed, split, replace e padStart. Depois refaça com Intl.NumberFormat e compare o resultado dos dois com ===.
valor.toFixed(2) dá "1234.50"; split(".") separa inteiro e centavos. Para os pontos de milhar sem laço, comece resolvendo até 999.999 (um ponto só) e observe o que falta. Para a versão com Intl: new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(valor). A comparação com === pode dar false mesmo com textos "iguais" — o Intl usa um espaço especial (não separável, código U+00A0) entre R$ e o número. Inspecione com .charCodeAt(2).
B2. Escreva um script que, ao carregar a página, exiba no Console: a data e a hora formatadas em pt-BR, o tamanho da janela (largura × altura), o navegador em uso e uma tabela com cinco produtos (nome, preço, estoque).
Resultado esperado: quatro blocos no Console; a data no formato "dia/mês/ano", o tamanho como 1366 x 768 (varia por máquina), o texto de navigator.userAgent e uma console.table com cinco linhas.
Dica
new Date().toLocaleDateString("pt-BR") e new Date().toLocaleTimeString("pt-BR") formatam data e hora; window.innerWidth e window.innerHeight dão o tamanho; navigator.userAgent identifica o navegador. Redimensione a janela e recarregue para ver os números mudarem.
B3. Crie um arquivo tipos.js que demonstre, com console.table, o resultado de typeof para dez valores diferentes, e escreva um comentário explicando cada resultado inesperado.
Resultado esperado: uma tabela com colunas valor, tipo e dez linhas, incluindo null, NaN, [], function () {} e 10n; pelo menos três comentários explicando resultados surpreendentes.
Dica
Monte um array de objetos no formato { valor: String(x), tipo: typeof x } — o String(x) evita que o console.table tente expandir arrays e funções. Os resultados inesperados a comentar: typeof null, typeof NaN, typeof [] e typeof function () {}.
B4. Escreva descreverConversao(entrada) que receba uma string e devolva, em uma única frase montada com template literal, o resultado de Number(entrada), parseInt(entrada) e parseFloat(entrada), dizendo qual dos três é NaN.
Resultado esperado: descreverConversao("42px") devolve "Number: NaN | parseInt: 42 | parseFloat: 42 | NaN em: Number"; descreverConversao("3.9kg") devolve "Number: NaN | parseInt: 3 | parseFloat: 3.9 | NaN em: Number"; descreverConversao("abc") indica NaN nos três.
Dica
Number.isNaN(x) diz se x é NaN. Monte a lista de quem deu NaN concatenando strings condicionalmente com o ternário condicao ? "Number " : "" — a Aula 11 formaliza esse operador.
C1. Crie js/diagnostico.js e inclua-o (com defer) em todas as páginas do site do evento. Ao carregar, o script deve imprimir um único console.group chamado "Diagnóstico" com: o caminho da página (location.pathname), o título, o idioma do navegador (navigator.language), se está online (navigator.onLine), a largura da janela classificada como texto ("estreita" abaixo de 600 px, "média" até 1024 px, "larga" acima — use ternários encadeados) e o tempo, em milissegundos, que o próprio script levou para montar tudo isso (console.time/console.timeEnd). Ao fim, remova o arquivo das páginas — ele foi só um exercício.
Dica
Chame console.time("diagnostico") na primeira linha e console.timeEnd("diagnostico") na última. Para a classificação: const largura = window.innerWidth; const faixa = largura < 600 ? "estreita" : largura <= 1024 ? "média" : "larga";.
Você acabou de ver que "5" + 3 é "53" e "5" - 3 é 2. Alguém afirma que "com prática dá para prever qualquer coerção sem testar". Vamos ver. Abaixo estão doze expressões; escreva a sua previsão para cada uma antes de digitar no Console, depois confira e conte os acertos. Cada erro é uma regra que você ainda não internalizou — anote-a.
Um arquivo coercao.js com as doze expressões, cada uma precedida por um comentário com a sua previsão e seguida por um console.log que mostra o resultado real.
Ao lado de cada expressão errada, um comentário de uma linha explicando a regra que você não conhecia.
Uma linha final no arquivo com a contagem: // Acertos: N de 12.
Nenhum == sobreviveu em nenhum dos seus arquivos .js após este exercício — busque no VS Code com Ctrl+Shift+F e troque todos por ===.
Pistas
Procure "Equality comparisons and sameness" na MDN e leia a tabela de ==.
Um array vira string com join(",") antes de qualquer +; um objeto comum vira "[object Object]".
O + unário (o +"a") força conversão para número — e "a" não é um número.
null == false é false porque null só é == a undefined e a ele mesmo; a regra dos falsy não vale para o ==.
⭐⭐
⭐⭐ Caça ao bug: cinco erros em um script de 20 linhas¶
javascriptbugdevtools
O script abaixo deveria mostrar, na página de inscrição, o nome do evento e as vagas restantes. Ele tem cinco defeitos: um impede o arquivo inteiro de rodar, um faz a página quebrar antes de o HTML existir, um produz NaN, um produz um TypeError e um é silencioso — o resultado aparece, mas está errado. Encontre os cinco usando só as mensagens do Console e a aba Sources, e explique cada um.
Arquivo:bug.html
HTML
<!DOCTYPE html><htmllang="pt-BR"><head><metacharset="UTF-8"><title>Caça ao bug</title><scriptsrc="js/bug.js"></script></head><body><h1id="titulo">—</h1><p>Restam <strongid="vagas">—</strong> vagas.</p></body></html>
Um sistema de inscrição cobra R$ 0,10 de taxa por SMS e envia três. No Console, 0.1 * 3 dá 0.30000000000000004; o cliente vê "R$ 0,30000000000000004" no boleto e liga furioso. Bancos, lojas e sistemas de ingressos não guardam dinheiro em ponto flutuante — guardam centavos inteiros. Investigue por que 0.1 + 0.2 !== 0.3, descubra pelo menos três operações do dia a dia que quebram com decimais, e escreva js/dinheiro.js, um script que representa valores em centavos e formata com Intl.NumberFormat.
Critérios de pronto
Um comentário de até dez linhas no topo do arquivo explicando, com suas palavras, por que 0.1 não tem representação exata em binário (cite a quantidade de bits da mantissa no IEEE 754).
Três pares de console.log mostrando uma conta que erra em ponto flutuante e a mesma conta correta em centavos (por exemplo: 0.1 + 0.2, 1.005.toFixed(2), 19.99 * 3).
Uma demonstração de que Math.abs(a - b) < Number.EPSILON compara decimais com segurança quando centavos não são opção.
A formatação final usa Intl.NumberFormat("pt-BR", …) a partir de um inteiro em centavos, e o resultado de formatar(1999 * 3) é "R$ 59,97".
Pistas
Leia a página https://0.30000000000000004.com/ — ela mostra o mesmo problema em dezenas de linguagens; não é um defeito do JavaScript.
No Console, (0.1).toString(2) mostra a representação binária: repare que é uma dízima periódica, como 1/3 em decimal.
(1.005).toFixed(2) dá "1.00", não "1.01", porque 1.005 é, na verdade, 1.00499999999999989… — teste (1.005).toPrecision(20).
Para formatar centavos: divida por 100 só na hora de exibir (centavos / 100) e entregue esse número ao format do Intl.NumberFormat.
Parte 1 — Leitura (20 min). FLANAGAN, D. JavaScript: o guia definitivo, capítulos introdutórios sobre tipos, valores e variáveis. STEFANOV, S. Padrões JavaScript, capítulo de fundamentos (o trecho sobre variáveis globais e var). Na MDN, leia "Tipos e estruturas de dados do JavaScript" (link em Para aprofundar).
Parte 2 — Produção (30 min). Hoje fecha o Marco 2 (instruções completas logo abaixo). Além dele, produza o exercício B4 em um arquivo conversao.js com pelo menos cinco casos de teste no Console. No seu projeto autoral: crie a pasta js, inclua js/app.js com defer em todas as páginas e imprima no Console, com console.table, pelo menos cinco dados do seu domínio (por exemplo, cinco plantas do catálogo, cinco quadras da agenda). Em uma das páginas, calcule e exiba um número derivado de constantes — o equivalente às "vagas restantes" do site do evento.
Critério de pronto: todas as páginas do projeto autoral carregam js/app.js sem erro no Console; uma página exibe um valor calculado por JavaScript no lugar de um número digitado no HTML; o conversao.js roda sem erros e mostra os cinco casos.
Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: traga um resultado surpreendente de comparação ou de operação em JavaScript (diferente dos que apareceram nesta aula), explique tecnicamente por que ele ocorre e como evitá-lo.
Guarde no seu repositório: commit + push (ou a pasta do projeto).
Escopo. Ao fim da Unidade 2, o mesmo site do Marco 1 (com as correções apontadas já aplicadas) precisa estar completamente estilizado: layout, menu, responsividade em três larguras e animações.
Requisitos.
#
Requisito
Onde foi estudado
1
CSS externo, em arquivo único, organizado por seções comentadas
Aula 05
2
Reset ou normalização no topo da folha, com box-sizing: border-box global
Aulas 05 e 06
3
Sistema de design em variáveis CSS: cores, espaçamentos, tipografia e raios
Aula 06
4
Uso demonstrado de seletores de classe, atributo, combinadores, pseudoclasses e pseudoelementos
Aula 06
5
Layout principal com CSS Grid e componentes internos com Flexbox
Aula 07
6
Menu de navegação estilizado, com item ativo destacado e link de salto para o conteúdo
Aula 07
7
Responsividade mobile first, com no mínimo três breakpoints
Aula 08
8
Menu responsivo funcional em telas estreitas
Aula 08
9
Imagens e tipografia fluidas
Aula 08
10
Estados visuais em todos os elementos interativos: :hover, :focus-visible, :active, :disabled
Aulas 06 e 09
11
Transições nos elementos interativos e ao menos uma animação com @keyframes que cumpra uma função
Aula 09
12
Bloco prefers-reduced-motion ao final da folha
Aula 09
13
Contraste WCAG AA em todos os textos, verificado e documentado em contraste.md
Aula 06
14
Formulário estilizado com indicação visual de campo inválido que não dependa só de cor
Aulas 06 e 09
15
Sem float para estrutura e sem !important (exceto dentro do bloco prefers-reduced-motion)
Aulas 07 e 09
Frameworks CSS (Bootstrap, Tailwind e similares) não entram aqui — o objetivo deste marco é demonstrar domínio do CSS puro.
Checklist de qualidade.
Folha de estilo organizada e sistema de design coerente (variáveis, não valores soltos).
Domínio de seletores e especificidade — nenhum !important usado como atalho.
Layout com Grid e Flexbox aplicados onde cada um faz sentido, não ao acaso.
Responsividade real nas três larguras, testada em dispositivo de verdade, não só no simulador.
Estados visuais completos e contraste acessível em todo texto.
Transições e animações com propósito, nunca só para "parecer moderno".
Coerência visual entre as cinco páginas e capricho geral no acabamento.
Como saber que está pronto.
Rode o Lighthouse (modo mobile) em cada página: Acessibilidade e Boas práticas ≥ 90.
No DevTools, alterne prefers-reduced-motion: reduce e prefers-color-scheme: dark e confira que nada quebra.
Redimensione a janela (ou use o modo Responsive) em 360 px, 768 px e 1440 px: nenhuma rolagem horizontal, nenhum texto cortado.
Verifique contraste.md contra o WebAIM: todo par texto × fundo em AA.
Use IA para tirar dúvida ou revisar sintaxe — não para gerar a folha de estilo inteira. Se você não souber explicar por que escolheu Grid em vez de Flexbox num trecho, ainda não é seu.
javascript.info: https://javascript.info/ — capítulos "Hello, world!", "Variables", "Data types" e "Type Conversions"; é o melhor tutorial gratuito da linguagem.
Na próxima aula, o JavaScript começa a decidir: você vai aprofundar escopo e const/let, dominar os operadores aritméticos, de comparação e lógicos (incluindo ?? e ?.) e escrever as primeiras estruturas condicionais — e a página de inscrição do evento passará a mostrar "Últimas vagas!" sozinha.
[ ] Site do evento com as cinco páginas funcionando no Live Server e js/app.js incluído com defer no <head> de todas elas.
[ ] js/inscricao.js criado na Aula 10, calculando vagas restantes e percentual de ocupação e escrevendo os valores em inscricao.html.
[ ] Console do navegador aberto (F12) e sem nenhuma linha vermelha em nenhuma das cinco páginas.
[ ] Revisar da Aula 10: const/let, os sete tipos primitivos, typeof, os oito valores falsy, === versus == e template literals.
Na aula passada o JavaScript entrou no site do evento: você criou js/app.js, aprendeu a declarar variáveis, conheceu os tipos primitivos e fez a página de inscrição calcular as vagas restantes em vez de exibir um número digitado à mão. O script, porém, faz sempre a mesma coisa — ele não decide nada. Hoje você aprende a calcular de verdade (com todos os operadores e com o objeto Math), a combinar condições com &&, ||, ?? e ?. e a escrever as primeiras estruturas de decisão. Ao fim da aula, a página de inscrição avisa "Últimas vagas!" sozinha e a página inicial mostra quantos dias faltam para o evento.
Na Aula 10 você aprendeu a regra prática: declare tudo com const; troque para let só quando o valor precisar mudar; nunca use var. O que ficou faltando é o motivo técnico. Ele tem um nome: escopo — a região do código em que um nome existe.
Existem três escopos em JavaScript:
Escopo
Onde o nome existe
Criado por
Global
o arquivo inteiro (e todos os outros scripts da página)
declaração fora de qualquer bloco ou função
De função
dentro da função onde foi declarado
var, parâmetros, const/let dentro da função
De bloco
dentro do par de chaves { } mais próximo
const e let
Um bloco é qualquer par de chaves: o corpo de um if, de um for, de uma função — ou um par de chaves solto, escrito só para agrupar código.
JavaScript
// js/exemplos-escopo.jsconstnomeEvento="Semana Acadêmica";// escopo global: vale no arquivo todo{constlocal="Auditório Central";// escopo de bloco: só existe aqui dentroconsole.log(nomeEvento);// "Semana Acadêmica" — enxerga o de foraconsole.log(local);// "Auditório Central"}console.log(local);// Uncaught ReferenceError: local is not defined
A regra é assimétrica e vale a pena guardar: de dentro você enxerga o de fora; de fora você não enxerga o de dentro. Um bloco é uma sala com janela: quem está dentro vê a rua, quem está na rua não vê a sala.
var não respeita blocos. Ele só respeita funções. Compare:
JavaScript
// js/exemplos-var.jsif(true){varcomVar="vazou";letcomLet="presa";constcomConst="presa";}console.log(comVar);// "vazou" — var ignorou as chaves do ifconsole.log(comLet);// Uncaught ReferenceError: comLet is not definedconsole.log(comConst);// Uncaught ReferenceError: comConst is not defined
A variável declarada com var dentro do if continua viva fora dele. Em um script de dez linhas isso parece inofensivo; em um arquivo de trezentas, é a receita para dois trechos distantes usarem o mesmo nome sem perceber e sobrescreverem os valores um do outro.
Há um segundo problema, ainda pior. var permite redeclarar o mesmo nome em silêncio:
JavaScript
varinscritos=87;varinscritos=120;// nenhum aviso: o valor anterior simplesmente sumiuconsole.log(inscritos);// 120letvagas=87;letvagas=120;// Uncaught SyntaxError: Identifier 'vagas' has already been declared
Com let, o mesmo erro de digitação vira um erro de sintaxe que o navegador aponta antes de rodar qualquer coisa. É exatamente o que você quer: falhar cedo, com uma mensagem clara.
🧠 Você sabia?
Antes de 2015, var era a única forma de declarar variáveis — e o vazamento de escopo era tão incômodo que a comunidade inventou uma gambiarra famosa, a IIFE (Immediately Invoked Function Expression): embrulhar o código inteiro dentro de uma função anônima executada na hora, escrita como (function () { })(); com o programa todo dentro das chaves, só para criar um escopo. Bibliotecas inteiras (jQuery entre elas) começam assim. Quando let e const chegaram com escopo de bloco, a IIFE perdeu o motivo de existir. Se você abrir um arquivo .js de dez anos atrás e encontrar esse embrulho, agora sabe o que ele está fazendo.
Antes de executar qualquer linha, o motor JavaScript lê o arquivo inteiro e registra todas as declarações. Esse registro antecipado se chama hoisting (içamento). O que muda entre var e let/const é o que acontece com o nome nesse intervalo:
JavaScript
console.log(comVar);// undefined — o nome já existe, o valor ainda nãovarcomVar=10;console.log(comLet);// Uncaught ReferenceError: Cannot access 'comLet' before initializationletcomLet=10;
var é içado e inicializado com undefined — por isso a leitura não falha, ela devolve lixo. let e const são içados, mas ficam em um estado inacessível desde o começo do bloco até a linha da declaração. Esse intervalo tem nome: zona morta temporal (temporal dead zone). Tocar na variável ali dentro é um ReferenceError — e isso é uma boa notícia, porque um erro visível é infinitamente melhor que um undefined silencioso que só vai causar problema três funções adiante.
⚠️ Atenção
As duas mensagens são parecidas e significam coisas diferentes. x is not defined quer dizer "esse nome não existe em lugar nenhum" — geralmente um erro de digitação. Cannot access 'x' before initialization quer dizer "o nome existe, mas você o usou antes da linha que o declara" — mova a declaração para cima.
Declarar, dentro de um bloco, um nome que já existe fora dele é sombreamento (shadowing). É permitido, e às vezes útil, mas confunde quem lê:
JavaScript
consttrilha="Desenvolvimento Web";{consttrilha="Segurança";// sombreia a de fora, só dentro deste blococonsole.log(trilha);// "Segurança"}console.log(trilha);// "Desenvolvimento Web" — intacta
Evite sombrear nomes importantes. Se dois valores são coisas diferentes, dê nomes diferentes: trilhaPadrao e trilhaEscolhida dizem mais do que duas variáveis chamadas trilha.
Uma convenção que vale ouro em projetos pequenos: valores fixos do domínio ficam no topo do arquivo, em MAIUSCULO_COM_UNDERSCORE, e nunca aparecem "soltos" no meio do código.
JavaScript
// js/inscricao.js — topo do arquivoconstVAGAS_TOTAIS=120;constLIMITE_ULTIMAS_VAGAS=20;constTAXA_INSCRICAO=25;constDESCONTO_ESTUDANTE=0.5;
Um número solto no meio de uma conta (if (restantes < 20)) é o que se chama de número mágico: seis meses depois ninguém lembra de onde veio o 20, e quando o valor mudar você vai ter que caçá-lo em cinco lugares. Com a constante, você troca uma linha e o programa inteiro obedece — e o nome documenta a intenção.
// js/exemplos-aritmetica.jsconsole.log(5+3);// 8 — somaconsole.log(5-3);// 2 — subtraçãoconsole.log(5*3);// 15 — multiplicaçãoconsole.log(5/3);// 1.6666666666666667 — divisão (sempre com decimais)console.log(5%3);// 2 — resto da divisão inteiraconsole.log(5**3);// 125 — potência (5 elevado a 3)console.log(-5);// -5 — negação (o único operador unário da lista)
Duas surpresas para quem vem de outra linguagem: a divisão nunca trunca (5 / 2 é 2.5, não 2) e não existe operador de divisão inteira. Para obter a parte inteira use Math.trunc(5 / 2) ou Math.floor(5 / 2), da seção 3.
O operador % devolve o que sobra de uma divisão inteira. Ele resolve quatro problemas que aparecem o tempo todo:
JavaScript
// 1. Par ou ímparconsole.log(10%2);// 0 → parconsole.log(7%2);// 1 → ímpar// 2. Múltiplo de alguma coisa (a cada 5 inscritos, um brinde)constinscritos=85;console.log(inscritos%5);// 0 → é múltiplo de 5// 3. Ciclos: dar a volta em uma lista de 3 coresconsole.log(0%3,1%3,2%3,3%3,4%3);// 0 1 2 0 1// 4. Quebrar um total em unidades (segundos → minutos e segundos)consttotalSegundos=4000;consthoras=Math.trunc(totalSegundos/3600);// 1constminutos=Math.trunc((totalSegundos%3600)/60);// 6constsegundos=totalSegundos%60;// 40console.log(`${horas}h ${minutos}min ${segundos}s`);// "1h 6min 40s"
💡 Dica
Com números negativos, % em JavaScript devolve o sinal do dividendo: -7 % 3 é -1, não 2. Se você precisa de um resto sempre positivo (para ciclos, por exemplo), use ((n % m) + m) % m.
letcontador=5;contador++;// pós-incremento: usa o valor e DEPOIS soma 1++contador;// pré-incremento: soma 1 e DEPOIS usa o valorcontador--;// pós-decremento--contador;// pré-decrementoconsole.log(contador);// 5 — subiu duas vezes e desceu duas
A diferença entre n++ e ++n só aparece quando você usa o resultado da expressão no mesmo lugar:
JavaScript
leta=5;console.log(a++);// 5 — imprime, depois vira 6console.log(a);// 6letb=5;console.log(++b);// 6 — vira 6, depois imprimeconsole.log(b);// 6
Na prática, use ++ sozinho em uma linha (é o caso dos laços da Aula 12) e evite combiná-lo com outra operação — código como x = a++ + ++a é um enigma, não um programa.
Os operadores de atribuição composta encurtam a forma "pegue o valor, faça uma conta, guarde de volta":
Toda expressão com mais de um operador segue uma ordem, do mais "forte" para o mais "fraco". Esta é a parte da tabela que importa no Nível 1:
Nível
Operadores
Sentido de avaliação
1 (mais forte)
( ), ., [ ], chamada de função
da esquerda para a direita
2
!, - unário, ++, --, typeof
da direita para a esquerda
3
**
da direita para a esquerda
4
*, /, %
da esquerda para a direita
5
+, -
da esquerda para a direita
6
<, <=, >, >=
da esquerda para a direita
7
===, !==, ==, !=
da esquerda para a direita
8
&&
da esquerda para a direita
9
\|\|, ??
da esquerda para a direita
10 (mais fraco)
? :, =, +=, -=
da direita para a esquerda
JavaScript
console.log(2+3*4);// 14 — a multiplicação vem antesconsole.log((2+3)*4);// 20 — o parêntese muda tudoconsole.log(2**3**2);// 512 — ** é da direita para a esquerda: 2 ** (3 ** 2)console.log((-2)**2);// 4 — o parêntese é obrigatório aqui (veja abaixo)console.log(10-4-3);// 3 — da esquerda para a direita: (10 - 4) - 3console.log(1+2+"3");// "33" — soma numérica primeiro, depois concatena
Escrever -2 ** 2 sem parênteses é Uncaught SyntaxError: Unary operator used immediately before exponentiation expression. A linguagem se recusa a adivinhar se você quis (-2) ** 2 (que dá 4) ou -(2 ** 2) (que dá −4) — e faz bem.
💡 Dica
Não decore a tabela: use parênteses. Ninguém nunca perdeu tempo lendo uma expressão com parênteses demais; muita gente já perdeu uma tarde caçando um bug em uma expressão com parênteses de menos. Escreva (a + b) / 2, não a + b / 2.
Você viu na Aula 10 que 0.1 + 0.2 dá 0.30000000000000004. A causa é a representação binária: números decimais como 0,1 viram dízimas periódicas em base 2, exatamente como 1/3 vira 0,333… em base 10. Como o padrão IEEE 754 guarda 53 bits de precisão, a dízima é cortada — e o erro aparece na décima sétima casa.
JavaScript
console.log(0.1+0.2);// 0.30000000000000004console.log(0.1+0.2===0.3);// falseconsole.log(1.005*100);// 100.49999999999999console.log((1.005).toFixed(2));// "1.00" — e não "1.01"// Comparando decimais com segurança: a diferença é desprezível?console.log(Math.abs(0.1+0.2-0.3)<Number.EPSILON);// true
Number.EPSILON é a menor diferença representável entre 1 e o próximo número — aproximadamente 2,22 × 10⁻¹⁶. A regra prática: nunca compare decimais com ===; compare a diferença absoluta com uma tolerância. E, para dinheiro, faça o que os bancos fazem: guarde centavos inteiros e divida por 100 só na hora de exibir.
Outro ponto de atenção é o que acontece quando um valor não numérico entra na conta:
JavaScript
console.log("120"-87);// 33 — o - converte a stringconsole.log("120"+87);// "12087" — o + concatenaconsole.log(Number("120")+87);// 207 — a forma corretaconsole.log(undefined+1);// NaNconsole.log(0/0);// NaNconsole.log(Number.isNaN(0/0));// true
Todo valor vindo de um formulário chega como string. Converta com Number() antes de qualquer conta — é o erro número um dos primeiros scripts, e você vai reencontrá-lo na Aula 14.
🔬 Investigue
Abra o Console e digite 0.1 + 0.2 — o resultado com o erro na ponta aparece. Agora digite (0.1).toString(2): você vê a representação binária de 0,1, uma dízima periódica com 0011 se repetindo. Compare com (0.5).toString(2), que dá 0.1 exato — porque 0,5 é uma potência de 2. Por fim, teste 0.1 + 0.2 === 0.3 e Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON. Você acabou de ver, em três linhas, por que sistemas financeiros não usam ponto flutuante.
console.log(Math.round(4.5));// 5 — meio para cimaconsole.log(Math.round(4.4));// 4console.log(Math.round(-4.5));// -4 — atenção: "para cima" é em direção ao +infinitoconsole.log(Math.floor(4.9));// 4 — sempre para baixoconsole.log(Math.ceil(4.1));// 5 — sempre para cimaconsole.log(Math.trunc(4.9));// 4 — corta a parte decimal, sem arredondarconsole.log(Math.trunc(-4.9));// -4 — floor daria -5
Escolha pelo significado: para quantas páginas cabem 47 itens de 10 em 10, Math.ceil (5, porque a quinta página existe mesmo com 7 itens). Para quantos brindes completos dá para montar, Math.floor. Para exibir uma média, toFixed(1) — lembrando que ele devolve string.
console.log(Math.abs(-15));// 15 — valor absolutoconsole.log(Math.max(3,9,1));// 9console.log(Math.min(3,9,1));// 1console.log(Math.pow(2,10));// 1024 — o mesmo que 2 ** 10console.log(Math.sqrt(144));// 12console.log(Math.cbrt(27));// 3console.log(Math.sign(-8));// -1 — -1, 0 ou 1console.log(Math.hypot(3,4));// 5 — raiz de (3² + 4²)console.log(Math.PI);// 3.141592653589793console.log(Math.random());// número entre 0 (incluso) e 1 (excluso)
Math.max e Math.min recebem números soltos, não um array — para o array você vai usar Math.max(...numeros) com o operador spread da Aula 12.
// Um inteiro entre 0 e 9constsorteioSimples=Math.floor(Math.random()*10);// Um inteiro entre minimo e maximo, ambos inclusosconstminimo=1;constmaximo=6;constdado=Math.floor(Math.random()*(maximo-minimo+1))+minimo;console.log("Dado:",dado);
Leia a fórmula de dentro para fora: Math.random() dá algo como 0,7314; multiplicar por (maximo - minimo + 1) espalha o valor pela faixa; Math.floor corta para o inteiro de baixo; somar minimo desloca a faixa para o início certo. O + 1 é o que inclui o maximo no sorteio — sem ele, o 6 nunca sai.
Limitar um valor a uma faixa (clamp) é outro padrão comum, e sai de graça combinando min e max:
console.log(10>9);// trueconsole.log(10>=10);// trueconsole.log(10<9);// falseconsole.log(10<=9);// falseconsole.log(10===10);// true — mesmo valor E mesmo tipoconsole.log(10!=="10");// true — tipos diferentes
Você já sabe da Aula 10 que == converte os tipos antes de comparar e produz resultados absurdos (0 == "" é true, "" == "0" é false). A regra desta trilha é definitiva: === e !==, sempre. Se um dos lados chegou como texto, converta explicitamente (Number(entrada) === 5) em vez de relaxar a comparação.
console.log("a"<"b");// true — ordem dos códigos Unicodeconsole.log("Z"<"a");// true — maiúsculas vêm antes das minúsculasconsole.log("10"<"9");// true — comparação de TEXTO: "1" vem antes de "9"console.log(10<9);// false — comparação de NÚMEROSconsole.log("Ana"<"Álvaro");// true — "A" (código 65) vem antes de "Á" (código 193)
As três últimas linhas são armadilhas clássicas. Comparar strings com < compara códigos Unicode, não ordem alfabética do português: todas as letras acentuadas ficam depois de todas as não acentuadas, então uma lista ordenada assim joga "Álvaro" para o fim, depois de "Zuleide". Para ordenar nomes em português, use localeCompare, que conhece as regras do idioma:
JavaScript
console.log("Ana".localeCompare("Álvaro","pt-BR"));// 1 → "Ana" vem DEPOIS de "Álvaro"console.log("ana".localeCompare("Ana","pt-BR"));// -1 → minúscula antes, sem virar outra letra
Repare no resultado oposto: para o operador <, "Ana" vem antes; para o português, "Álvaro" vem antes, porque o acento não muda a letra.
localeCompare devolve um número negativo, zero ou positivo — exatamente o que a função de ordenação de arrays espera, como você verá na Aula 12.
📌 Vale gravar
Três resultados costumam confundir: "10" < "9" é true (comparação textual), NaN === NaN é false (o único valor diferente de si mesmo) e 0.1 + 0.2 === 0.3 é false (ponto flutuante). Saiba explicar cada um em uma frase.
consttemVaga=true;constinscricoesAbertas=false;console.log(temVaga&&inscricoesAbertas);// false — E: só é true se AMBOS forem trueconsole.log(temVaga||inscricoesAbertas);// true — OU: é true se ALGUM for trueconsole.log(!temVaga);// false — NÃO: inverte
Expressão
&& (E)
\|\| (OU)
true com true
true
true
true com false
false
true
false com true
false
true
false com false
false
false
5.2 Curto-circuito: o que eles realmente devolvem¶
Aqui está a parte que quase ninguém aprende no primeiro contato. && e ||não devolvem true ou false — eles devolvem um dos operandos, e param de avaliar assim que o resultado está decidido. Isso se chama curto-circuito.
JavaScript
console.log(true&&"texto");// "texto" — o E precisa checar o segundo; devolve eleconsole.log(false&&"texto");// false — decidido no primeiro; o segundo nem é lidoconsole.log("primeiro"||"segundo");// "primeiro" — o OU para no primeiro truthyconsole.log(""||"padrão");// "padrão" — "" é falsy, então segue adianteconsole.log(0||100);// 100console.log(null&&"nunca lido");// null
Regra em uma frase: && devolve o primeiro valor falsy que encontrar (ou o último, se todos forem truthy); || devolve o primeiro valor truthy (ou o último, se todos forem falsy).
Isso permite dois padrões que você vai ver em todo código JavaScript moderno:
JavaScript
// Valor padrão quando a variável está vaziaconstcorEscolhida="";constcor=corEscolhida||"azul";// "azul"// Executar algo apenas se a condição for verdadeiraconstmodoDebug=true;modoDebug&&console.log("Modo de depuração ligado");
O segundo padrão é engenhoso, mas prefira um if de verdade: ele diz a intenção sem exigir que quem lê conheça o truque.
O || tem um defeito grave: ele considera 0 e "" valores "vazios", porque são falsy. Em um formulário, isso corrompe dados legítimos.
JavaScript
constinscritos=0;console.log(inscritos||87);// 87 — ERRADO: zero inscritos é um dado válido!console.log(inscritos??87);// 0 — CERTO
O operador ?? (nullish coalescing, coalescência nula) só usa o valor da direita quando o da esquerda é null ou undefined. Todo o resto — inclusive 0, "" e false — passa direto.
Valor da esquerda
\|\| "padrão"
?? "padrão"
undefined
"padrão"
"padrão"
null
"padrão"
"padrão"
0
"padrão"
0
""
"padrão"
""
false
"padrão"
false
Existe também a atribuição ??=, que só escreve se o valor atual for nulo:
⚠️ Atenção
Misturar ?? com && ou || na mesma expressão, sem parênteses, é um SyntaxError proposital da linguagem: a || b ?? c não compila. Os projetistas preferiram obrigar você a escrever (a || b) ?? c a deixar a ambiguidade passar. É uma das poucas vezes em que JavaScript é rígido — aproveite.
Ele funciona em três formas: objeto?.propriedade, objeto?.[chave] e funcao?.(). A combinação ?. com ?? é o par mais útil do JavaScript moderno: "leia com segurança e, se não houver nada, use este padrão".
💡 Dica
Não espalhe ?. por todo lado. Use onde o valor pode legitimamente não existir (um campo opcional, um elemento que só existe em uma página). Se o valor deveria existir e não existe, você quer o erro — ele está te avisando de um bug de verdade, e o ?. só o esconderia.
O JavaScript nasceu nos Estados Unidos, e por isso (1234.5).toString() devolve "1234.5" — ponto decimal, sem separador de milhar. Mostrar isso para um usuário brasileiro é um erro de produto. A solução padrão é a família Intl.
Um objeto Date guarda um instante como a quantidade de milissegundos desde 1º de janeiro de 1970, UTC — a chamada época Unix.
JavaScript
constagora=newDate();console.log(agora.getFullYear());// o ano, com quatro dígitosconsole.log(agora.getMonth());// 0 a 11 — janeiro é 0! (armadilha clássica)console.log(agora.getDate());// o dia do mês, 1 a 31console.log(agora.getDay());// o dia da semana, 0 (domingo) a 6 (sábado)console.log(agora.getHours());// a hora, 0 a 23console.log(agora.getTime());// milissegundos desde a época Unix
Para exibir, use os métodos toLocale*, que respeitam o idioma:
JavaScript
constagora=newDate();console.log(agora.toLocaleDateString("pt-BR"));// dia/mês/anoconsole.log(agora.toLocaleTimeString("pt-BR"));// hora:minuto:segundoconsole.log(agora.toLocaleDateString("pt-BR",{weekday:"long",day:"2-digit",month:"long",}));// algo como "segunda-feira, 05 de maio"
Subtrair duas datas devolve a diferença em milissegundos — porque o operador - converte cada objeto em número, exatamente a coerção que você estudou na Aula 10:
JavaScript
constMS_POR_DIA=1000*60*60*24;constagora=newDate();constdaquiUmaSemana=newDate();daquiUmaSemana.setDate(agora.getDate()+7);constdiferenca=daquiUmaSemana-agora;// número, em milissegundosconsole.log(Math.round(diferenca/MS_POR_DIA));// 7
setDate aceita valores fora da faixa e ajusta o mês sozinho: se hoje é dia 28 e você soma 7, o objeto vira dia 4 do mês seguinte, sem que você precise saber quantos dias tem o mês. É a forma segura de fazer contas com calendário.
🧠 Você sabia?
O getMonth() devolver 0 para janeiro não é um capricho: a API de datas do JavaScript foi copiada, em 1995 e às pressas, da classe java.util.Date do Java, que já tinha essa numeração. O erro se espalhou para milhões de linhas de código e nunca pôde ser corrigido. O comitê da linguagem trabalha desde então em uma substituta completa, a API Temporal, com meses de 1 a 12 e objetos imutáveis. Enquanto ela não chega a todos os navegadores, some 1 ao getMonth() — e prefira toLocaleDateString para exibir.
Até agora todo script que você escreveu executava de cima para baixo, sem desvios. A estrutura condicional é o que permite ao programa escolher um caminho.
A condição é convertida para booleano. Qualquer valor serve; os oito falsy da Aula 10 (false, 0, -0, 0n, "", null, undefined, NaN) levam ao else, todo o resto entra no if.
A ordem importa. Os testes são avaliados de cima para baixo e o primeiro verdadeiro vence — os demais nem são lidos. Se você escrever if (nota >= 4) antes de if (nota >= 6), ninguém nunca será aprovado.
Use sempre as chaves. JavaScript permite omiti-las quando há uma instrução só, e essa permissão já causou falhas de segurança famosas:
JavaScript
consttemVaga=false;if(temVaga)console.log("Inscrição liberada");console.log("Enviando e-mail de confirmação");// SEMPRE executa: não está no if!
O recuo engana o olho; o interpretador só enxerga a primeira linha como corpo do if. Com chaves, o problema não existe.
constvagasRestantes=12;constinscricoesAbertas=true;constidade=17;if(inscricoesAbertas&&vagasRestantes>0){console.log("Pode se inscrever");}if(idade<18||idade>=65){console.log("Precisa de autorização ou tem gratuidade");}// Erro clássico: JavaScript não entende "entre"// if (0 < vagasRestantes < 20) — não faça issoif(vagasRestantes>0&&vagasRestantes<20){console.log("Últimas vagas");}
Por que 0 < vagasRestantes < 20 não funciona? Porque a avaliação é da esquerda para a direita: 0 < 12 dá true, e então true < 20 compara 1 < 20, que também dá true — mesmo se vagasRestantes fosse 500. A condição parece certa e está sempre certa, que é o pior tipo de bug.
Para escolher um valor entre dois, o ternário é mais direto que um if:
JavaScript
constnota=7.5;constsituacao=nota>=6?"Aprovado":"Reprovado";console.log(situacao);// Muito usado dentro de template literalsconstvagas=3;console.log(`Restam ${vagas}${vagas===1?"vaga":"vagas"}.`);
Leia condicao ? valorSeVerdadeiro : valorSeFalso como uma pergunta: "a condição é verdadeira? então isto; senão, aquilo".
Ternários podem ser encadeados, e aí a legibilidade depende da formatação:
JavaScript
constocupacao=82;conststatus=ocupacao>=100?"esgotado":ocupacao>=80?"últimas vagas":ocupacao>=50?"enchendo":"vagas à vontade";console.log(status);// "últimas vagas"
Escrito assim, em uma coluna, o encadeamento vira quase uma tabela e continua legível. Escrito em uma linha só, vira um enigma. Regra: ternário para escolher um valor; if para executar blocos de instruções. Se você precisa fazer três coisas dentro de um ramo, o ternário é a ferramenta errada.
Quando a decisão é sobre um único valor comparado com várias possibilidades exatas, o switch é mais claro que uma escada de else if:
JavaScript
consttrilha="web";switch(trilha){case"web":console.log("Laboratório 1 — Desenvolvimento Web");break;case"dados":console.log("Laboratório 2 — Ciência de Dados");break;case"seguranca":console.log("Sala 105 — Segurança");break;default:console.log("Local a definir");}
Quatro pontos essenciais:
A comparação de cada case é estrita (===). case "3" não casa com o número 3.
break encerra o switch. Sem ele, a execução "vaza" para os casos seguintes e executa tudo até encontrar um break ou o fim do bloco.
default é o "senão" — pode ficar em qualquer posição, mas por convenção vai no fim.
O vazamento pode ser intencional: casos empilhados sem corpo compartilham o mesmo bloco.
JavaScript
constdiaDaSemana=6;switch(diaDaSemana){case0:case6:console.log("Fim de semana — campus fechado");break;case1:case2:case3:case4:case5:console.log("Dia útil — evento das 19h às 22h");break;default:console.log("Dia inválido");}
Um detalhe de escopo: as chaves do switch formam um único bloco, então declarar const dentro de um case afeta todos os outros. Se precisar de variáveis locais em um caso, embrulhe o corpo dele em chaves próprias:
JavaScript
consttipo="minicurso";switch(tipo){case"minicurso":{constduracao=4;console.log(`Minicurso de ${duracao} horas`);break;}case"palestra":{constduracao=1;console.log(`Palestra de ${duracao} hora`);break;}default:console.log("Tipo desconhecido");}
Executar blocos diferentes conforme faixas ou condições compostas
if / else if / else
Comparar um valor com várias possibilidades exatas
switch
Interromper cedo quando algo não faz sentido
if no topo, com aviso no console
🔬 Investigue
Cole no Console o switch do dia da semana acima, mas apague todos os break. Rode com diaDaSemana = 0 e observe: as três mensagens aparecem, uma atrás da outra, porque a execução vazou por todos os casos. Agora coloque o break só no primeiro caso e rode de novo com diaDaSemana = 1. Você acabou de reproduzir, de propósito, o bug mais difícil de enxergar em switch — e vai reconhecê-lo em um segundo quando ele acontecer sem querer.
💻 Mão na massa — A página de inscrição decide sozinha¶
Hoje o site da Semana Acadêmica de Sistemas de Informação ganha três comportamentos novos: o aviso automático de "Últimas vagas!", a taxa de inscrição formatada em reais e uma contagem regressiva na página inicial. Tudo com o que você acabou de aprender — nenhuma linha nova de manipulação da página além do textContent que você já usa desde a Aula 10.
Passo 1 — o parágrafo de aviso na página de inscrição¶
Abra inscricao.html e acrescente, logo abaixo da seção de vagas criada na Aula 10, um parágrafo vazio para o aviso e a linha da taxa.
site-evento/inscricao.html — trecho dentro do <main>, antes do formulário
HTML
<sectionclass="vagas"><h2>Vagas</h2><p>
Restam <strongid="vagas-restantes">—</strong> de
<spanid="vagas-totais">—</span> vagas
(<spanid="percentual-ocupacao">—</span>% ocupadas).
</p><pid="aviso-vagas"class="aviso"role="status"></p><pclass="taxa">
Taxa de inscrição: <strongid="valor-taxa">—</strong><spanid="observacao-taxa"></span></p></section>
Repare que o aria-live="polite"saiu do <section class="vagas"> — apague-o do seu HTML. Ele estava ali desde a Aula 10, quando a seção inteira era o que mudava; agora quem muda é o parágrafo #aviso-vagas, e ele já tem role="status", que implica aria-live="polite". Duas regiões vivas aninhadas fazem o leitor de tela anunciar a mesma coisa duas vezes, ou anunciar a seção inteira (título, contagem e taxa) a cada mudança de uma palavra. Uma região viva, no menor elemento que muda — essa é a regra.
O parágrafo #aviso-vagas nasce vazio. Quando o script não tiver nada a dizer, ele continua vazio — e o CSS do Passo 6 esconde parágrafos vazios, então nada aparece na tela. O role="status" faz leitores de tela anunciarem a mensagem quando ela surgir, sem interromper o que o usuário está fazendo.
Substitua o conteúdo de js/inscricao.js pelo arquivo abaixo. As cinco declarações do topo são a única parte que a organização do evento precisa editar durante a semana — todo o resto se ajusta sozinho.
site-evento/js/inscricao.js
JavaScript
// inscricao.js — carregado apenas por inscricao.html// ===== CONSTANTES DO EVENTO =====constVAGAS_TOTAIS=120;constLIMITE_ULTIMAS_VAGAS=20;// a partir daqui, o aviso aparececonstTAXA_INSCRICAO=25;// em reaisconstDESCONTO_ESTUDANTE=0.5;// 50%letinscritos=87;// let: muda quando o formulário funcionar (Aula 14)// ===== CÁLCULOS =====// Math.max evita número negativo se alguém digitar mais inscritos que vagasconstvagasRestantes=Math.max(0,VAGAS_TOTAIS-inscritos);constfracaoOcupada=inscritos/VAGAS_TOTAIS;constpercentualOcupacao=Math.min(100,fracaoOcupada*100);consttaxaComDesconto=TAXA_INSCRICAO*(1-DESCONTO_ESTUDANTE);console.group("Vagas");console.log("Totais:",VAGAS_TOTAIS);console.log("Inscritos:",inscritos);console.log("Restantes:",vagasRestantes);console.log("Ocupação:",percentualOcupacao.toFixed(1),"%");console.groupEnd();
Math.max(0, …) e Math.min(100, …) são a defesa contra dados inconsistentes: mesmo que alguém digite 130 inscritos, a página mostra 0 vagas e 100% de ocupação, em vez de "-10 vagas (108,3% ocupadas)".
Continue o mesmo arquivo. Esta é a parte que decide.
site-evento/js/inscricao.js — continuação
JavaScript
// ===== DECISÃO: qual mensagem o visitante deve ver =====letmensagem="";letsituacao="";if(vagasRestantes===0){situacao="esgotado";mensagem="Inscrições encerradas — todas as vagas foram preenchidas.";}elseif(vagasRestantes<=5){situacao="critico";mensagem=`Corra! Restam apenas ${vagasRestantes}${vagasRestantes===1?"vaga":"vagas"}.`;}elseif(vagasRestantes<=LIMITE_ULTIMAS_VAGAS){situacao="alerta";mensagem=`Últimas vagas! Menos de ${LIMITE_ULTIMAS_VAGAS} lugares disponíveis.`;}else{situacao="tranquilo";mensagem="";}console.log("Situação:",situacao);// ===== ESCRITA NA PÁGINA =====document.querySelector("#vagas-restantes").textContent=vagasRestantes;document.querySelector("#vagas-totais").textContent=VAGAS_TOTAIS;document.querySelector("#percentual-ocupacao").textContent=percentualOcupacao.toFixed(1);document.querySelector("#aviso-vagas").textContent=mensagem;
Repare na ordem dos testes: do caso mais restrito (=== 0) para o mais amplo (<= LIMITE_ULTIMAS_VAGAS). Se você invertesse as duas últimas condições, a mensagem "Corra!" nunca apareceria — porque vagasRestantes <= 20 também é verdadeira quando restam 3 vagas, e o primeiro teste verdadeiro vence.
O ternário dentro do template literal resolve o singular e o plural. Essa concordância é o tipo de detalhe que separa uma página feita com capricho de uma que mostra "Restam 1 vagas".
// ===== FORMATAÇÃO EM REAIS =====constformatarReal=newIntl.NumberFormat("pt-BR",{style:"currency",currency:"BRL",});// Estudantes pagam metade; o aviso só aparece se houver descontoconsttemDesconto=DESCONTO_ESTUDANTE>0;document.querySelector("#valor-taxa").textContent=formatarReal.format(TAXA_INSCRICAO);document.querySelector("#observacao-taxa").textContent=temDesconto?`(estudantes: ${formatarReal.format(taxaComDesconto)})`:"";console.log("Taxa cheia:",formatarReal.format(TAXA_INSCRICAO));console.log("Taxa com desconto:",formatarReal.format(taxaComDesconto));
Passo 5 — a contagem regressiva na página inicial¶
A contagem vive em js/app.js, que é carregado pelas cinco páginas. Só que o elemento da contagem existe apenas em index.html — nas outras páginas querySelector devolverá null, e tentar escrever em null derruba o script inteiro com Cannot read properties of null. A solução é um if: só escreve se o elemento existir.
site-evento/index.html — trecho dentro do herói do <main>
site-evento/js/app.js — acrescente ao fim do arquivo criado na Aula 10
JavaScript
// ===== CONTAGEM REGRESSIVA PARA A ABERTURA =====// A data do evento é calculada a partir de hoje, para que o exemplo nunca// fique no passado. No seu projeto autoral, troque o cálculo abaixo pela// data real do seu evento: new Date(ano, mes - 1, dia) — lembre do mes - 1.constDIAS_ATE_A_ABERTURA=45;constHORA_DE_ABERTURA=19;constMS_POR_DIA=1000*60*60*24;// Meia-noite de hoje: queremos contar dias de CALENDÁRIO, não horas corridasconstinicioDeHoje=newDate();inicioDeHoje.setHours(0,0,0,0);// new Date(outraData) cria uma cópia; sem isso, alterar uma alteraria a outraconstdataDoEvento=newDate(inicioDeHoje);dataDoEvento.setDate(inicioDeHoje.getDate()+DIAS_ATE_A_ABERTURA);// Math.round, não Math.ceil: no dia em que o horário de verão muda, um "dia"// tem 23 ou 25 horas, e a divisão dá 44,96 ou 45,04 — o round acerta os doisconstdiasRestantes=Math.round((dataDoEvento-inicioDeHoje)/MS_POR_DIA);constdataFormatada=dataDoEvento.toLocaleDateString("pt-BR",{weekday:"long",day:"2-digit",month:"long",});lettextoContagem="";if(diasRestantes>1){textoContagem=`Faltam ${diasRestantes} dias — abertura ${dataFormatada}, às ${HORA_DE_ABERTURA}h.`;}elseif(diasRestantes===1){textoContagem=`É amanhã! Abertura ${dataFormatada}, às ${HORA_DE_ABERTURA}h.`;}elseif(diasRestantes===0){textoContagem=`É hoje! Abertura às ${HORA_DE_ABERTURA}h.`;}else{textoContagem="Esta edição já aconteceu. Obrigado a quem participou!";}// O elemento só existe em index.html — nas outras páginas, querySelector devolve nullconstelementoContagem=document.querySelector("#contagem-regressiva");if(elementoContagem!==null){elementoContagem.textContent=textoContagem;}console.log("Dias restantes:",diasRestantes);
Esse if no fim é a lição prática da aula: um script compartilhado por várias páginas precisa verificar se o elemento existe antes de tocá-lo. Você poderia escrever a mesma verificação com curto-circuito (elementoContagem && (elementoContagem.textContent = textoContagem)), mas o if explícito é mais fácil de ler — e legibilidade vale mais que economia de caracteres.
site-evento/css/estilo.css — acrescente na seção de componentes
CSS
/* ===== Aviso de vagas (Aula 11) ===== */.aviso{margin-top:var(--espaco-pequeno);padding:var(--espaco-pequeno)var(--espaco-medio);border-left:4pxsolid#b42318;border-radius:var(--raio-borda);background:#fdf2f2;color:#8a1b12;font-weight:600;}/* Se o script não escreveu nada, o parágrafo some da tela */.aviso:empty{display:none;}.taxa{margin-top:var(--espaco-medio);}.contagem{display:inline-block;padding:var(--espaco-pequeno)var(--espaco-medio);border-radius:999px;background:var(--cor-primaria);color:#fff;font-weight:600;}
A pseudoclasse :empty casa com elementos que não têm nenhum filho, nem texto. É a forma mais limpa de esconder um aviso que ainda não tem conteúdo, sem precisar de JavaScript para mexer em classes — isso fica para a Aula 13.
Use os nomes de variáveis do seu sistema de design da Aula 06; os acima seguem os da Mão na massa da Aula 07.
Abra inscricao.html no Live Server com inscritos = 87. A página mostra 33 de 120 vagas, 72.5% de ocupação, taxa R$ 25,00 com observação de R$ 12,50 — e nenhum aviso (33 é maior que o limite de 20).
Troque para inscritos = 105, salve e recarregue: aparece a faixa vermelha com "Últimas vagas! Menos de 20 lugares disponíveis."
Troque para inscritos = 119: a mensagem vira "Corra! Restam apenas 1 vaga." — no singular.
Troque para inscritos = 130: a página mostra 0 vagas, 100.0% e "Inscrições encerradas". Nenhum número negativo aparece. Volte para 87.
Abra index.html: o balão azul mostra "Faltam 45 dias — abertura …, às 19h.", com o dia da semana e o mês por extenso em português.
Abra contato.html e olhe o Console: a linha "Dias restantes: 45" aparece e nenhum erro vermelho — porque o if protegeu o script na página que não tem o elemento da contagem.
Comente o if (deixando só a atribuição) e recarregue contato.html: aparece Uncaught TypeError: Cannot read properties of null (reading 'textContent'). Desfaça.
Resultado esperado: a página de inscrição muda de comportamento sozinha ao trocar um único número, e a página inicial mostra a contagem sem que ninguém precise editar o HTML.
A1. Diferencie let, const e var quanto a três critérios: escopo, reatribuição e redeclaração. Escreva um exemplo curto de cada diferença.
A2. Por que const lista = [1, 2]; lista.push(3); funciona, mas lista = [3] produz Uncaught TypeError: Assignment to constant variable.? Explique com suas palavras o que exatamente const congela.
A3. Explique escopo de bloco com um exemplo que produza Uncaught ReferenceError: x is not defined. Depois altere o exemplo para produzir Uncaught ReferenceError: Cannot access 'x' before initialization e explique a diferença entre as duas mensagens.
A4. Escreva um ternário que atribua "Par" ou "Ímpar" a uma variável paridade, conforme o valor de n. Teste com n = 0, n = 7 e n = -4.
A5. Qual a diferença entre || e ??? Dê um caso concreto do site do evento em que os dois produzem resultados diferentes, e diga qual é o correto.
A6. Qual o resultado de 0.1 + 0.2 === 0.3? Explique a causa e escreva a forma correta de comparar esses dois valores.
A7. Qual a diferença entre % e /? Escreva a expressão que verifica se um número é par e a que descobre quantas horas completas há em 7300 segundos.
A8. Diferencie ++i de i++ com um exemplo em que os dois produzem saídas diferentes no console.log.
A9. Escreva a condicional encadeada que converte uma média em conceito: >= 9 vira A, >= 7 vira B, >= 6 vira C, o resto vira D. Depois reescreva com ternário encadeado e diga qual das duas versões você prefere e por quê.
A10. Quando switch é preferível a uma escada de else if? Por que o break é praticamente obrigatório, e em que situação omiti-lo é intencional?
A11. Anote sua previsão para cada linha e só depois teste no Console:
A12. O trecho abaixo deveria mostrar "Últimas vagas", mas mostra sempre a mesma coisa, para qualquer valor. Encontre o erro sem rodar e explique por que a condição é sempre verdadeira.
JavaScript
constvagas=500;if(0<vagas<20){console.log("Últimas vagas");}else{console.log("Vagas à vontade");}
B1. Escreva validarSenha(senha), que devolva um objeto { valida: true, erros: [] } verificando quatro regras: mínimo de 8 caracteres, ao menos uma letra maiúscula, ao menos um dígito e ao menos um caractere que não seja letra nem dígito. Sem expressões regulares (elas são da Aula 14) — use os métodos de string da Aula 10 e comparações de caractere.
Resultado esperado: validarSenha("abc") devolve valida: false com três erros descritivos; validarSenha("Semana@Web7") devolve valida: true e lista vazia; cada erro é uma frase que o usuário entenderia, não um código.
Dica
Percorrer a string caractere a caractere é assunto da Aula 12, mas você não precisa disso: senha.toUpperCase() !== senha já indica que existe pelo menos uma minúscula, e senha.toLowerCase() !== senha indica uma maiúscula. Para o dígito, teste se "0123456789".includes(caractere) para os caracteres que você extrair com senha[0], senha.at(-1) e comparações. Monte a lista de erros com um array e push — a Aula 12 formaliza, mas const erros = []; erros.push("texto"); já funciona.
B2. Escreva converterTemperatura(valor, de, para) que aceite "C", "F" e "K" em qualquer combinação. Use switch para identificar a unidade de origem, converta tudo para Celsius internamente e depois para a unidade de destino. Se a unidade for inválida, devolva null e escreva um console.error explicando.
Converter tudo para uma unidade intermediária evita escrever nove fórmulas — bastam seis. As fórmulas: C = (F - 32) * 5 / 9, C = K - 273.15, F = C * 9 / 5 + 32, K = C + 273.15. Use de.toUpperCase() antes do switch para aceitar "c" e "C".
B3. Escreva classificarIMC(peso, altura) que calcule o índice de massa corporal (peso / altura ** 2) e devolva um objeto com o valor arredondado para uma casa e a faixa correspondente (abaixo do peso, adequado, sobrepeso, obesidade). Trate entradas inválidas: valores não numéricos, zero ou negativos devem devolver null com um aviso no console.
Comece pelas validações, com um if que devolve null logo no início — isso evita aninhar todo o resto do código dentro de um else. Number.isFinite(peso) é melhor que typeof peso === "number", porque NaN e Infinity também são do tipo number. Para arredondar em uma casa e continuar com um número (não uma string), use Math.round(imc * 10) / 10.
B4. Implemente calcularFrete(uf, peso, valorCompra) com regras encadeadas: frete grátis acima de R$ 200; base de R$ 15,00; acréscimo de R$ 10,00 para as UFs do Norte e Centro-Oeste; acréscimo de R$ 2,00 por quilo acima de 10 kg. Devolva o valor já formatado em reais com Intl.NumberFormat.
Teste a regra do frete grátis primeiro e devolva imediatamente — as outras regras não precisam ser avaliadas. Para as UFs, "AC AM AP PA RO RR TO DF GO MS MT".includes(uf.toUpperCase()) resolve sem array. O peso excedente é Math.max(0, peso - 10), o que dispensa um if.
B5. Escreva analisarInscricao(nome, idade, tipoIngresso) que valide os três campos e devolva um objeto com { valida, mensagem, valorDevido }. Regras: nome com pelo menos 3 caracteres depois do trim(); idade entre 14 e 120; tipoIngresso só pode ser "estudante", "profissional" ou "apoiador", com valores de R$ 12,50, R$ 25,00 e R$ 50,00. Menores de 18 anos recebem, além do resultado, um aviso de que precisam de autorização.
Resultado esperado: analisarInscricao("Ana", 17, "estudante") devolve valida: true, valorDevido: 12.5 e uma mensagem que menciona a autorização; analisarInscricao(" ", 30, "estudante") devolve valida: false com mensagem sobre o nome; analisarInscricao("Ana", 30, "vip") devolve valida: false com mensagem sobre o tipo de ingresso.
Dica
Use switch para o preço e if para as validações. O aviso de menor de idade é um bom uso do operador && dentro de um template literal: ${idade < 18 ? " Menores precisam de autorização." : ""}. Devolva sempre o mesmo formato de objeto, mesmo quando inválido — quem chama a função não deveria precisar checar se a propriedade existe.
B6. Reescreva o bloco de decisão do Passo 3 da Mão na massa usando apenas ternários encadeados, sem nenhum if. Depois escreva um parágrafo comparando as duas versões: qual é mais fácil de ler? Qual é mais fácil de alterar quando surgir uma quinta faixa?
Resultado esperado: dois arquivos (decisao-if.js e decisao-ternario.js) que produzem exatamente a mesma saída para os valores 0, 3, 15, 33 e 130 de inscritos, e um texto de comparação com pelo menos três argumentos.
Dica
Para que a versão com ternário produza duas variáveis (situacao e mensagem), você precisará de dois encadeamentos — ou de um objeto. Formate o encadeamento em coluna, uma condição por linha, como no exemplo da seção 7.3. Ao comparar, considere também o caso de um ramo precisar executar duas instruções: o que acontece com o ternário nessa hora?
C1. No seu projeto autoral, crie o equivalente ao painel de vagas do site do evento: um bloco de estado que muda sozinho conforme um único número que você edita no topo do script. Requisitos: pelo menos quatro faixas de decisão (com if/else if), um valor formatado com Intl.NumberFormat (preço, distância, peso — o que fizer sentido no seu domínio), uma data formatada com toLocaleDateString("pt-BR"), concordância correta de singular e plural com ternário, e nenhum número mágico no meio do código (todos os limites devem ser constantes nomeadas no topo).
Dica
Comece escrevendo, em comentários, as quatro faixas em português: "se não há nenhum, então…; se há menos de 5, então…". Só depois traduza para if. Teste cada faixa trocando o número e recarregando — se alguma nunca aparece, a ordem dos testes está errada. Lembre-se de usar Math.max(0, …) para nunca exibir um número negativo.
C2. Escreva js/relogio-evento.js que, ao carregar qualquer página, imprima no Console um único console.group chamado "Relógio do evento" com: a data e a hora atuais formatadas em pt-BR; quantos dias faltam para a abertura (usando a constante DIAS_ATE_A_ABERTURA); a fase do evento como texto ("pré-inscrições" acima de 30 dias, "inscrições abertas" entre 30 e 8, "reta final" entre 7 e 1, "acontecendo" em 0, "encerrado" abaixo de 0), decidida com switch (true); e o tempo restante quebrado em dias, horas e minutos com %.
Dica
switch (true) é uma forma pouco conhecida e perfeitamente válida: cada case recebe uma expressão booleana, e o primeiro case cujo valor for true é executado — case diasRestantes > 30:. Para a quebra em dias/horas/minutos, calcule a diferença total em milissegundos uma vez e vá dividindo: dias com Math.trunc(ms / MS_POR_DIA), horas com Math.trunc((ms % MS_POR_DIA) / MS_POR_HORA), e assim por diante.
Quantas vezes você precisa sortear um número de 1 a 6 até que os seis valores tenham saído pelo menos uma vez? A intuição diz "umas dez"; a matemática diz 14,7 em média; e o seu computador pode dizer a verdade em três segundos. Antes de aprender laços (isso é a Aula 12), você consegue investigar isso na mão: um script que sorteia uma vez por recarregamento, guarda o resultado e mostra a contagem. Use o sorteio da seção 3.3 e as condicionais de hoje para construir um mini-experimento honesto — e descubra, de quebra, se Math.random() é tão uniforme quanto promete.
Critérios de pronto
Um arquivo sorteio.js que, a cada recarregamento, sorteia um inteiro de 1 a 6 e imprime no Console o valor e um comentário decidido por switch (por exemplo, "número da sorte", "de novo esse não").
Uma versão com faixa configurável por duas constantes (MINIMO e MAXIMO) no topo, funcionando corretamente para 1..6, 0..9 e 10..20.
Uma verificação, com console.assert, de que o valor sorteado nunca sai da faixa — inclusive quando MINIMO === MAXIMO.
Um comentário de três linhas explicando, com suas palavras, por que a fórmula usa Math.floor e por que existe o + 1.
Pistas
Troque Math.floor por Math.round na fórmula e recarregue trinta vezes anotando os resultados: os extremos saem com metade da frequência dos outros. Descubra por quê.
Math.random() nunca devolve exatamente 1 — a documentação da MDN diz "de 0 (inclusivo) até 1 (exclusivo)". É essa exclusão que faz o Math.floor funcionar.
Para guardar a contagem entre recarregamentos, uma linha basta: localStorage. Procure localStorage.getItem na MDN — a Aula 14 aprofunda o assunto.
O script abaixo deveria classificar as inscrições do evento e mostrar o preço final. Ele roda sem nenhuma linha vermelha no Console — e mesmo assim está errado em cinco pontos: uma faixa nunca é alcançada, um desconto legítimo é ignorado, um preço aparece com quinze casas decimais, uma comparação é sempre verdadeira e um switch executa dois casos de uma vez. Nenhum erro é de sintaxe; todos são de lógica. Encontre os cinco usando o depurador da aba Sources, não console.log espalhado.
A organização da Semana Acadêmica precisa saber, a qualquer momento, em que fase está: falta mais de um mês, está na reta final, é hoje, ou já acabou? E precisa que a página inicial diga isso em português correto — "falta 1 dia", "faltam 2 dias", "é hoje", "começa em 3 horas". Datas são a área do JavaScript onde mais gente escorrega: fuso horário, mês que começa em zero, mudança de mês, horário de verão. Construa um módulo de cronograma que acerte todos esses casos e prove que acerta.
Critérios de pronto
Um arquivo cronograma.js com as constantes do evento no topo (dias até a abertura, hora de início, duração em dias) e nenhum número mágico no restante do código.
Uma variável de texto que descreve a situação com concordância correta em seis casos: mais de um dia, exatamente um dia, hoje antes da abertura (mostrando horas e minutos), durante o evento, no último dia e depois do fim.
A quebra do tempo restante em dias, horas e minutos usando % e Math.trunc, sem nenhuma biblioteca externa.
A data de abertura exibida por extenso em português com toLocaleDateString("pt-BR", …), incluindo o dia da semana.
Uma seção de testes no fim do arquivo que força os seis casos alterando a constante de dias (inclusive valores negativos) e imprime o resultado de cada um com console.group.
O texto é escrito em um elemento com role="status", para que leitores de tela anunciem a mudança.
Pistas
Comece pelo caso mais difícil: o dia da abertura. Nesse dia, diasRestantes é 0, mas ainda podem faltar horas — e a mensagem "faltam 0 dias" seria absurda.
Para forçar os seis casos sem esperar o calendário, transforme a constante de dias em uma variável e escreva um bloco de testes que a altera e recalcula. É exatamente assim que se testa código dependente de tempo.
Math.ceil e Math.trunc dão respostas diferentes para 0,3 dia restante. Decida qual você quer antes de escrever a condição, e escreva um comentário justificando.
Consulte a página de Intl.DateTimeFormat na MDN e compare com toLocaleDateString: os dois aceitam as mesmas opções, e um deles é mais eficiente quando o formato se repete.
Cuidado com o fuso: uma string ISO só com a data (no formato AAAA-MM-DD) é interpretada como UTC e pode "voltar um dia" no Brasil. Passar os números separados, new Date(ano, mes - 1, dia), evita a armadilha.
Parte 1 — Leitura (20 min). FLANAGAN, D. JavaScript: o guia definitivo, capítulos sobre expressões, operadores e instruções. Na MDN, leia "Expressões e operadores" e a página do operador de coalescência nula (links em Para aprofundar). Anote dois operadores que existem na MDN e não apareceram nesta aula.
Parte 2 — Produção (30 min). Produza os exercícios B1 (validarSenha) e B4 (calcularFrete) em arquivos .js comentados, cada um com pelo menos cinco casos de teste demonstrados no Console. No seu projeto autoral, produza o exercício C1: o bloco de estado que decide sozinho, com as constantes no topo, quatro faixas e formatação em pt-BR.
Critério de pronto: ao trocar um único número no topo do script do projeto autoral e recarregar, a mensagem e o destaque visual mudam de faixa; nenhuma página do projeto mostra erro vermelho no Console; nenhum número mágico aparece no meio do código; nenhum var e nenhum == sobreviveram nos arquivos .js.
Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: defenda, com argumentos técnicos, a prática de declarar tudo como const por padrão e usar let só quando a reatribuição for necessária. Traga um trecho do seu próprio projeto em que a escolha importou.
Guarde no seu repositório: commit + push (ou a pasta do projeto).
javascript.info: https://javascript.info/ — capítulos "Operators", "Comparisons", "Conditional branching" e "Logical operators"; cada um tem exercícios com solução comentada.
web.dev — Learn JavaScript: https://web.dev/learn/javascript — os módulos de operadores e de fluxo de controle, com exemplos interativos.
FLANAGAN, David. JavaScript: o guia definitivo. Bookman, 2014 — capítulos de expressões, operadores e instruções.
STEFANOV, Stoyan. Padrões JavaScript. Novatec, 2010 — capítulo 2, na parte sobre escopo e declaração de variáveis.
MILETTO, Evandro M.; BERTAGNOLLI, Silvia C. Desenvolvimento de software II. Bookman, 2014 — capítulo sobre lógica de programação aplicada à web.
Na próxima aula, o programa deixa de fazer uma coisa de cada vez: você vai aprender as estruturas de repetição (for, while, do…while, for…of), organizar informação em arrays e objetos e produzir os primeiros relatórios do evento no Console — totais por dia, busca por trilha e a lista de palestras que vai alimentar as páginas nas aulas seguintes.
[ ] Site do evento com js/app.js (contagem regressiva) e js/inscricao.js (vagas e aviso automático) funcionando, sem erros no Console.
[ ] Console do navegador aberto e a aba Sources localizada — hoje ela sai do papel de coadjuvante.
[ ] Revisar da Aula 11: const/let e escopo de bloco, operadores aritméticos e lógicos, if/else if/else, ternário e switch.
[ ] Revisar da Aula 10: template literals, typeof, valores falsy e console.table.
Na aula passada o seu script aprendeu a decidir: a página de inscrição escolhe sozinha entre quatro mensagens e a página inicial calcula quantos dias faltam para o evento. Mas ele ainda trata um dado de cada vez — um número de inscritos, uma data, uma taxa. Um site de evento real tem doze atividades, seis palestrantes, três dias e quatro trilhas. Hoje você aprende a guardar coleções de informação em arrays e objetos e a percorrê-las com estruturas de repetição, produzindo relatórios completos no Console. Tudo ainda acontece no console — na próxima aula esses mesmos dados viram cartões na tela.
Em 1966, dois pesquisadores italianos, Corrado Böhm e Giuseppe Jacopini, provaram um resultado que organizou a programação para sempre: qualquer algoritmo computável pode ser escrito combinando apenas três estruturas.
Estrutura
O que faz
Em JavaScript
Sequência
executa instruções, uma após a outra, na ordem escrita
linhas soltas, uma embaixo da outra
Decisão
escolhe entre caminhos conforme uma condição
if, else if, else, ternário, switch
Repetição
repete um trecho enquanto uma condição for verdadeira
for, while, do…while, for…of, for…in
Você já domina as duas primeiras. A terceira é a que falta — e é a que muda a escala do que você consegue fazer: com repetição, o mesmo código que trata uma palestra trata mil.
1.1 Sequência: a ordem importa mais do que parece¶
JavaScript
// js/exemplos-sequencia.jsletinscritos=87;constnovos=5;inscritos=inscritos+novos;// 92constpercentual=inscritos/120;// usa o valor JÁ atualizadoconsole.log(percentual.toFixed(2));// "0.77"
Se as duas últimas linhas trocassem de lugar, o percentual seria calculado sobre 87 e ficaria errado — sem nenhum erro no Console. Esse é o tipo de bug que a estrutura sequencial esconde: o programa roda, o número aparece, e está errado.
O exemplo clássico de dependência de ordem é a troca de valores entre duas variáveis:
JavaScript
letprimeiro="Ana";letsegundo="Bruno";// ERRADO: o valor de primeiro se perde na primeira linha// primeiro = segundo;// segundo = primeiro; // "Bruno" — os dois ficam iguais// CERTO: uma variável temporária guarda o valor que seria perdidolettemporario=primeiro;primeiro=segundo;segundo=temporario;console.log(primeiro,segundo);// "Bruno" "Ana"
Em JavaScript moderno existe um atalho — a desestruturação de array, que você vê na seção 5.6: [primeiro, segundo] = [segundo, primeiro]. Mas entender por que a variável temporária é necessária vale mais do que decorar o atalho.
O cabeçalho tem três partes separadas por ponto e vírgula, e cada uma roda em um momento diferente:
Parte
Quando executa
Papel
let i = 0
uma única vez, antes de tudo
inicialização do contador
i < 5
antes de cada repetição
condição de continuação
i++
depois de cada repetição
atualização do contador
Traduzindo para português: "comece com i valendo 0; enquanto i for menor que 5, execute o corpo; depois de cada execução, some 1 a i".
Duas convenções: o contador quase sempre se chama i (de índice), e ele quase sempre começa em zero — porque os índices de arrays começam em zero, como você verá na seção 3.
JavaScript
// Contagem regressiva: o contador pode decrescerfor(leti=3;i>0;i--){console.log(i);}console.log("Começou!");// 3, 2, 1, Começou!// O passo não precisa ser 1for(leti=0;i<=20;i+=5){console.log(i);// 0, 5, 10, 15, 20}
⚠️ Atenção
Declare o contador com let, nunca com const: o i++ reatribui a variável, e com const você recebe Uncaught TypeError: Assignment to constant variable.. E declare dentro do for (for (let i = 0; …)), não antes: assim o i existe só dentro do laço e não polui o resto do arquivo.
letsaldoVagas=8;letfila=0;while(saldoVagas>0){fila++;saldoVagas-=2;// cada grupo da fila ocupa 2 vagas}console.log("Grupos atendidos:",fila);// 4
O while testa antes de executar. Se a condição já começar falsa, o corpo não roda nenhuma vez. Use-o quando a quantidade de repetições depende de algo que muda dentro do laço.
Aqui o teste vem depois, então o corpo executa pelo menos uma vez, mesmo com a condição falsa desde o início:
JavaScript
letcontador=100;do{console.log("Executei mesmo assim");}while(contador<5);
É a escolha certa quando a ação precisa acontecer antes de haver o que testar: pedir um dado, sortear um valor, tentar uma conexão. Fora desses casos, o while comum é mais previsível.
Quando você só quer os valores de uma coleção e não se importa com o índice, o for…of é mais curto e mais seguro:
JavaScript
consttrilhas=["Desenvolvimento Web","Ciência de Dados","Segurança","Inteligência Artificial"];for(consttrilhaoftrilhas){console.log(trilha);}
Repare no const: cada repetição cria uma variável nova, então não há reatribuição — e o const funciona. Não há contador, não há length, não há como errar o limite.
for…of funciona com tudo que é iterável: arrays, strings, Map, Set e a lista de elementos que você vai obter do DOM na Aula 13.
JavaScript
for(constletraof"WebLab"){console.log(letra);// W, e, b, L, a, b}
Se você precisar do índice e do valor ao mesmo tempo, use entries() com desestruturação:
O for…in percorre as chaves (nomes de propriedades) de um objeto:
JavaScript
constpalestrante={nome:"Ana Lúcia",instituicao:"Universidade Estadual",area:"ia"};for(constchaveinpalestrante){console.log(`${chave}: ${palestrante[chave]}`);}// nome: Ana Lúcia// instituicao: Universidade Estadual// area: ia
Repare que o valor é lido com palestrante[chave], notação de colchetes — porque o nome da propriedade está em uma variável. Escrever palestrante.chave procuraria uma propriedade literalmente chamada "chave", que não existe.
⚠️ Atenção
Não use for…in em arrays. Ele percorre os índices como texto ("0", "1", "2") e também qualquer propriedade extra que alguém tenha adicionado ao array, em ordem não garantida. Para arrays use for…of (valores), for clássico (índices) ou forEach. A regra é fácil de guardar: in para objetos, of para arrays.
constnotas=[8,6,9,4,7,10];for(constnotaofnotas){if(nota<5){continue;// pula esta repetição e vai para a próxima}if(nota===10){break;// encerra o laço inteiro}console.log("Nota válida:",nota);}// 8, 6, 9, 7 — o 4 foi pulado e o laço parou antes de imprimir o 10
continue pula o restante do corpo e segue para a próxima repetição. break abandona o laço imediatamente. Os dois deixam o código mais direto quando substituem um if gigante que envolve todo o corpo — mas dois ou três break espalhados no mesmo laço são sinal de que a lógica pede outra estrutura.
// NÃO EXECUTE: o navegador congela// let i = 0;// while (i < 10) {// console.log(i); // faltou o i++: a condição nunca fica falsa// }
Um laço infinito trava a aba inteira, porque o JavaScript do navegador roda em uma única linha de execução: enquanto o laço não termina, nada é desenhado e nenhum clique é atendido. As três causas mais comuns:
Esquecer de atualizar a variável da condição (o caso acima).
Atualizar na direção errada (i-- em um laço que testa i < 10).
Comparar decimais com === — for (let x = 0; x !== 1; x += 0.1) nunca para, porque a soma nunca dá exatamente 1, como você viu na Aula 11.
Se travar: feche a aba (Ctrl+W) ou use o gerenciador de tarefas do navegador (Shift+Esc no Chrome). Depois, antes de rodar de novo, coloque um freio de segurança:
JavaScript
leti=0;letvoltas=0;constLIMITE_DE_SEGURANCA=1000;while(i<10){voltas++;if(voltas>LIMITE_DE_SEGURANCA){console.error("Laço passou do limite — provável laço infinito");break;}i++;}
🧠 Você sabia?
A pergunta "este programa vai parar algum dia?" tem nome próprio: problema da parada. Alan Turing provou, em 1936, que é impossível escrever um programa capaz de responder a essa pergunta para qualquer outro programa — não por falta de esforço ou de computador rápido, mas por impossibilidade lógica. É por isso que nem o navegador nem o VS Code conseguem avisar "você escreveu um laço infinito": eles só percebem que a página parou de responder e oferecem encerrá-la. A responsabilidade de garantir que o laço termina é, e sempre será, sua.
Um laço dentro de outro percorre duas dimensões. O de fora anda uma vez para cada volta completa do de dentro:
JavaScript
// Tabuada de 1 a 5for(lettabuada=1;tabuada<=5;tabuada++){console.group(`Tabuada do ${tabuada}`);for(letmultiplicador=1;multiplicador<=10;multiplicador++){console.log(`${tabuada} x ${multiplicador} = ${tabuada*multiplicador}`);}console.groupEnd();}
São 5 × 10 = 50 execuções do corpo interno. Esse produto é o que torna laços aninhados perigosos: dois laços sobre uma lista de 1.000 itens dão 1.000.000 de execuções. Com três, um bilhão. Antes de aninhar, pergunte se não existe uma estrutura de dados que evite o segundo laço — no seu projeto, quase sempre existe.
Um uso legítimo e comum: agrupar por categoria, como a programação por dia que você vai montar na Mão na massa.
constnotas=[8.5,6.0,9.0,4.5,7.0];constvazio=[];constmisturado=["texto",42,true,null];// permitido, mas eviteconsole.log(notas[0]);// 8.5 — o primeiro item tem índice 0console.log(notas[4]);// 7 — o último de cinco itensconsole.log(notas[5]);// undefined — não existe, e não dá erroconsole.log(notas.length);// 5 — quantidade de itensconsole.log(notas[notas.length-1]);// 7 — forma clássica de pegar o últimoconsole.log(notas.at(-1));// 7 — forma moderna: índice negativo conta do fimconsole.log(notas.at(-2));// 4.5
O detalhe que mais gera bug: o índice do último item é length - 1, não length. Um laço escrito com i <= notas.length lê uma posição a mais e devolve undefined — o famoso erro "por um" (off-by-one).
JavaScript
// Errado: lê o índice 5, que não existefor(leti=0;i<=notas.length;i++){console.log(notas[i]);// a última linha imprime undefined}// Certofor(leti=0;i<notas.length;i++){console.log(notas[i]);}
constfila=["Ana","Bruno","Carla"];fila.push("Diego");// adiciona no fim → ["Ana","Bruno","Carla","Diego"]fila.pop();// remove do fim e devolve "Diego"fila.unshift("Zuleide");// adiciona no início → ["Zuleide","Ana","Bruno","Carla"]fila.shift();// remove do início e devolve "Zuleide"fila.splice(1,1);// remove 1 item a partir do índice 1 → ["Ana","Carla"]fila.splice(1,0,"Beto");// insere "Beto" no índice 1 sem remover nadaconsole.log(fila);// ["Ana", "Beto", "Carla"]constnumeros=[10,9,1,25];numeros.sort((a,b)=>a-b);// ordena numericamente → [1, 9, 10, 25]numeros.reverse();// inverte → [25, 10, 9, 1]console.log(numeros);
push e pop funcionam no fim do array e são rápidos; shift e unshift funcionam no início e obrigam o motor a reposicionar todos os itens — em listas grandes, prefira o fim.
⚠️ Atençãosort()sem função comparadora converte tudo em texto antes de comparar: [10, 9, 1].sort() devolve [1, 10, 9], porque "10" vem antes de "9" na ordem de texto (você viu isso na Aula 11). Para números, passe sempre (a, b) => a - b (crescente) ou (a, b) => b - a (decrescente). Para textos em português, (a, b) => a.localeCompare(b, "pt-BR").
Estes não tocam no array original — devolvem outra coisa:
JavaScript
constnotas=[8.5,6.0,9.0,4.5,7.0];console.log(notas.slice(1,3));// [6, 9] — do índice 1 até ANTES do 3console.log(notas.slice(-2));// [4.5, 7] — os dois últimosconsole.log(notas.concat([10]));// [8.5, 6, 9, 4.5, 7, 10]console.log([...notas,10]);// idem, com o operador spreadconsole.log(notas.indexOf(9.0));// 2 — o índice, ou -1 se não existirconsole.log(notas.includes(9.0));// true — só quer saber se existeconsole.log(notas.join(" · "));// "8.5 · 6 · 9 · 4.5 · 7" — vira stringconsole.log(notas);// o original continua intacto
O operador spread (...) "espalha" os itens de um array. Ele é a forma moderna de copiar e juntar listas:
JavaScript
constdia1=["Abertura","Git do zero"];constdia2=["Acessibilidade","Segurança"];consttudo=[...dia1,...dia2];// junta as duasconstcopia=[...dia1];// cópia independenteconstcomExtra=[...dia1,"Extra"];// cópia com um item a maiscopia.push("Só na cópia");console.log(dia1);// ["Abertura", "Git do zero"] — o original não mudouconsole.log(tudo.length,comExtra.length);// 4 3
Isso importa por causa de uma característica da linguagem: arrays e objetos são guardados por referência. const copia = dia1 não cria uma cópia, cria um segundo nome para a mesma lista — mexer em um muda o outro. O spread resolve.
JavaScript
constoriginal=[1,2,3];constapelido=original;// MESMA lista, dois nomesconstcopiaDeVerdade=[...original];apelido.push(4);console.log(original);// [1, 2, 3, 4] — mudou!console.log(copiaDeVerdade);// [1, 2, 3] — independente
🔬 Investigue
Cole no Console: const a = [1, 2, 3]; const b = a; const c = [...a]; b.push(99);. Agora inspecione a, b e c. O array a tem o 99, mesmo você tendo alterado b. Faça o mesmo teste com um número: let x = 1; let y = x; y = 99; — aí x continua 1. Você acabou de ver a diferença entre valor (primitivos, copiados) e referência (arrays e objetos, compartilhados). Essa é a causa da maioria dos bugs "eu não mexi nessa lista!" que você vai encontrar no semestre.
Os métodos a seguir recebem uma função como argumento e a aplicam a cada item. Eles substituem a maior parte dos for que você escreveria — com menos código e menos chance de erro de índice.
A sintaxe (item) => item.preco é uma arrow function, formalizada na Aula 13. Por enquanto, leia assim: "recebe item, devolve item.preco".
map devolve um array novo, do mesmo tamanho, com cada item transformado. É a ferramenta que, na Aula 13, transforma dados em cartões de HTML.
E olhe de novo para o primeiro resultado: 27.500000000000004, e não 27.5. É o ponto flutuante da Aula 11 aparecendo de novo — 1.1 não tem representação exata em binário, e o erro se propaga item a item. Por isso a segunda linha existe: toFixed(2) (ou o Intl.NumberFormat da Aula 11) é o que você mostra na tela; o número cru fica para as contas.
filter devolve um array novo com os itens que passaram no teste — de tamanho menor ou igual ao original. Se ninguém passar, devolve [] (array vazio, que é truthy: teste com length === 0, não com if (!lista)).
constalunos=[{nome:"Ana",nota:8.5},{nome:"Bruno",nota:5.0},{nome:"Carla",nota:9.0},];constana=alunos.find((aluno)=>aluno.nome==="Ana");console.log(ana);// { nome: "Ana", nota: 8.5 }constposicao=alunos.findIndex((aluno)=>aluno.nota<6);console.log(posicao);// 1constninguem=alunos.find((aluno)=>aluno.nota===10);console.log(ninguem);// undefined — sempre teste antes de usar!
find devolve o item (ou undefined); findIndex devolve o índice (ou -1). Confundir os dois é erro comum: if (alunos.findIndex(…)) é sempre verdadeiro quando o índice é diferente de 0, e falso quando o item achado está na primeira posição.
constnotas=[8.5,6.0,9.0,4.5,7.0];constsoma=notas.reduce((acumulado,nota)=>acumulado+nota,0);console.log(soma);// 35console.log(soma/notas.length);// 7 — a média
reduce é o mais poderoso e o mais confuso à primeira vista. Ele recebe dois argumentos: a função e o valor inicial do acumulador (o 0 no fim). A cada volta, a função recebe o acumulado até agora e o item atual, e devolve o novo acumulado.
Repare no ?? 0 da Aula 11: na primeira vez que uma área aparece, conta[area] é undefined, e o ?? o substitui por zero.
⚠️ AtençãoSempre passe o valor inicial do reduce. Sem ele, o primeiro item vira o acumulador — o que quebra quando o array está vazio (Uncaught TypeError: Reduce of empty array with no initial value) e produz resultados errados quando o acumulador tem tipo diferente dos itens.
constnotas=[8.5,6.0,9.0,4.5];console.log(notas.some((n)=>n===10));// false — algum é 10?console.log(notas.some((n)=>n<5));// true — algum está abaixo de 5?console.log(notas.every((n)=>n>=4));// true — todos são >= 4?console.log(notas.every((n)=>n>=6));// false
Como map e filter devolvem arrays, você pode ligá-los em sequência. Leia sempre da esquerda para a direita, como uma frase:
JavaScript
constalunos=[{nome:"Ana",nota:8.5},{nome:"Bruno",nota:5.0},{nome:"Carla",nota:9.0},{nome:"Diego",nota:3.5},];constaprovadosEmOrdem=alunos.filter((aluno)=>aluno.nota>=6)// fica só quem passou.map((aluno)=>aluno.nome)// vira lista de nomes.sort((a,b)=>a.localeCompare(b,"pt-BR"))// ordena em português.join(", ");// vira uma stringconsole.log(aprovadosEmOrdem);// "Ana, Carla"
Uma armadilha escondida nesse encadeamento: sortaltera o array em que atua. Aqui isso é seguro, porque o array veio de map e é descartável. Mas alunos.sort(…) bagunçaria a ordem original dos seus dados para sempre. Quando quiser ordenar sem estragar o original, ordene uma cópia: [...alunos].sort(…).
📌 Vale gravarmap transforma e devolve um array do mesmo tamanho; filter seleciona e devolve um array de tamanho menor ou igual; forEach percorre e devolve undefined. Usar map só para imprimir no console é erro de intenção — o array criado é jogado fora. Saiba dizer o que cada um devolve.
constpalestrante={nome:"Ana Lúcia Ferreira",instituicao:"Universidade Estadual — Sinop",area:"ia",confirmado:true,temas:["redes neurais","agricultura de precisão"],contato:{email:"ana.ferreira@exemplo.edu.br",cidade:"Sinop",},};console.log(palestrante.nome);// "Ana Lúcia Ferreira" — notação de pontoconsole.log(palestrante["instituicao"]);// notação de colchetesconsole.log(palestrante.contato.cidade);// "Sinop" — objeto aninhadoconsole.log(palestrante.temas[0]);// "redes neurais" — array dentro de objetoconsole.log(palestrante.telefone);// undefined — propriedade inexistente
Cada par é uma propriedade: um nome (a chave) e um valor. O valor pode ser qualquer coisa — número, texto, booleano, array, outro objeto.
Quando usar colchetes em vez de ponto? Em dois casos: quando o nome da propriedade está em uma variável (como no for…in) e quando o nome tem espaços ou caracteres especiais.
JavaScript
constcampo="area";console.log(palestrante[campo]);// "ia" — lê a propriedade cujo nome está na variávelconsole.log(palestrante.campo);// undefined — procura uma propriedade chamada "campo"
Repare que tudo isso funciona mesmo com palestrante declarado como const: como você viu na Aula 10, const congela a ligação entre o nome e o objeto, não o conteúdo do objeto.
constpalestrante={nome:"Ana Lúcia",instituicao:"Universidade Estadual",area:"ia"};// Com for...infor(constchaveinpalestrante){console.log(chave,"→",palestrante[chave]);}// Com os utilitários de Objectconsole.log(Object.keys(palestrante));// ["nome", "instituicao", "area"]console.log(Object.values(palestrante));// ["Ana Lúcia", "Universidade Estadual", "ia"]console.log(Object.entries(palestrante));// [["nome","Ana Lúcia"], ["instituicao","Universidade Estadual"], ["area","ia"]]// Object.entries + for...of + desestruturação: o padrão mais legívelfor(const[chave,valor]ofObject.entries(palestrante)){console.log(`${chave}: ${valor}`);}
Object.keys, values e entries devolvem arrays — o que significa que todos os métodos da seção 4 funcionam com eles. É assim que se filtra ou ordena as propriedades de um objeto.
constbase={nome:"Ana",area:"ia",confirmado:false};constcopia={...base};// cópia independenteconstatualizado={...base,confirmado:true};// cópia com uma alteraçãoconstcomExtra={...base,sala:"Lab 3"};// cópia com propriedade novaconsole.log(atualizado);// { nome: "Ana", area: "ia", confirmado: true }console.log(base.confirmado);// false — o original não mudou
A ordem importa: propriedades escritas depois do spread sobrescrevem as que vieram dele. Esse padrão — copiar e alterar em vez de modificar no lugar — é a base do funcionamento de Vue e React, que você verá no Nível 3.
🔎 Por baixo do capô
O spread faz uma cópia rasa (shallow): as propriedades de primeiro nível são copiadas, mas objetos aninhados continuam sendo a mesma referência. Em const copia = { ...palestrante }, alterar copia.nome não afeta o original, mas alterar copia.contato.cidadeafeta, porque contato é o mesmo objeto nos dois. Para uma cópia profunda de dados simples, existe structuredClone(objeto), disponível em todos os navegadores modernos.
this é um assunto grande, com armadilhas próprias — a Aula 13 volta a ele ao falar de arrow functions. Por ora, guarde a forma: dentro de um método, this.propriedade acessa o próprio objeto.
Desestruturar é extrair propriedades para variáveis soltas, em uma linha:
JavaScript
constpalestrante={nome:"Ana Lúcia Ferreira",area:"ia",contato:{email:"ana@exemplo.edu.br",cidade:"Sinop"},};// Sem desestruturaçãoconstnomeAntigo=palestrante.nome;constareaAntiga=palestrante.area;// Com desestruturaçãoconst{nome,area}=palestrante;console.log(nome,area);// Renomeando e com valor padrãoconst{nome:nomeCompleto,telefone="não informado"}=palestrante;console.log(nomeCompleto,telefone);// "Ana Lúcia Ferreira" "não informado"// Aninhadaconst{contato:{cidade}}=palestrante;console.log(cidade);// "Sinop"// Em arrays, a posição é que mandaconst[primeiraNota,segundaNota]=[8.5,7.0,9.0];console.log(primeiraNota,segundaNota);// 8.5 7// A troca de valores da seção 1.1, agora em uma linhaleta="Ana";letb="Bruno";[a,b]=[b,a];console.log(a,b);// "Bruno" "Ana"
Você vai reencontrar a desestruturação em toda parte a partir da Aula 13 — é a forma padrão de receber parâmetros de função e de ler dados de uma API.
Junte as duas ideias e você tem a estrutura de dados mais comum do desenvolvimento web: uma lista de registros, cada um com as mesmas propriedades. É o formato de uma tabela de banco de dados, de uma resposta de API em JSON e da lista de produtos de qualquer loja.
JavaScript
constprodutos=[{id:1,nome:"Notebook",preco:3500,categoria:"informatica",estoque:12},{id:2,nome:"Mouse",preco:80,categoria:"informatica",estoque:0},{id:3,nome:"Caderno",preco:25,categoria:"papelaria",estoque:40},];// Quais estão disponíveis?constdisponiveis=produtos.filter((p)=>p.estoque>0);console.log(disponiveis.length);// 2// Quanto vale o estoque inteiro?constvalorTotal=produtos.reduce((total,p)=>total+p.preco*p.estoque,0);console.log(valorTotal);// 43000// Do mais barato ao mais caro (sem estragar o original)constporPreco=[...produtos].sort((a,b)=>a.preco-b.preco);console.log(porPreco.map((p)=>p.nome));// ["Caderno", "Mouse", "Notebook"]// Achar pelo identificadorconstitem=produtos.find((p)=>p.id===3);console.log(item.nome);// "Caderno"// Busca por nome parcial, ignorando maiúsculasconsttermo="note";constachados=produtos.filter((p)=>p.nome.toLowerCase().includes(termo.toLowerCase()));console.log(achados.map((p)=>p.nome));// ["Notebook"]
Três convenções que você deve adotar desde já:
Todo registro tem um id único e estável. É por ele que você acha, atualiza e remove — nunca pela posição no array, que muda a cada ordenação.
Todos os registros têm as mesmas propriedades. Se um item não tem valor para uma delas, use null ou "", não omita a propriedade — assim map e filter nunca encontram undefined inesperado.
Valores de categoria são códigos curtos e sem acento ("ia", "web"), com o texto bonito ("Inteligência Artificial") ficando na exibição. Isso evita comparar strings com acento e maiúscula, e é exatamente o que os filtros da Aula 13 vão precisar.
Clique no número da linha dentro do laço: aparece um marcador azul.
Recarregue a página. A execução para ali, com a linha destacada.
No painel Scope, à direita, veja o valor de cada variável naquele instante — inclusive o contador do laço.
Use os botões do topo: F10 executa a próxima linha, F11 entra em uma função, F8 continua até o próximo breakpoint.
7.2 Breakpoint condicional — o truque que economiza a aula¶
Parar 200 vezes é inútil. Clique com o botão direito no número da linha e escolha Add conditional breakpoint. Digite uma condição, por exemplo i === 47 ou palestra.inscritos > palestra.vagas. A execução só para quando a condição for verdadeira — e você chega direto no caso problemático.
Há ainda o logpoint (botão direito → Add logpoint): ele imprime uma mensagem sem parar a execução e sem sujar o seu código com console.log que você depois esquece de remover.
No painel Watch, clique em + e digite uma expressão — palestras.length, total / contador, palestra.vagas - palestra.inscritos. O DevTools recalcula e mostra o valor a cada passo. É a forma mais rápida de descobrir em qual volta a conta começou a errar.
🔬 Investigue
Crie um arquivo com o laço for (let i = 0; i < 100; i++) { const quadrado = i * i; } e ponha um breakpoint condicional em i === 42. Recarregue: a execução para na quadragésima terceira volta e o painel Scope mostra i: 42. Pressione F10 uma vez e veja quadrado aparecer valendo 1764. Agora adicione i * 2 ao painel Watch e pressione F8 — o breakpoint não dispara de novo, porque a condição só é verdadeira uma vez. Compare o esforço disso com o de encontrar a mesma informação em cem linhas de console.log.
💻 Mão na massa — Os dados do evento e os relatórios no Console¶
Até agora, cada informação do site da Semana Acadêmica de Sistemas de Informação estava digitada no HTML, uma por uma. Hoje você cria a fonte única de dados do projeto — um arquivo com dois arrays de objetos — e escreve os primeiros relatórios sobre eles. Na Aula 13 esses mesmos dados deixam o console e viram cartões na tela.
Passo 1 — criar js/dados.js e carregá-lo antes dos outros¶
Crie o arquivo js/dados.js e inclua-o no <head> das cinco páginas, antes de js/app.js. A ordem importa: com defer, os scripts executam na ordem em que aparecem no HTML, e o app.js precisa que os dados já existam.
site-evento/index.html — trecho do <head> (repita nas cinco páginas)
// dados.js — fonte única de dados do site do evento.// Carregado antes de todos os outros scripts, em todas as páginas.// A partir da Aula 13, estas listas alimentam as páginas de verdade.constpalestrantes=[{id:1,nome:"Ana Lúcia Ferreira",instituicao:"Universidade Estadual — Sinop",area:"ia",tema:"Redes neurais para prever a safra de soja",foto:"img/palestrante-01.jpg",},{id:2,nome:"Bruno Takahashi",instituicao:"Startup AgroData",area:"dados",tema:"Dashboards que os produtores realmente usam",foto:"img/palestrante-02.jpg",},{id:3,nome:"Carla Mendes",instituicao:"UFMT",area:"seguranca",tema:"O que um ataque de phishing ensina sobre UX",foto:"img/palestrante-03.jpg",},{id:4,nome:"Diego Nascimento",instituicao:"Prefeitura de Sinop",area:"web",tema:"Acessibilidade em portais públicos: erros que vimos",foto:"img/palestrante-04.jpg",},{id:5,nome:"Eduarda Ribeiro",instituicao:"Universidade Estadual — Sinop",area:"web",tema:"Do HTML ao deploy: o caminho do estudante",foto:"img/palestrante-05.jpg",},{id:6,nome:"Felipe Arruda",instituicao:"Cooperativa Coopercana",area:"ia",tema:"Visão computacional no controle de pragas",foto:"img/palestrante-06.jpg",},];
Repare que area guarda um código curto e sem acento. O nome bonito de cada área vive em um objeto separado, que funciona como um dicionário de tradução:
site-evento/js/dados.js — continuação
JavaScript
// Dicionário de áreas: código → nome de exibiçãoconstnomesDasAreas={web:"Desenvolvimento Web",dados:"Ciência de Dados",seguranca:"Segurança",ia:"Inteligência Artificial",};
constpalestras=[{id:1,titulo:"Abertura e palestra magna: o futuro do desenvolvimento web",tipo:"palestra",area:"web",dia:1,hora:"19:00",local:"Auditório Central",vagas:300,inscritos:212,palestranteId:5,},{id:2,titulo:"Minicurso: Git e GitHub do zero",tipo:"minicurso",area:"web",dia:1,hora:"20:00",local:"Laboratório 2",vagas:30,inscritos:30,palestranteId:4,},{id:3,titulo:"Mesa-redonda: mercado de trabalho em Sinop",tipo:"mesa",area:"web",dia:1,hora:"20:00",local:"Sala 105",vagas:80,inscritos:47,palestranteId:5,},{id:4,titulo:"Dashboards que os produtores realmente usam",tipo:"palestra",area:"dados",dia:1,hora:"21:00",local:"Auditório Central",vagas:300,inscritos:96,palestranteId:2,},{id:5,titulo:"Minicurso: acessibilidade na prática",tipo:"minicurso",area:"web",dia:2,hora:"19:00",local:"Laboratório 1",vagas:30,inscritos:28,palestranteId:4,},{id:6,titulo:"Minicurso: primeiros passos com redes neurais",tipo:"minicurso",area:"ia",dia:2,hora:"19:00",local:"Laboratório 3",vagas:25,inscritos:25,palestranteId:1,},{id:7,titulo:"Segurança em aplicações web: dez erros comuns",tipo:"palestra",area:"seguranca",dia:2,hora:"20:30",local:"Auditório Central",vagas:300,inscritos:154,palestranteId:3,},{id:8,titulo:"Visão computacional no controle de pragas",tipo:"palestra",area:"ia",dia:2,hora:"21:00",local:"Sala 105",vagas:80,inscritos:61,palestranteId:6,},{id:9,titulo:"Maratona de programação",tipo:"maratona",area:"web",dia:3,hora:"18:30",local:"Laboratórios 1 e 2",vagas:60,inscritos:45,palestranteId:5,},{id:10,titulo:"Minicurso: phishing e engenharia social",tipo:"minicurso",area:"seguranca",dia:3,hora:"19:00",local:"Laboratório 3",vagas:25,inscritos:19,palestranteId:3,},{id:11,titulo:"Dados abertos e cidades inteligentes",tipo:"palestra",area:"dados",dia:3,hora:"20:00",local:"Auditório Central",vagas:300,inscritos:88,palestranteId:2,},{id:12,titulo:"Encerramento e premiação",tipo:"palestra",area:"web",dia:3,hora:"22:00",local:"Auditório Central",vagas:300,inscritos:130,palestranteId:5,},];console.log(`dados.js carregado: ${palestras.length} atividades, ${palestrantes.length} palestrantes.`);
O campo palestranteId é uma referência: em vez de repetir nome, instituição e foto em cada palestra, guardamos só o identificador e buscamos o resto quando precisar. É exatamente o que uma chave estrangeira faz em um banco de dados, assunto do Nível 2.
// relatorios.js — relatórios do evento no Console.// Depende de js/dados.js, que precisa ser carregado antes.constDIAS_DO_EVENTO=3;constformatarPercentual=newIntl.NumberFormat("pt-BR",{style:"percent",maximumFractionDigits:1,});// ===== 1. A programação completa em tabela =====console.group("Programação completa");console.table(palestras,["dia","hora","titulo","local"]);console.groupEnd();// ===== 2. Totais gerais (reduce) =====consttotalVagas=palestras.reduce((soma,p)=>soma+p.vagas,0);consttotalInscritos=palestras.reduce((soma,p)=>soma+p.inscritos,0);console.group("Totais gerais");console.log("Atividades:",palestras.length);console.log("Vagas oferecidas:",totalVagas);console.log("Inscrições feitas:",totalInscritos);console.log("Ocupação geral:",formatarPercentual.format(totalInscritos/totalVagas));console.groupEnd();// ===== 3. Resumo por dia (laço aninhado: dias × atividades) =====console.group("Resumo por dia");for(letdia=1;dia<=DIAS_DO_EVENTO;dia++){constdoDia=palestras.filter((p)=>p.dia===dia);constinscritosDoDia=doDia.reduce((soma,p)=>soma+p.inscritos,0);console.group(`Dia ${dia} — ${doDia.length} atividades, ${inscritosDoDia} inscrições`);for(constpalestraofdoDia){console.log(`${palestra.hora} · ${palestra.titulo} (${palestra.local})`);}console.groupEnd();}console.groupEnd();// ===== 4. Contagem por área (reduce com objeto acumulador) =====constporArea=palestras.reduce((conta,p)=>{conta[p.area]=(conta[p.area]??0)+1;returnconta;},{});console.group("Atividades por área");for(const[codigo,quantidade]ofObject.entries(porArea)){console.log(`${nomesDasAreas[codigo]}: ${quantidade}`);}console.groupEnd();// ===== 5. Atividades esgotadas e as mais concorridas =====constesgotadas=palestras.filter((p)=>p.inscritos>=p.vagas);constmaisConcorrida=palestras.reduce((maior,p)=>p.inscritos/p.vagas>maior.inscritos/maior.vagas?p:maior);console.group("Alertas");console.log("Esgotadas:",esgotadas.map((p)=>p.titulo));console.log("Mais concorrida:",maisConcorrida.titulo,formatarPercentual.format(maisConcorrida.inscritos/maisConcorrida.vagas));console.log("Alguma atividade sem inscritos?",palestras.some((p)=>p.inscritos===0));console.log("Todas têm local definido?",palestras.every((p)=>p.local!==""));console.groupEnd();// ===== 6. Busca por trilha (filter + includes) =====consttrilhaProcurada="seguranca";constdaTrilha=palestras.filter((p)=>p.area===trilhaProcurada);console.group(`Trilha: ${nomesDasAreas[trilhaProcurada]}`);if(daTrilha.length===0){console.log("Nenhuma atividade nesta trilha.");}else{daTrilha.forEach((p)=>console.log(`Dia ${p.dia}, ${p.hora} — ${p.titulo}`));}console.groupEnd();// ===== 7. Juntando as duas listas (find) =====console.group("Quem apresenta o quê");for(constpalestraofpalestras){constresponsavel=palestrantes.find((pessoa)=>pessoa.id===palestra.palestranteId);constnome=responsavel?.nome??"A definir";console.log(`${palestra.titulo} → ${nome}`);}console.groupEnd();// ===== 8. Palestras ordenadas por ocupação (cópia + sort) =====constporOcupacao=[...palestras].sort((a,b)=>b.inscritos/b.vagas-a.inscritos/a.vagas).map((p)=>({titulo:p.titulo,ocupacao:formatarPercentual.format(p.inscritos/p.vagas),}));console.group("Ranking de ocupação");console.table(porOcupacao);console.groupEnd();
Três detalhes valem atenção:
console.table(palestras, ["dia", "hora", "titulo", "local"]) — o segundo argumento escolhe quais colunas mostrar. Sem ele, a tabela sai com as dez propriedades e fica ilegível na projeção.
responsavel?.nome ?? "A definir" — se algum palestranteId apontar para um identificador inexistente, find devolve undefined; o ?. evita o TypeError e o ?? fornece o texto padrão. É a dupla da Aula 11 trabalhando junto com o find de hoje.
[...palestras].sort(…) — a cópia protege a ordem original. Sem os três pontinhos, o array palestras ficaria reordenado para todos os scripts que rodarem depois.
Abra programacao.html no Live Server e o Console (F12). Devem aparecer oito grupos, todos recolhíveis, e nenhuma linha vermelha.
A primeira linha do Console deve ser dados.js carregado: 12 atividades, 6 palestrantes., provando que a ordem dos scripts está certa.
Em "Totais gerais": 1.830 vagas, 935 inscrições e ocupação de 51,1%.
Em "Resumo por dia": 4 atividades em cada um dos três dias.
Em "Alertas": duas atividades esgotadas (o minicurso de Git e o de redes neurais) e a resposta false para "Alguma atividade sem inscritos?".
Em "Quem apresenta o quê": nenhum "A definir". Troque o palestranteId de uma palestra para 99, recarregue e confira que aparece "A definir" — sem erro. Desfaça.
Troque trilhaProcurada para "ia" e recarregue: o grupo passa a listar duas atividades. Troque para "robotica": aparece "Nenhuma atividade nesta trilha." Volte para "seguranca".
Ponha um breakpoint condicional na linha do for (const palestra of palestras) com a condição palestra.dia === 3 e recarregue: a execução para na primeira atividade do terceiro dia. Confira no painel Scope.
Resultado esperado: toda a informação do evento vive em um único arquivo de dados, e qualquer relatório novo é uma linha de filter, map ou reduce — nenhuma informação repetida no HTML.
A1. Qual a diferença entre for…of e for…in? Quando usar cada um? O que acontece se você usar for…in em um array?
A2. Qual a diferença entre map, filter e forEach? O que cada um devolve? Dê um exemplo em que usar map é erro de intenção.
A3. Dado const n = [4, 8, 15, 16, 23, 42], escreva expressões que devolvam: (a) só os pares; (b) cada valor dobrado; (c) a soma total; (d) o primeiro maior que 20; (e) true se todos forem positivos; (f) os três maiores, em ordem decrescente.
A4. Dado const p = { nome: "Ana", end: { cidade: "Sinop", uf: "MT" } }, escreva: (a) o acesso à cidade; (b) a desestruturação de nome; (c) uma cópia com a propriedade fase valendo 3; (d) um array com todas as chaves de p; (e) o acesso à cidade usando notação de colchetes com o nome da propriedade em uma variável.
A5. O que é uma estrutura sequencial? Reescreva o trecho abaixo na ordem correta e explique por que a ordem original produz um resultado errado sem gerar nenhum erro.
A6. Escreva o mesmo laço, que imprime os números de 1 a 10, nas quatro formas: for, while, do…while e for…of (neste último, sobre um array criado com [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).
A7. Qual a diferença entre break e continue? Escreva um laço que percorra [3, 7, 0, 5, 9] e imprima os valores, pulando os zeros e parando ao encontrar um número maior que 8.
A8. Escreva um laço infinito acidental (sem executá-lo), explique por que ele nunca termina e mostre as duas maneiras de corrigi-lo.
A9. Quando do…while é a escolha correta? Dê um exemplo do site do evento em que ele seria mais adequado que while.
A10. Escreva um laço aninhado que gere a tabuada de 1 a 5, agrupada com console.group. Quantas vezes o corpo interno executa? Mostre a conta.
A11. Anote sua previsão para cada linha e só depois teste no Console:
A12. O trecho abaixo deveria imprimir os cinco nomes, mas imprime seis linhas e a última é undefined. Encontre o erro, corrija e explique o que é um erro "por um".
B1. Usando o array palestras da Mão na massa, escreva funções que devolvam: (a) o total de vagas ainda livres no evento; (b) as atividades sem nenhuma vaga livre; (c) a atividade com mais inscritos; (d) um objeto com a média de ocupação por área; (e) a lista ordenada por horário dentro de cada dia; (f) uma busca por título parcial, ignorando maiúsculas e acentos.
Resultado esperado: seis funções testadas no Console, cada uma com pelo menos duas chamadas de exemplo; a busca por "git" encontra "Minicurso: Git e GitHub do zero"; a busca por "programacao" (sem cedilha nem til) encontra "Maratona de programação".
Dica
Para (d), use reduce com um objeto acumulador guardando soma e contagem por área, e só depois divida. Para (f), normalize os dois lados da comparação com texto.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase() — a expressão regular será formalizada na Aula 14, mas você pode usá-la como receita e explicar em um comentário o que ela faz.
B2. Escreva contarPalavras(texto) que devolva um objeto com cada palavra e sua frequência, ignorando maiúsculas e pontuação. Depois escreva topPalavras(texto, n) que devolva as n mais frequentes, em ordem decrescente, como um array de objetos { palavra, vezes }.
Resultado esperado: contarPalavras("O evento é bom. O evento é gratuito!") devolve { o: 2, evento: 2, é: 2, bom: 1, gratuito: 1 }; topPalavras(texto, 2) devolve os dois primeiros em ordem decrescente de frequência.
Dica
texto.toLowerCase().split(/[^a-zà-ú]+/) quebra o texto em palavras descartando pontuação; filtre as strings vazias que sobram nas pontas. Para ordenar o resultado, Object.entries(contagem) transforma o objeto em array de pares — e aí sort e map funcionam normalmente.
B3. Escreva gerarTabuada(n) que devolva uma string formatada com a tabuada de 1 a 10 de n (uma linha por multiplicação), e gerarTodasTabuadas() que devolva as tabuadas de 1 a 10 concatenadas, separadas por uma linha em branco. Use apenas laços e template literals.
Resultado esperado: gerarTabuada(7) devolve uma string de dez linhas começando por "7 x 1 = 7"; console.log(gerarTodasTabuadas()) imprime 10 blocos legíveis no Console.
Dica
Acumule com texto += `${n} x ${i} = ${n * i}\n`. O \n é a quebra de linha da Aula 10 — e o console.log de uma string com \n já imprime em várias linhas. Uma alternativa mais elegante: monte um array de linhas e use join("\n") no fim.
B4. Usando o array palestrantes, escreva um único encadeamento (filter + map + sort + join) que produza uma string com os nomes dos palestrantes das áreas de web e de IA, em ordem alfabética portuguesa, separados por vírgula.
Resultado esperado: uma linha de código (quebrada em várias por legibilidade) que devolve exatamente "Ana Lúcia Ferreira, Diego Nascimento, Eduarda Ribeiro, Felipe Arruda".
Dica
O filtro precisa aceitar duas áreas: ["web", "ia"].includes(pessoa.area) é mais limpo que duas comparações com ||. Na ordenação, localeCompare(b, "pt-BR") é obrigatório para que "Ana Lúcia" apareça antes de "Diego" mesmo com acentos.
B5. Escreva analisarTurma(alunos) que receba um array de objetos { nome, notas: [] } e devolva um objeto com: a média de cada aluno, a média geral da turma, o melhor e o pior desempenho e a quantidade de aprovados (média maior ou igual a 6). Use apenas métodos de ordem superior, sem nenhum for.
Resultado esperado: para uma turma de cinco alunos, um objeto com medias (array de { nome, media }), mediaGeral, melhor, pior e aprovados; todas as médias arredondadas em uma casa decimal.
Dica
Comece com map para calcular a média de cada aluno (um reduce dentro do map). Com esse array pronto, mediaGeral é outro reduce, melhor e pior saem de um reduce comparativo (ou de um sort sobre uma cópia) e aprovados é filter(...).length. Arredonde só no fim, nunca no meio — arredondar antes de somar distorce o resultado.
B6. Escreva agruparPorDia(palestras) que devolva um objeto no formato { 1: [...], 2: [...], 3: [...] }, com as atividades de cada dia já ordenadas por horário. Depois imprima o resultado com for…in e console.group, uma seção por dia.
Resultado esperado: um objeto com três chaves; agruparPorDia(palestras)[2].length devolve 4; dentro de cada dia, a primeira atividade é a de horário mais cedo.
Dica
Um reduce com objeto acumulador resolve o agrupamento: se a chave do dia ainda não existe, crie um array vazio antes de dar push. Como hora é uma string no formato "HH:MM", a comparação de texto já ordena corretamente — "09:00" < "19:00" é true. Isso só funciona porque o formato tem dois dígitos sempre; explique isso em um comentário.
C1. Modele o seu projeto autoral como dados. Crie js/dados.js com pelo menos doze registros em um array de objetos, cada um com no mínimo seis propriedades (incluindo id único, um campo de categoria em código curto, um campo numérico e um campo de texto longo). Crie também o objeto-dicionário que traduz os códigos de categoria em nomes de exibição. Depois escreva js/relatorios.js com pelo menos seis relatórios diferentes no Console: um console.table filtrado, um total com reduce, uma contagem por categoria, um ranking ordenado, uma busca por texto parcial e um relatório que combine duas listas com find.
Dica
Escolha as propriedades pensando no que a página vai mostrar na Aula 13 — se um cartão precisa de foto, o registro precisa do caminho da imagem. Doze registros parecem muitos, mas são o mínimo para que filtros e ordenações mostrem alguma coisa interessante; com três, todo relatório fica igual. Copie a estrutura do dados.js do evento e troque o domínio, não a arquitetura.
C2. Escreva primos(n) que devolva todos os números primos até n usando o crivo de Eratóstenes: crie um array de n + 1 posições marcadas como true, percorra a partir do 2 e marque como false todos os múltiplos de cada número que ainda estiver marcado. Meça o tempo com console.time para n = 100, n = 10.000 e n = 1.000.000 e compare com a abordagem ingênua (testar a divisibilidade de cada número por todos os anteriores).
Dica
new Array(n + 1).fill(true) cria o array inicial. O laço externo só precisa ir até Math.sqrt(n) — todo múltiplo maior já terá sido marcado por um fator menor. O laço interno pode começar em i * i, não em i * 2, pelo mesmo motivo. A diferença de tempo entre as duas abordagens em um milhão de números é da ordem de mil vezes; anote os números medidos.
Todo conjunto de dados esconde uma informação que ninguém tinha pensado em perguntar. Com os arrays palestras e palestrantes da Mão na massa, quantas perguntas interessantes você consegue responder em uma linha cada? Qual instituição tem mais representantes? Qual horário concentra mais inscrições? Existe alguma sala ociosa? Descubra três fatos sobre o evento que não aparecem em nenhum relatório do Passo 4 — e prove cada um com código, não com opinião.
Critérios de pronto
Um arquivo curiosidades.js com três relatórios novos, cada um resolvido com uma única expressão encadeada (filter, map, reduce, sort), acompanhada de um comentário de uma linha explicando a pergunta que ele responde.
Cada relatório imprime o resultado com console.group e um título em português.
Pelo menos um dos três usa Object.entries para percorrer o resultado de um agrupamento.
Um comentário no fim do arquivo com a descoberta mais surpreendente e o que a organização do evento deveria fazer a respeito.
Pistas
Agrupar é sempre reduce com um objeto acumulador: conta[chave] = (conta[chave] ?? 0) + 1.
Para "qual é o maior", reduce comparativo evita ordenar a lista inteira — e é mais rápido em listas grandes.
Duas listas se cruzam com find (um item) ou filter (vários): "quantas atividades cada palestrante tem" é um map sobre palestrantes com um filter sobre palestras dentro.
O script abaixo deveria montar o resumo da programação. Ele roda, imprime números — e todos estão errados. São seis defeitos: um erro "por um", um for…in usado onde deveria ser for…of, um sort que ordena como texto, um reduce sem valor inicial que quebra com lista vazia, um array modificado durante a iteração e um find cujo resultado é usado sem verificação. Encontre os seis usando breakpoints condicionais — não console.log espalhado.
js/resumo-com-bug.js
JavaScript
constatividades=[{id:1,titulo:"Abertura",dia:1,inscritos:212},{id:2,titulo:"Git do zero",dia:1,inscritos:30},{id:3,titulo:"Acessibilidade",dia:2,inscritos:28},{id:4,titulo:"Segurança",dia:2,inscritos:154},{id:5,titulo:"Maratona",dia:3,inscritos:45},];lettotal=0;for(leti=0;i<=atividades.length;i++){total+=atividades[i].inscritos;}console.log("Total de inscritos:",total);for(constatividadeinatividades){console.log("Atividade:",atividade.titulo);}constporInscritos=atividades.sort((a,b)=>a.inscritos>b.inscritos);console.log("Mais concorrida:",porInscritos[0].titulo);constvazias=[];constmedia=vazias.reduce((soma,a)=>soma+a.inscritos);console.log("Média das vazias:",media);atividades.forEach((a)=>{if(a.inscritos<50){atividades.splice(atividades.indexOf(a),1);}});console.log("Sobraram:",atividades.length);constprocurada=atividades.find((a)=>a.id===99);console.log("Procurada:",procurada.titulo);
Critérios de pronto
Um arquivo resumo-corrigido.js que imprime: total de 469 inscritos, os cinco títulos, "Segurança" como mais concorrida, 0 como média da lista vazia, a contagem correta das atividades com menos de 50 inscritos e uma mensagem amigável quando o find não acha nada.
O array original atividades continua com cinco itens ao fim do script (nenhum método destrutivo agiu sobre ele).
Um arquivo bugs.md com uma tabela de seis linhas: sintoma, causa e correção.
Pelo menos um breakpoint condicional usado durante a investigação, documentado com a condição que você digitou.
Pistas
O primeiro erro produz Uncaught TypeError: Cannot read properties of undefined (reading 'inscritos') — e a linha do erro diz exatamente qual índice foi longe demais.
for…in sobre um array entrega as chaves como texto; imprima typeof atividade dentro do laço para se convencer.
Uma função comparadora de sort precisa devolver um número (negativo, zero ou positivo), não um booleano. true vira 1 e false vira 0 — a ordenação fica indefinida.
Remover itens de um array enquanto o percorre faz o índice interno pular posições. Prefira filter, que constrói uma lista nova.
Todo find pode devolver undefined. A dupla ?. e ?? da Aula 11 resolve em uma linha.
Você já agrupou atividades por dia e contou por área. Agora repare: os dois códigos são quase idênticos — muda apenas a propriedade usada como chave. Programadores experientes sentem coceira ao ver isso e escrevem uma função que resolve os dois casos. Construa agruparPor(lista, chave), que funcione com qualquer array de objetos e qualquer propriedade, e depois estenda-a para aceitar também uma função que calcula a chave — o que permite agrupar por faixas ("lotado", "meio cheio", "vazio") e não só por valores existentes. Ao terminar, compare a sua versão com o método nativo Object.groupBy e descubra por que ele ainda não pode ser usado em qualquer projeto.
Critérios de pronto
agruparPor(palestras, "dia") devolve { 1: [...], 2: [...], 3: [...] }, e agruparPor(palestrantes, "area") devolve as quatro áreas — com a mesma função, sem nenhum if sobre o nome da propriedade.
agruparPor(palestras, (p) => p.inscritos >= p.vagas ? "esgotada" : "com vagas") devolve dois grupos, provando que a função aceita tanto um nome de propriedade quanto uma função de classificação.
Uma função contarPor(lista, chave) construída sobreagruparPor, sem repetir a lógica de agrupamento.
Um bloco de testes com pelo menos seis casos, incluindo lista vazia e uma chave inexistente (que deve gerar um único grupo "undefined", e não um erro).
Um comentário de até dez linhas comparando a sua implementação com Object.groupBy da documentação da MDN: o que muda na assinatura, o que muda no tipo devolvido e qual é a situação de suporte nos navegadores.
Pistas
typeof chave === "function" distingue os dois modos de uso — a mesma técnica que bibliotecas famosas usam para aceitar parâmetros flexíveis.
O corpo é um reduce de cinco linhas: calcule a chave, crie o array se ele não existir (acumulado[valor] ??= []), dê push, devolva o acumulado.
contarPor pode ser Object.entries(agruparPor(lista, chave)).map(([k, itens]) => [k, itens.length]) — e depois Object.fromEntries monta o objeto de volta.
Object.groupBy devolve um objeto sem protótipo e aceita apenas função (nunca o nome da propriedade). Procure a tabela de compatibilidade na MDN e veja a partir de qual versão de cada navegador ele existe.
Parte 1 — Leitura (20 min). FLANAGAN, D. JavaScript: o guia definitivo, capítulos de estruturas de controle e de arrays. STEFANOV, S. Padrões JavaScript, capítulo sobre arrays e objetos. Na MDN, leia "Laços e iterações" e a página de Array (links em Para aprofundar). Anote dois métodos de array que existem na MDN e não apareceram nesta aula.
Parte 2 — Produção (30 min). Produza os exercícios B2 (contarPalavras e topPalavras) e B5 (analisarTurma) em arquivos .js comentados, com pelo menos cinco casos de teste cada, demonstrados no Console. Produza também o exercício C1: o js/dados.js do seu projeto autoral, com doze registros, e o js/relatorios.js com seis relatórios.
Critério de pronto: o dados.js do projeto autoral carrega em todas as páginas sem erro e imprime, na primeira linha do Console, a contagem de registros; os seis relatórios rodam sem nenhuma linha vermelha; nenhum relatório altera a ordem original do array de dados; nenhum dado do domínio continua digitado à mão no HTML das páginas que serão renderizadas na próxima aula.
Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: discuta quando o for clássico ainda é preferível a map/filter/reduce, com um exemplo concreto de cada situação (um em que o laço vence, um em que o método vence).
Guarde no seu repositório: commit + push (ou a pasta do projeto).
Ao fim desta aula, o repositório do seu projeto autoral deve ter:
[ ] js/dados.js com pelo menos doze registros em um array de objetos, cada um com id único e no mínimo seis propriedades.
[ ] Um objeto-dicionário que traduz códigos de categoria em nomes de exibição.
[ ] js/dados.js incluído com deferantes dos demais scripts em todas as páginas, e a contagem de registros impressa no Console ao carregar.
[ ] js/relatorios.js com pelo menos seis relatórios: console.table com colunas escolhidas, total com reduce, contagem por categoria, ranking ordenado, busca por texto parcial e junção de duas listas com find.
[ ] Pelo menos um laço aninhado usado para agrupar (por dia, por categoria ou por faixa).
[ ] Nenhuma ordenação feita diretamente sobre o array de dados (sempre [...lista].sort(…)).
[ ] Nenhum for…in sobre arrays e nenhum sort de números sem função comparadora.
[ ] Zero ocorrências de var e de == em todos os arquivos .js.
[ ] Nenhum erro vermelho no Console em nenhuma das páginas.
FLANAGAN, David. JavaScript: o guia definitivo. Bookman, 2014 — capítulos de instruções, arrays e objetos.
STEFANOV, Stoyan. Padrões JavaScript. Novatec, 2010 — capítulo 3 (literais e construtores), sobre a forma correta de criar arrays e objetos.
MILETTO, Evandro M.; BERTAGNOLLI, Silvia C. Desenvolvimento de software II. Bookman, 2014 — capítulo de estruturas de dados aplicadas à web.
Na próxima aula, o JavaScript sai do console e entra na página: você vai escrever as suas próprias funções, conhecer o DOM — a árvore de objetos que o navegador monta a partir do HTML — e reagir a cliques, teclas e envios de formulário. A lista de palestrantes que hoje só existe no dados.js vai virar cartões de verdade na tela, com filtro por área.
[ ] VS Code com a extensão Live Server funcionando e o DevTools do navegador aberto na aba Console.
[ ] O site do evento acadêmico com as cinco páginas estilizadas e responsivas (Unidade 2) e o js/app.js ligado com defer.
[ ] js/dados.js e js/relatorios.js (Aula 12) no projeto, com os arrays palestrantes e palestras e o objeto nomesDasAreas — hoje eles saem do console e vão para a tela.
[ ] Conforto com arrays e objetos: forEach, map, filter, find e a notação { chave: valor } (Aula 12).
[ ] Seletores CSS (Aula 06): #id, .classe, elemento, [atributo], descendente e :checked — querySelector usa exatamente essa sintaxe.
Na aula passada você aprendeu a controlar o fluxo do programa (condicionais e laços) e a organizar dados em arrays e objetos — mas tudo aconteceu no console. Hoje o JavaScript sai do console e entra na página: você vai escrever funções reutilizáveis, manipular a árvore do documento e reagir a cliques, teclas e envios de formulário. Na próxima aula, esse mesmo conhecimento valida o formulário de inscrição e cria a busca da programação.
Uma função é um bloco de código com nome, que recebe entradas (parâmetros), faz algo e devolve uma saída (return). Você já usou funções prontas o semestre inteiro — console.log, alert, Number, push — hoje passa a escrever as suas. O motivo é simples: sem funções, todo comportamento repetido vira código copiado e colado, e cada correção precisa ser feita em vários lugares.
Declarações sofrem hoisting (içamento): o motor JavaScript lê o arquivo inteiro antes de executar e "sobe" as declarações de função para o topo. Por isso é possível chamar calcularMedia em uma linha acima de onde ela foi escrita. Isso é útil para organizar o arquivo com as funções principais no fim e o "programa" no começo.
constcalcularMedia=(a1,a2,a3)=>(a1+a2+a3)/3;// Um parâmetro: parênteses opcionaisconstdobrar=(n)=>n*2;// Sem parâmetrosconstsaudacao=()=>"Olá!";// Corpo com várias linhas exige chaves e return explícitoconstclassificar=(nota)=>{if(nota>=6)return"Aprovado";if(nota>=4)return"Exame";return"Reprovado";};// Retornando um objeto: envolva em parênteses,// senão as chaves são lidas como corpo da funçãoconstcriarAluno=(nome,nota)=>({nome,nota});console.log(dobrar(21));// 42console.log(classificar(5.5));// "Exame"console.log(criarAluno("Ana",9.2));// { nome: "Ana", nota: 9.2 }
⚠️ Atenção
Arrow functions não têm this próprio — herdam o do escopo onde foram criadas. Isso as torna ideais como callbacks (funções passadas para forEach, map, addEventListener) e inadequadas como métodos de objeto que precisam acessar o próprio objeto por this.
Forma
Hoisting
this próprio
Quando usar
Declaração function nome() {}
Sim
Sim
Funções principais do arquivo, chamadas de vários lugares
// Valor padrão: usado quando o argumento não é passado (ou é undefined)functionsaudar(nome="visitante",saudacao="Olá"){return`${saudacao}, ${nome}!`;}saudar();// "Olá, visitante!"saudar("Ana","Bom dia");// "Bom dia, Ana!"// Rest (...) no parâmetro: número variável de argumentos vira um arrayfunctionsomar(...numeros){returnnumeros.reduce((acumulado,n)=>acumulado+n,0);}somar(1,2,3,4,5);// 15// Spread (...) na chamada: "espalha" um array como argumentos separadosconstnotas=[8,7,9];calcularMedia(...notas);// o mesmo que calcularMedia(8, 7, 9)// Objeto como parâmetro: argumentos nomeados, em qualquer ordemfunctioncadastrar({nome,email,curso="Não informado"}){return`${nome} | ${email} | ${curso}`;}cadastrar({email:"a@b.com",nome:"Ana"});// "Ana | a@b.com | Não informado"
O último padrão — objeto como parâmetro — resolve um problema real: quando a função tem quatro ou cinco parâmetros, ninguém lembra a ordem. Com um objeto, a chamada se autodocumenta.
constglobal="visível em toda parte";functionexemplo(){constlocal="só dentro desta função";if(true){letbloco="só dentro deste bloco";consttambemBloco="idem";varvazado="vaza para a função inteira";// por isso não use var}console.log(bloco);// ReferenceError: bloco is not definedconsole.log(vazado);// funciona — comportamento confuso do var}console.log(local);// ReferenceError: local is not defined
let e const têm escopo de bloco ({}). var tem escopo de função. Regra: cada variável deve existir no menor escopo possível. Se só é usada dentro do if, declare dentro do if.
Funções que recebem ou retornam outras funções. map, filter e reduce (Aula 12) são exemplos: recebem uma função e a aplicam a cada item. Você também pode criar as suas:
aplicarDesconto(10) devolve uma nova função que "lembra" o percentual. Esse "lembrar" tem nome — closure — e é o mecanismo por trás do debounce que você verá na seção 16.
Uma função, uma responsabilidade. Se o nome tem "e" (salvarEEnviar), provavelmente são duas funções.
Nomes são verbos:calcularMedia, validarEmail, formatarData, renderizarLista.
Evite mais de 3–4 parâmetros. Acima disso, receba um objeto.
Prefira retornar valores a alterar variáveis externas. Funções puras (mesma entrada → mesma saída, sem efeitos colaterais) são fáceis de testar no console.
Retorne cedo para reduzir aninhamento:
JavaScript
// Em vez de aninhar três ifsfunctionprocessar(dado){if(!dado)returnnull;if(!dado.ativo)returnnull;returndado.valor*2;}
O DOM (Document Object Model) é a representação em memória do documento, montada pelo navegador ao interpretar o HTML. Ele expõe o documento como uma árvore de objetos que o JavaScript pode ler e modificar.
document
└── html
└── body
├── h1#titulo
│ └── "Olá" (nó de texto)
└── ul.lista
├── li → "Item 1"
└── li → "Item 2"
Três verdades importantes:
O DOM não é o HTML. O HTML é o texto-fonte; o DOM é a estrutura viva em memória. Se o JavaScript adicionar elementos, eles existem no DOM sem existir no arquivo .html. Por isso "Exibir código-fonte" (Ctrl+U) mostra o arquivo original, enquanto a aba Elements do DevTools mostra o DOM atual.
Alterar o DOM altera a tela imediatamente. Não há "salvar" nem "atualizar".
Recarregar a página descarta tudo. Toda alteração feita por JavaScript some — a menos que seja persistida (você verá localStorage na próxima aula).
O objeto de entrada é document, que é filho de window (o objeto global do navegador — alert, setTimeout e location também vivem nele).
🧠 Você sabia?
O DOM nasceu de uma guerra. Em 1997, Netscape Navigator 4 e Internet Explorer 4 lançaram, cada um, seu próprio modelo de objetos para "HTML dinâmico" — document.layers de um lado, document.all do outro — e o mesmo script não rodava nos dois. Programadores escreviam tudo em dobro. O W3C publicou o DOM Level 1 em 1998 para acabar com isso, e hoje o padrão é mantido pelo WHATWG como o DOM Living Standard. O querySelector que você vai usar só chegou em 2008, com a especificação Selectors API.
// Retorna o PRIMEIRO elemento que casa com o seletor CSS (ou null)consttitulo=document.querySelector("#titulo");constprimeiroItem=document.querySelector(".lista li");constbotao=document.querySelector("button.primario");constcampo=document.querySelector("input[type='email']");// Retorna TODOS os que casam, como NodeList (mesmo que vazia)constitens=document.querySelectorAll(".lista li");constparagrafos=document.querySelectorAll("article p");
querySelector e querySelectorAll aceitam qualquer seletor CSS — combinadores, pseudoclasses, seletores de atributo. É por isso que a Aula 06 é pré-requisito desta.
document.getElementById("titulo");// sem #document.getElementsByClassName("lista");// HTMLCollection AO VIVOdocument.getElementsByTagName("li");// HTMLCollection AO VIVOdocument.getElementsByName("turno");// NodeList
Característica
querySelectorAll
getElementsBy*
Retorna
NodeList estática
HTMLCollection ao vivo
Reflete mudanças posteriores no DOM
Não
Sim
Tem forEach
Sim
Não (precisa converter)
Aceita seletor CSS
Sim
Não
⚠️ Atenção
A coleção "ao vivo" é uma fonte clássica de bugs: se você remove elementos dentro de um laço que percorre uma HTMLCollection, a coleção encolhe durante a iteração e você pula elementos. Prefira querySelectorAll.
constitens=document.querySelectorAll("li");itens.forEach((item)=>console.log(item.textContent));// Se precisar de métodos de array (map, filter, find), converta:consttextos=Array.from(itens).map((i)=>i.textContent);consttextos2=[...itens].map((i)=>i.textContent);
⚠️ AtençãoquerySelector retorna null quando não encontra. Chamar algo em null gera TypeError: Cannot read properties of null (reading 'addEventListener'). Este é o erro mais frequente de quem está aprendendo DOM. Suas duas causas quase sempre são: (a) seletor errado (#menu quando o id é menu-principal); (b) script rodando antes de o HTML existir — resolvido com defer na tag <script>.
🔬 Investigue
Abra qualquer página, vá à aba Elements do DevTools e clique em um elemento. Volte ao Console e digite $0 — o navegador devolve o elemento selecionado. Agora experimente $0.textContent, $0.classList, $0.parentElement e $0.style.outline = "3px solid red". $0 é o último elemento clicado, $1 o anterior, e assim por diante. É a forma mais rápida de testar seletores: $$(".card") equivale a document.querySelectorAll(".card").
consttitulo=document.querySelector("#titulo");// LEITURAtitulo.textContent;// texto puro, incluindo o de elementos ocultostitulo.innerText;// texto como renderizado (respeita CSS, mais lento)titulo.innerHTML;// conteúdo HTML como string// ESCRITAtitulo.textContent="Novo título";titulo.innerHTML="Novo <strong>título</strong>";
Propriedade
Interpreta HTML?
Uso
textContent
Não — tags viram texto literal
Padrão. Use sempre que possível
innerText
Não
Quando importa o texto visível (respeita display: none)
innerHTML
Sim
Só quando você realmente precisa inserir marcação
⚠️ Atenção — XSS
Nunca faça elemento.innerHTML = dadoDoUsuario. Se o usuário digitar <img src=x onerror="alert(document.cookie)"> em um campo e você inserir isso com innerHTML, o código dele executa na sua página — para todo mundo que abrir. Isso se chama Cross-Site Scripting (XSS) e é uma das vulnerabilidades mais exploradas da Web.
Regra: dado vindo de usuário → textContent. Marcação que você mesmo escreveu → innerHTML é aceitável.
constlink=document.querySelector("a");link.getAttribute("href");link.setAttribute("href","https://www.wikipedia.org");link.setAttribute("target","_blank");link.hasAttribute("download");// true/falselink.removeAttribute("target");// Propriedades diretas para atributos comunslink.href;link.id;img.src;img.alt;input.value;// valor atual do campoinput.checked;// true/false para checkbox/radioinput.disabled=true;select.value;
💡 Dicainput.value é diferente de input.getAttribute("value"). O primeiro é o valor atual (o que o usuário digitou); o segundo é o valor inicial escrito no HTML. Ao trabalhar com formulários, use sempre .value.
constbtn=document.querySelector("button");btn.dataset.id;// "42" (sempre string!)btn.dataset.acao;// "excluir"btn.dataset.nomeProduto;// "Notebook" — data-nome-produto vira nomeProduto (camelCase)btn.dataset.status="ativo";// cria data-status="ativo" no HTML
Atributos data-* são a forma padrão de guardar informação de aplicação no HTML. Serão essenciais na delegação de eventos (seção 14) e no CRUD do Boss da Unidade: cada botão de editar/excluir carrega o data-id do registro.
constcard=document.querySelector(".card");card.classList.add("ativo");card.classList.add("destaque","novo");// várias de uma vezcard.classList.remove("ativo");card.classList.toggle("aberto");// adiciona se não tem, remove se temcard.classList.toggle("aberto",condicao);// força adicionar (true) ou remover (false)card.classList.contains("ativo");// true/falsecard.classList.replace("antigo","novo");
⚠️ Atenção
Evite card.className = "novo" — isso substitui todas as classes existentes de uma vez.
card.style.backgroundColor="#0b3d5c";// camelCase, não background-colorcard.style.fontSize="18px";// sempre com unidadecard.style.display="none";card.style.setProperty("--cor-tema","#e74c3c");// variáveis CSS// Leitura do estilo COMPUTADO (o que realmente está aplicado, vindo de qualquer folha)constestilo=getComputedStyle(card);estilo.backgroundColor;estilo.width;
💡 Dica
Prefira trocar classes a manipular style diretamente. O CSS fica no CSS, o comportamento fica no JS, e você mantém a separação de responsabilidades que construiu na Unidade 2.
JavaScript
// Espalha estilo pelo JavaScriptel.style.display="none";el.style.opacity="0";// O CSS define o que é "oculto"; o JS só decide quandoel.classList.add("oculto");
// 1. Criarconstli=document.createElement("li");// 2. Configurarli.textContent="Novo item";li.classList.add("item");li.dataset.id="7";// 3. Inserirconstlista=document.querySelector("ul");lista.appendChild(li);// no fimlista.append(li,outroLi);// no fim, aceita vários nós e textolista.prepend(li);// no iníciolista.insertBefore(li,referencia);// antes de um filho específico// Inserção posicional em relação a um elementoelemento.insertAdjacentElement("beforebegin",novo);// antes do elementoelemento.insertAdjacentElement("afterbegin",novo);// primeiro filhoelemento.insertAdjacentElement("beforeend",novo);// último filhoelemento.insertAdjacentElement("afterend",novo);// depois do elementoelemento.insertAdjacentHTML("beforeend","<li>Item</li>");// com string HTML
elemento.remove();// remove a si mesmopai.removeChild(filho);// forma antigapai.replaceChild(novo,antigo);elemento.replaceWith(novo);lista.innerHTML="";// esvazia (rápido e comum)
constitem=document.querySelector(".item");// Para cimaitem.parentElement;item.closest(".card");// ancestral mais próximo que casa com o seletor (inclui o próprio)// Para baixoitem.children;// HTMLCollection de elementos filhositem.firstElementChild;item.lastElementChild;item.querySelector(".titulo");// busca DENTRO do item// Para os ladositem.nextElementSibling;item.previousElementSibling;
⚠️ Atenção
Existem versões sem Element no nome (parentNode, firstChild, nextSibling) que consideram nós de texto — inclusive espaços e quebras de linha do seu HTML. Isso surpreende: ul.firstChild costuma ser um nó de texto vazio, não o primeiro <li>. Use sempre as versões com Element.
closest() é indispensável: em uma lista, ao clicar num botão dentro de um card, btn.closest(".card") devolve o card inteiro — mesmo que o clique tenha sido em um ícone dentro do botão.
Este é o padrão central do desenvolvimento front-end e será usado em todas as aulas seguintes: os dados vivem em um array; uma função desenha a tela a partir dele. Quando os dados mudam, chama-se a função de novo.
JavaScript
// js/produtos.jsconstprodutos=[{id:1,nome:"Notebook",preco:3500,categoria:"Informática"},{id:2,nome:"Mouse",preco:80,categoria:"Periféricos"},{id:3,nome:"Teclado",preco:150,categoria:"Periféricos"},];constcontainer=document.querySelector("#lista-produtos");functionrenderizar(lista){container.innerHTML="";// 1. limpaif(lista.length===0){// 2. estado vazioconstaviso=document.createElement("p");aviso.classList.add("vazio");aviso.textContent="Nenhum produto encontrado.";container.appendChild(aviso);return;}lista.forEach((produto)=>{// 3. cria um nó por itemconstcard=document.createElement("article");card.classList.add("card-produto");card.dataset.id=produto.id;consttitulo=document.createElement("h3");titulo.textContent=produto.nome;// textContent: seguro contra XSSconstpreco=document.createElement("p");preco.classList.add("preco");preco.textContent=produto.preco.toLocaleString("pt-BR",{style:"currency",currency:"BRL",});constbtn=document.createElement("button");btn.type="button";btn.textContent="Excluir";btn.dataset.id=produto.id;btn.classList.add("btn-excluir");card.append(titulo,preco,btn);container.appendChild(card);});}renderizar(produtos);
Se p.nome veio de um campo digitado por alguém, essa versão é vulnerável. Uma saída intermediária: montar a marcação fixa com template literal e preencher os textos variáveis com textContent depois.
Cada appendChild direto no documento pode disparar um reflow (recálculo de layout). Para inserir muitos elementos, monte fora do documento e insira uma vez só:
JavaScript
constfragmento=document.createDocumentFragment();lista.forEach((item)=>{constli=document.createElement("li");li.textContent=item.nome;fragmento.appendChild(li);// ainda fora do documento});container.appendChild(fragmento);// uma única operação no DOM
🔎 Por baixo do capô
O navegador não redesenha a tela a cada linha de JavaScript — ele acumula mudanças e recalcula o layout quando precisa (ou quando você lê uma medida, como offsetHeight, forçando o cálculo). Mesmo assim, inserir 500 nós um a um é mais caro que inserir um fragmento com 500 nós. Para listas de até algumas dezenas de itens, a diferença é imperceptível; para centenas, ela aparece no celular.
Um evento é um sinal de que algo aconteceu: um clique, uma tecla, o envio de um formulário, o fim do carregamento. O JavaScript no navegador é orientado a eventos: você registra funções (ouvintes ou listeners) que serão chamadas quando o evento ocorrer. Entre um evento e outro, seu código não faz nada — o navegador fica esperando.
constbotao=document.querySelector("#meu-botao");botao.addEventListener("click",function(evento){console.log("Clicado!",evento.target);});// Com arrow functionbotao.addEventListener("click",()=>{document.querySelector("#saida").textContent="Você clicou!";});// Garantir que o DOM está pronto (desnecessário se o script usa defer)document.addEventListener("DOMContentLoaded",()=>{console.log("DOM montado");});
<!-- 1. Atributo HTML — evite: mistura comportamento com estrutura --><buttononclick="alert('Oi')">Clique</button>
JavaScript
// 2. Propriedade do elemento — permite apenas UM ouvinte por eventobotao.onclick=function(){console.log("A");};botao.onclick=function(){console.log("B");};// sobrescreve o anterior!// 3. addEventListener — a forma corretabotao.addEventListener("click",function(){console.log("A");});botao.addEventListener("click",function(){console.log("B");});// Ambos executam, na ordem de registro
🧠 Você sabia?addEventListener só passou a funcionar em todos os navegadores em 2009, com o Internet Explorer 9. Até lá, o IE usava um método próprio, attachEvent, com outra assinatura e outro comportamento de this. Uma das razões de a biblioteca jQuery ter dominado a Web por uma década foi justamente esconder essa diferença atrás de um .on("click", fn) único. Hoje o padrão venceu e o jQuery é desnecessário — mas você ainda encontrará onclick e attachEvent em código legado.
elemento.addEventListener(tipo,funcao,opcoes);// Exemplosbotao.addEventListener("click",tratarClique);botao.addEventListener("click",()=>console.log("Oi"));botao.addEventListener("click",tratarClique,{once:true,// executa apenas uma vez e se removecapture:false,// fase de captura em vez de borbulhamento (seção 13)passive:true,// promete não chamar preventDefault (melhora a rolagem no celular)});// Remoção — exige a MESMA referência de funçãofunctiontratarClique(){console.log("Oi");}botao.addEventListener("click",tratarClique);botao.removeEventListener("click",tratarClique);// funcionabotao.addEventListener("click",()=>{});botao.removeEventListener("click",()=>{});// não remove nada
⚠️ Atenção
Funções anônimas não podem ser removidas, porque cada arrow function escrita é um objeto novo — a segunda () => {} não é a mesma que a primeira. Se precisar remover depois, nomeie a função.
⚠️ Atenção — o erro mais comum de todosbotao.addEventListener("click", tratarClique()) — com parênteses — chama a função imediatamente, no momento do registro, e passa o retorno dela (geralmente undefined) como ouvinte. O clique depois não faz nada. Passe a referência: tratarClique, sem parênteses. Se precisar passar argumentos, envolva em uma arrow: () => tratarClique(42).
campo.addEventListener("keydown",(e)=>{console.log(e.key);// "a", "Enter", "Escape", "ArrowUp", " "console.log(e.code);// "KeyA", "Enter", "Space" — posição física da teclaconsole.log(e.ctrlKey,e.shiftKey,e.altKey);if(e.key==="Enter")enviar();if(e.key==="Escape")fecharModal();if(e.ctrlKey&&e.key==="s"){e.preventDefault();// impede o "Salvar página" do navegadorsalvar();}});
💡 Dica
Use e.key para saber qual caractere; use e.code para saber qual tecla física (importante em jogos e em teclados de layouts diferentes — no teclado ABNT2, e.key da tecla ao lado do Enter é "ç", mas e.code é "Semicolon").
touchstart, touchmove, touchend. Em geral, click já funciona em telas de toque — só use eventos de toque para gestos específicos (arrastar, pinçar).
🔬 Investigue
No Console, digite monitorEvents(document.body, "click") e clique em qualquer lugar da página: cada clique é impresso com o objeto do evento. Troque para monitorEvents(document.body, ["keydown", "keyup"]) e digite algo. Quando cansar, unmonitorEvents(document.body). Depois selecione um botão na aba Elements e abra a sub-aba Event Listeners no painel lateral: ela lista todos os ouvintes registrados naquele elemento e nos ancestrais — com o arquivo e a linha onde cada um foi escrito. É assim que você descobre "quem está reagindo a esse clique" em um site que não escreveu.
Toda função ouvinte recebe automaticamente um objeto com informações sobre o que ocorreu.
JavaScript
elemento.addEventListener("click",function(event){event.type;// "click"event.target;// elemento que ORIGINOU o eventoevent.currentTarget;// elemento onde o OUVINTE está registradoevent.timeStamp;// milissegundos desde o carregamento da páginaevent.clientX,event.clientY;// coordenadas na janelaevent.pageX,event.pageY;// coordenadas na página (com rolagem)event.preventDefault();// cancela o comportamento padrãoevent.stopPropagation();// interrompe a propagação});
⚠️ Atenção — target × currentTarget
Se você clica em um <span> dentro de um <button> que tem o ouvinte, target é o <span> e currentTarget é o <button>. Confundir os dois é a causa mais comum de bugs em delegação de eventos.
// Impede o envio e o recarregamento da páginaformulario.addEventListener("submit",(e)=>{e.preventDefault();processarDados();});// Impede a navegação de um linklink.addEventListener("click",(e)=>{e.preventDefault();abrirModal();});// Bloqueia caracteres não numéricos (teclas de um só caractere)campo.addEventListener("keydown",(e)=>{if(!/[0-9]/.test(e.key)&&e.key.length===1)e.preventDefault();});
Sem e.preventDefault() no submit, a página recarrega e todo o seu JavaScript é reiniciado — os dados somem e parece que "o código não funcionou". É o erro nº 1 da próxima aula.
Borbulhamento (bubbling): do alvo de volta até document. É a fase padrão — onde os ouvintes ficam, a menos que você peça o contrário.
JavaScript
document.querySelector("ul").addEventListener("click",()=>console.log("UL"));document.querySelector("li").addEventListener("click",()=>console.log("LI"));// Clicar no LI imprime: "LI", depois "UL"// Para ouvir na captura:ul.addEventListener("click",tratar,true);// ou { capture: true } — agora imprime "UL", depois "LI"
JavaScript
event.stopPropagation();// impede que continue subindoevent.stopImmediatePropagation();// também impede outros ouvintes no MESMO elemento
⚠️ Atenção
Use stopPropagation com parcimônia. Ele quebra a delegação e cria comportamentos difíceis de rastrear meses depois ("por que o menu não fecha quando clico aqui?"). Antes de usá-lo, pergunte se não é melhor verificar event.target.
O problema: você adiciona ouvintes a 50 botões de "excluir". Depois, o JavaScript cria mais 10 botões dinamicamente. Os novos não têm ouvinte — porque addEventListener só afeta elementos que existiam no momento do registro.
A solução: registre um único ouvinte no contêiner pai e descubra, pelo event.target, quem foi realmente clicado. Isso funciona porque os eventos borbulham.
JavaScript
constlista=document.querySelector("#lista-produtos");lista.addEventListener("click",(event)=>{// Encontra o botão, mesmo se o clique foi num ícone dentro deleconstbotaoExcluir=event.target.closest(".btn-excluir");if(!botaoExcluir)return;// clique fora dos botões: ignoraconstid=Number(botaoExcluir.dataset.id);excluirProduto(id);});
Vantagens:
Funciona para elementos criados depois — sem re-registrar nada.
Um ouvinte em vez de N — menos memória, melhor desempenho.
Código mais simples — a lógica fica concentrada em um lugar.
Útil para desacoplar partes da aplicação: um módulo avisa que algo aconteceu sem conhecer quem vai reagir. No site do evento, por exemplo, o formulário de inscrição pode disparar inscricaoConfirmada e o contador de vagas, em outro arquivo, apenas escuta.
Eventos como input, scroll, mousemove e resize disparam dezenas de vezes por segundo. Executar uma operação pesada (filtrar 500 itens, redesenhar uma lista) em cada disparo trava a interface.
JavaScript
// DEBOUNCE — só executa depois que os disparos PARAM por X msfunctiondebounce(fn,atraso=300){lettimer;returnfunction(...args){clearTimeout(timer);timer=setTimeout(()=>fn.apply(this,args),atraso);};}constbuscar=debounce((texto)=>{console.log("Buscando por:",texto);},400);campoBusca.addEventListener("input",(e)=>buscar(e.target.value));
JavaScript
// THROTTLE — executa no máximo uma vez a cada X msfunctionthrottle(fn,intervalo=200){letpodeExecutar=true;returnfunction(...args){if(!podeExecutar)return;podeExecutar=false;fn.apply(this,args);setTimeout(()=>{podeExecutar=true;},intervalo);};}window.addEventListener("scroll",throttle(()=>{console.log("Rolando");},200));
Aspecto
Debounce
Throttle
Quando executa
Após a pausa nos disparos
Em intervalos regulares
Uso típico
Busca ao digitar, validação, redimensionamento
Rolagem, movimento do mouse, animações
🔎 Por baixo do capôdebounce só funciona por causa das closures da seção 1: a variável timer fica "presa" dentro da função retornada e sobrevive entre uma chamada e outra. Cada tecla cancela o setTimeout anterior e agenda outro; só o último sobrevive 300 ms e executa fn.
// Só funciona com mousediv.addEventListener("click",acao);// Use um <button> de verdade: já é focável, acionável por Enter/Espaço// e anunciado como botão pelo leitor de telabotao.addEventListener("click",acao);
Se por algum motivo precisar tornar um elemento genérico interativo:
Regra: se algo é clicável, use <button>. Se navega para outro lugar, use <a href>. Reinventar controles com <div> é a origem da maioria dos problemas de acessibilidade em sites modernos — e o Lighthouse (Aula 15) penaliza isso.
📌 Vale gravar
Vale saber de cor: (1) a diferença entre target e currentTarget; (2) por que addEventListener("click", fn()) não funciona; (3) o que acontece sem preventDefault no submit; (4) por que delegação resolve elementos criados depois; (5) a ordem de impressão em um exemplo de borbulhamento.
18. A arquitetura: estado → dados → renderização → eventos¶
Antes de ir para o projeto, um esqueleto que vale para qualquer aplicação front-end — do menu de hoje ao CRUD do fim da unidade:
JavaScript
// Esqueleto genérico de organização — não é o js/app.js do projeto.// Os nomes abaixo (els, renderizar, tarefas) são de exemplo; no seu código,// use nomes específicos, porque scripts sem type="module" dividem o mesmo// escopo global e nomes genéricos colidem entre arquivos.// ===== ESTADO: a fonte única da verdade =====lettarefas=[];letfiltroAtual="todas";// ===== SELEÇÃO DE ELEMENTOS (uma vez só) =====constels={form:document.querySelector("#form-tarefa"),input:document.querySelector("#input-tarefa"),lista:document.querySelector("#lista-tarefas"),};// ===== FUNÇÕES DE DADOS (puras: alteram só o estado) =====functionadicionarTarefa(texto){tarefas.push({id:Date.now(),texto,concluida:false});}functionalternarConclusao(id){consttarefa=tarefas.find((t)=>t.id===id);if(tarefa)tarefa.concluida=!tarefa.concluida;}functionobterTarefasVisiveis(){if(filtroAtual==="pendentes")returntarefas.filter((t)=>!t.concluida);if(filtroAtual==="concluidas")returntarefas.filter((t)=>t.concluida);return[...tarefas];}// ===== RENDERIZAÇÃO (desenha a tela a partir do estado) =====functionrenderizar(){els.lista.innerHTML="";obterTarefasVisiveis().forEach((t)=>{constli=document.createElement("li");li.dataset.id=t.id;li.classList.toggle("concluida",t.concluida);li.textContent=t.texto;els.lista.appendChild(li);});}// ===== EVENTOS (capturam a intenção do usuário) =====functionregistrarEventos(){els.form.addEventListener("submit",(e)=>{e.preventDefault();consttexto=els.input.value.trim();if(texto==="")return;adicionarTarefa(texto);els.input.value="";renderizar();});els.lista.addEventListener("click",(e)=>{constli=e.target.closest("li[data-id]");if(!li)return;alternarConclusao(Number(li.dataset.id));renderizar();});}// ===== INICIALIZAÇÃO =====functioniniciar(){registrarEventos();renderizar();}iniciar();
O fluxo é sempre o mesmo: usuário age → evento → função altera o estado → renderizar() redesenha. Nunca altere o DOM "na mão" para refletir uma mudança de dado: se o usuário conclui uma tarefa, não risque o <li> — mude o array e renderize de novo. Quando DOM e dados divergem, os bugs se tornam impossíveis de rastrear. Frameworks como Vue e React (Nível 3) apenas automatizam essa mesma ideia.
💻 Mão na massa — O menu que você já tinha, agora entendido, e a lista de palestrantes¶
O site do evento já tem um menu hambúrguer que abre e fecha desde a Aula 08, com oito linhas de JavaScript que você colou "sem entender", e uma página de palestrantes escrita à mão, artigo por artigo. Hoje o js/menu.js cresce com o que você aprendeu (fechar com Esc, fechar ao clicar fora, reagir ao resize), o js/app.js ganha uma seção nova sem perder nada do que já fazia, e a página de palestrantes passa a ser renderizada a partir do js/dados.js da Aula 12, com filtro por área usando delegação.
⚠️ Atenção
Três coisas que não acontecem hoje: (1) a marcação do cabeçalho não muda — o <button class="menu-botao"> com o <span class="menu-botao__icone"> e a <ul id="a13-menu-principal" class="menu"> são os da Aula 08, com o .menu__cta de "Inscreva-se" no fim; (2) o CSS do menu não muda — a abertura continua sendo a animação de opacity + visibility + transform da Aula 09, sem display: none; (3) nenhum arquivo é apagado nem reescrito do zero. menu.js, efeitos.js, app.js, dados.js e relatorios.js continuam existindo, cada um com a sua responsabilidade.
// Seleciona o botão e a lista pelo seletor CSSconstbotao=document.querySelector('.menu-botao');constmenu=document.querySelector('#menu-principal');// A cada clique, inverte o valor de aria-expandedbotao.addEventListener('click',()=>{constaberto=botao.getAttribute('aria-expanded')==='true';botao.setAttribute('aria-expanded',String(!aberto));});
Agora você consegue ler cada linha: document.querySelector é a seleção da seção 3; addEventListener('click', …) é o registro de tratador da seção 10; a arrow function é a forma da seção 1. E consegue ver o que falta — as três melhorias que ficaram como exercício B3 da Aula 08.
Substitua o conteúdo do arquivo por esta versão. Os nomes botao e menu continuam os mesmos: eles pertencem a este arquivo e a mais nenhum. Se você declarar um const menu também no app.js, o navegador para tudo com Identifier 'menu' has already been declared — dois <script> sem type="module" compartilham o mesmo escopo global.
site-evento/js/menu.js (versão desta aula)
JavaScript
// menu.js — comportamento do menu hambúrguer (marcação e CSS: Aulas 08 e 09)// ===== ELEMENTOS =====constbotao=document.querySelector(".menu-botao");constmenu=document.querySelector("#menu-principal");constLARGURA_DESKTOP=768;// o mesmo ponto de quebra do CSS da Aula 08// ===== FUNÇÕES DE ESTADO =====// O estado mora no atributo aria-expanded do botão: uma fonte só, que o CSS// também lê (.menu-botao[aria-expanded="true"] + .menu). Nada de classe .aberto.functionmenuEstaAberto(){returnbotao.getAttribute("aria-expanded")==="true";}functiondefinirMenu(aberto){botao.setAttribute("aria-expanded",String(aberto));}functionfecharMenu(){definirMenu(false);}functionalternarMenu(){definirMenu(!menuEstaAberto());}// ===== EVENTOS =====functionregistrarEventosDoMenu(){// 1. Clique no botão: abre ou fechabotao.addEventListener("click",alternarMenu);// 2. Esc fecha e devolve o foco ao botão (seção 17: nunca perca o foco)document.addEventListener("keydown",(e)=>{if(e.key==="Escape"&&menuEstaAberto()){fecharMenu();botao.focus();}});// 3. Clique fora do menu e do botão: fecha (um ouvinte só, no document)document.addEventListener("click",(e)=>{constclicouDentro=menu.contains(e.target)||botao.contains(e.target);if(!clicouDentro&&menuEstaAberto()){fecharMenu();}});// 4. Ao alargar a janela para desktop, garante o estado fechadowindow.addEventListener("resize",()=>{if(window.innerWidth>=LARGURA_DESKTOP&&menuEstaAberto()){fecharMenu();}});}// ===== INICIALIZAÇÃO =====// Se uma página não tiver cabeçalho (a 404.html da Aula 15, por exemplo),// querySelector devolve null e o script não deve quebrar.if(botao&&menu){registrarEventosDoMenu();}
Três observações:
O estado continua no aria-expanded. Não existe classe .aberto neste projeto: o CSS das Aulas 08 e 09 já reage ao atributo, e o atributo é o que o leitor de tela anuncia. Um estado, um lugar.
O clique fora usa o document — é delegação (seção 14) aplicada à página inteira. Repare que ele funciona porque o clique no botão também chega ao document na fase de bolha; por isso a verificação botao.contains(e.target), senão o menu abriria e fecharia no mesmo clique.
resize dispara dezenas de vezes por segundo ao arrastar a janela. Aqui o trabalho é baratíssimo (uma comparação e, no máximo, um setAttribute), então não vale a pena um throttle. Na Aula 14 você vai encontrar um caso em que vale.
Passo 2 — Acrescentar ao js/app.js (sem apagar nada)¶
O js/app.js já tem, das Aulas 10 e 11: as constantes do evento, a saudação no console, o console.group, o console.table das cinco páginas e a contagem regressiva do index.html. Nada disso sai. Você só organiza o arquivo em blocos e acrescenta uma seção no fim.
Comece pondo os comentários de seção sobre o que já existe, na ordem da seção 18 desta aula:
site-evento/js/app.js (topo do arquivo, comentários acrescentados)
JavaScript
// app.js — comportamento comum a todas as páginas do site do evento// ===== ESTADO =====constNOME_EVENTO="Semana Acadêmica de Sistemas de Informação";constEDICAO=12;constLOCAL_EVENTO="Auditório Central";constTRILHAS="Desenvolvimento Web, Dados, Segurança";
Deixe a saudação, o console.group, o console.table e todo o bloco ===== CONTAGEM REGRESSIVA PARA A ABERTURA ===== exatamente onde estão. Depois, no fim do arquivo, acrescente:
site-evento/js/app.js (acrescente ao fim)
JavaScript
// ===== NAVEGAÇÃO: marcar a página atual =====/** * Marca com aria-current="page" o link do menu que aponta para a página aberta. * @param {string} [padrao="index.html"] - arquivo assumido quando a URL termina em "/" */functionmarcarPaginaAtual(padrao="index.html"){constlistaDoMenu=document.querySelector("#menu-principal");if(listaDoMenu===null)return;constarquivoAtual=location.pathname.split("/").pop()||padrao;listaDoMenu.querySelectorAll("a").forEach((link)=>{constdestino=link.getAttribute("href");if(destino===arquivoAtual){link.setAttribute("aria-current","page");}else{link.removeAttribute("aria-current");}});}marcarPaginaAtual();
⚠️ Atenção
Isto é uma troca declarada, não um acréscimo: até a Aula 08 o aria-current="page" era escrito à mão no HTML de cada página. Agora ele passa a ser aplicado pelo JavaScript, então apague o atributo das cinco páginas — se você deixar os dois, um link renomeado vai ficar marcado em dois lugares e a função vai remover o atributo que o HTML afirmava. Uma informação, uma fonte.
O preço dessa troca é real e você precisa conhecê-lo: sem JavaScript, o destaque da página atual some. Ele é aceitável aqui porque aria-current é reforço — o menu continua navegável, os links continuam funcionando e o <title> continua dizendo onde a pessoa está. Se o atributo fosse a única forma de saber a página atual, o certo seria mantê-lo no HTML.
Passo 3 — A página de palestrantes renderizada a partir do dados.js¶
O js/dados.js da Aula 12 já tem o array palestrantes (seis pessoas, com id, nome, instituicao, area, tema e foto) e o dicionário nomesDasAreas. O js/palestrantes.js de hoje consome os dois: ele não redeclara nem copia nada. Se você redeclarar aqui o array com const, as duas declarações colidem no escopo global e o navegador derruba o script inteiro com Uncaught SyntaxError: Identifier 'palestrantes' has already been declared.
Substitua o <main> de palestrantes.html pelo contêiner vazio, os botões de filtro e o contador — os seis <article> escritos à mão nas Aulas 02 e 04 saem daqui, porque a partir de agora quem os escreve é o JavaScript:
palestrantes.html (trecho: <head> e <main>)
HTML
<head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metaname="description"content="Conheça os palestrantes da Semana Acadêmica de Sistemas de Informação."><metaname="author"content="Semana Acadêmica de Sistemas de Informação"><title>Palestrantes — Semana Acadêmica de Sistemas de Informação</title><linkrel="stylesheet"href="css/estilo.css"><scriptsrc="js/menu.js"defer></script><scriptsrc="js/dados.js"defer></script><scriptsrc="js/app.js"defer></script><scriptsrc="js/palestrantes.js"defer></script></head>
HTML
<mainid="conteudo"tabindex="-1"class="container"><h1>Palestrantes</h1><p>Conheça quem vai compartilhar conhecimento nesta edição.</p><divid="filtros-area"class="filtros"role="group"aria-label="Filtrar por área"><buttontype="button"class="ativo"data-area="todas"aria-pressed="true">Todas</button><buttontype="button"data-area="web"aria-pressed="false">Desenvolvimento Web</button><buttontype="button"data-area="dados"aria-pressed="false">Ciência de Dados</button><buttontype="button"data-area="seguranca"aria-pressed="false">Segurança</button><buttontype="button"data-area="ia"aria-pressed="false">Inteligência Artificial</button></div><pid="contador-palestrantes"class="contador"role="status"></p><ulid="lista-palestrantes"class="cartoes"></ul></main>
Os quatro data-area são exatamente os códigos de nomesDasAreas (web, dados, seguranca, ia) — os mesmos que a Aula 12 usou nos relatórios do console. Repare também na ordem dos <script> no <head>: dados.jsantes de palestrantes.js, senão o array ainda não existe quando o segundo arquivo roda. Com defer, a ordem de execução é a ordem em que os <script> aparecem.
site-evento/js/palestrantes.js
JavaScript
// palestrantes.js — a lista de palestrantes renderizada a partir de js/dados.js.// Depende de js/dados.js (arrays `palestrantes` e objeto `nomesDasAreas`),// que precisa ser carregado antes. Nada é redeclarado aqui.// ===== ESTADO =====letareaAtual="todas";// ===== SELEÇÃO DE ELEMENTOS (uma vez só) =====constelementosPalestrantes={lista:document.querySelector("#lista-palestrantes"),filtros:document.querySelector("#filtros-area"),contador:document.querySelector("#contador-palestrantes"),};// ===== FUNÇÕES DE DADOS =====functionobterPalestrantesVisiveis(){if(areaAtual==="todas"){return[...palestrantes];}returnpalestrantes.filter((pessoa)=>pessoa.area===areaAtual);}// ===== RENDERIZAÇÃO =====/** * Monta o cartão de um palestrante. * @param {Object} pessoa - um item do array `palestrantes` de dados.js * @returns {HTMLLIElement} o <li> pronto para entrar na lista */functioncriarCartaoDePalestrante(pessoa){constitem=document.createElement("li");item.classList.add("cartao","cartao--palestrante");item.dataset.id=pessoa.id;constfoto=document.createElement("img");foto.src=pessoa.foto;foto.alt=`Foto de ${pessoa.nome}`;foto.width=240;foto.height=240;foto.loading="lazy";constnome=document.createElement("h2");nome.textContent=pessoa.nome;// textContent: nunca innerHTML para dadosconstinstituicao=document.createElement("p");instituicao.classList.add("cartao__meta");instituicao.textContent=pessoa.instituicao;constetiqueta=document.createElement("span");etiqueta.classList.add("etiqueta");etiqueta.textContent=nomesDasAreas[pessoa.area];consttema=document.createElement("p");tema.textContent=pessoa.tema;item.append(foto,nome,instituicao,etiqueta,tema);returnitem;}functionrenderizarPalestrantes(){constvisiveis=obterPalestrantesVisiveis();elementosPalestrantes.lista.innerHTML="";if(visiveis.length===0){constaviso=document.createElement("li");aviso.classList.add("vazio");aviso.textContent="Nenhum palestrante nesta área ainda. Volte em breve!";elementosPalestrantes.lista.appendChild(aviso);}else{constfragmento=document.createDocumentFragment();visiveis.forEach((pessoa)=>fragmento.appendChild(criarCartaoDePalestrante(pessoa)));elementosPalestrantes.lista.appendChild(fragmento);}consttotal=visiveis.length;elementosPalestrantes.contador.textContent=total===1?"1 palestrante":`${total} palestrantes`;}functionatualizarBotoesDeFiltro(){elementosPalestrantes.filtros.querySelectorAll("button[data-area]").forEach((botaoFiltro)=>{constativo=botaoFiltro.dataset.area===areaAtual;botaoFiltro.classList.toggle("ativo",ativo);botaoFiltro.setAttribute("aria-pressed",String(ativo));});}// ===== EVENTOS =====functionregistrarEventosDePalestrantes(){// Um único ouvinte para todos os botões de filtro (delegação, seção 14)elementosPalestrantes.filtros.addEventListener("click",(e)=>{constbotaoFiltro=e.target.closest("button[data-area]");if(!botaoFiltro)return;areaAtual=botaoFiltro.dataset.area;atualizarBotoesDeFiltro();renderizarPalestrantes();});}// ===== INICIALIZAÇÃO =====functioniniciarPalestrantes(){registrarEventosDePalestrantes();renderizarPalestrantes();}iniciarPalestrantes();
Duas escolhas de nome merecem explicação. Primeiro, elementosPalestrantes em vez de um genérico els: como todos os scripts sem type="module" dividem o mesmo escopo global, um nome curto e óbvio é justamente o que vai colidir com o de outro arquivo — na Aula 14 nasce um js/programacao.js que precisaria do mesmo els. Segundo, renderizarPalestrantes em vez de renderizar: pelo mesmo motivo. Nome específico é seguro; nome genérico é uma bomba-relógio.
Os cartões não precisam de CSS novo: <li class="cartao cartao--palestrante"> dentro de <ul class="cartoes"> reaproveita a grade repeat(auto-fit, minmax(280px, 1fr)) da Aula 07 e a foto redonda da Aula 08. Só os filtros, o contador, a etiqueta e o estado vazio são novos.
css/estilo.css (seção 5 — componentes)
CSS
/* ===== Filtros de área (Aula 13) ===== */.filtros{display:flex;flex-wrap:wrap;gap:var(--espaco-pequeno);margin-block:var(--espaco-medio);}.filtrosbutton{padding:var(--espaco-pequeno)var(--espaco-medio);border:2pxsolidvar(--cor-primaria);border-radius:999px;background:transparent;color:var(--cor-primaria);font:inherit;cursor:pointer;}.filtrosbutton.ativo{background:var(--cor-primaria);color:var(--cor-sobre-primaria);}.contador{color:var(--cor-texto);font-size:.875rem;}.etiqueta{justify-self:center;padding:2px10px;border-radius:999px;background:var(--cor-fundo);border:1pxsolidvar(--cor-borda);font-size:.8rem;}.vazio{grid-column:1/-1;padding:var(--espaco-grande);text-align:center;}
Nenhuma cor escrita à mão: todas saem do sistema de design da Aula 06 mais as variáveis que as Aulas 07 e 08 acrescentaram (--cor-superficie, --cor-borda, --cor-sobre-primaria). O :focus-visible global da Aula 06 já cuida do foco dos botões — não é preciso repeti-lo aqui.
Abra index.html com o Live Server e estreite a janela para menos de 768 px (ou use o modo dispositivo do DevTools, Ctrl+Shift+M). O botão "Menu" aparece; a lista, não.
Clique no botão: a lista desliza e aparece com a animação da Aula 09 e, na aba Elements, aria-expanded muda para "true". O ícone gira 90°.
Pressione Esc: o menu fecha e o foco volta ao botão (o anel de foco aparece nele). Abra de novo e clique fora do menu: fecha.
Alargue a janela para além de 768 px: o menu fecha sozinho e o botão desaparece.
Ainda no index.html, confira que a contagem regressiva continua na tela e que o Console ainda mostra a saudação, o grupo "Dados do evento" e a tabela das cinco páginas — se algo sumiu, o app.js foi sobrescrito em vez de ampliado.
Em qualquer página, na aba Elements, o link do menu correspondente à página aberta tem aria-current="page"; os outros quatro, não. Navegue para outra página e confira que o atributo mudou de lugar.
Vá a palestrantes.html: os seis cartões aparecem gerados pelo JavaScript (em Elements o <ul> está cheio; em "Exibir código-fonte", vazio). O contador mostra "6 palestrantes" e as fotos são as img/palestrante-01.jpg a img/palestrante-06.jpg.
Clique em "Segurança": só a Carla Mendes aparece e o contador diz "1 palestrante". Clique em "Desenvolvimento Web": aparecem Diego Nascimento e Eduarda Ribeiro. Clique em "Todas": tudo volta. Navegue pelos filtros com Tab e acione com Enter — funciona igual.
Console limpo nas cinco páginas: nenhuma linha vermelha e, em especial, nenhum Identifier 'palestrantes' has already been declared nem Identifier 'menu' has already been declared.
A1. Escreva a mesma função nas três formas (declaração, expressão e arrow): recebe o raio e retorna a área do círculo (Math.PI * raio ** 2).
A2. O que faz o operador rest (...numeros) em um parâmetro de função? E o spread em uma chamada? Dê um exemplo de cada.
A3. O que é o DOM? Ele é o mesmo que o arquivo HTML? Justifique com o que você vê em "Exibir código-fonte" versus a aba Elements.
A4. Qual a diferença entre querySelector e querySelectorAll? O que cada um retorna quando não encontra nada?
A5. Qual a diferença entre textContent, innerText e innerHTML?
A6. Explique o que é XSS e como textContent previne esse ataque.
A7. Escreva o código para: (a) selecionar o elemento de id menu; (b) selecionar todos os <li> dentro de .nav; (c) selecionar o primeiro input do tipo email.
A8. Escreva o código que adiciona a classe ativo, remove inativo e alterna aberto em um elemento.
A9. Escreva os três passos para criar e inserir um novo <li> com texto "Item" em uma <ul> existente.
A10. O que faz closest(".card")? Dê um caso de uso.
A11. Por que é preferível trocar classes a definir element.style diretamente?
A12. Um querySelector retornou null e o código quebrou com Cannot read properties of null. Cite duas causas prováveis e como corrigir cada uma.
A13. Por que addEventListener é preferível a elemento.onclick?
A14. Diferencie event.target de event.currentTarget com um exemplo concreto.
A15. Descreva as três fases da propagação de eventos.
A16. O que faz event.preventDefault()? Dê dois casos de uso.
A17. Qual a diferença entre stopPropagation() e preventDefault()?
A18. O que é delegação de eventos e qual problema ela resolve?
A19. Diferencie os eventos input e change. Quando usar cada um?
A20. Escreva o código que executa uma função quando o usuário pressiona Enter em um campo de texto.
A21. Escreva o código que impede o recarregamento da página ao enviar um formulário.
A22. O que faz a opção { once: true } em addEventListener?
A23. Por que <div onclick=""> é pior que <button> do ponto de vista de acessibilidade? Cite três motivos.
B1. Implemente calculadora como um objeto com os métodos somar, subtrair, multiplicar, dividir (tratando divisão por zero) e potencia, todos com validação de tipo dos parâmetros (typeof).
Resultado esperado:calculadora.dividir(10, 0) retorna null (ou lança um erro com mensagem clara); calculadora.somar("2", 3) rejeita a string; calculadora.potencia(2, 10) retorna 1024.
Dica
Crie uma função auxiliar saoNumeros(...valores) que use valores.every((v) => typeof v === "number" && !Number.isNaN(v)) e chame-a no início de cada método. Métodos de objeto precisam de this? Aqui não — cada método só usa seus parâmetros, então arrow functions ou métodos curtos servem.
B2. Dada uma lista <ul> com 10 <li>, escreva o JavaScript que: pinta os ímpares de cinza-claro (via classe); adiciona a classe destaque ao primeiro e ao último; adiciona um número de ordem antes do texto de cada um; e exibe no console o texto de todos.
Resultado esperado: a lista alterna cores, o primeiro e o último ficam em negrito, cada item começa com "1. ", "2. " e assim por diante, e o console mostra os 10 textos.
Dica
querySelectorAll("ul li").forEach((li, indice) => …) — o segundo parâmetro do forEach é o índice. Para o texto, li.textContent = `${indice + 1}. ${li.textContent}`. Para o primeiro e o último, compare indice com 0 e com lista.length - 1.
B3. Construa uma tabela dinâmica: dado um array de alunos com três notas, gere uma <table> completa com <thead>, <tbody>, média calculada e uma coluna "Situação" cuja célula recebe a classe .aprovado, .exame ou .reprovado conforme o valor (≥ 6, ≥ 4, abaixo).
Resultado esperado: uma tabela com uma linha por aluno, a média com duas casas (toFixed(2)) e a célula de situação colorida pelo CSS.
Dica
Crie criarLinha(aluno) que devolve um <tr> com <td> gerados por createElement. Use reduce para a média e a função classificar da seção 1 para a situação. Monte tudo em um DocumentFragment e insira no <tbody> uma vez.
B4. Escreva uma função criarElemento(tag, atributos, filhos) que crie e configure um elemento genericamente. Exemplo de uso:
Object.entries(atributos).forEach(([nome, valor]) => el.setAttribute(nome, valor)). Para os filhos, se for string use document.createTextNode; se for elemento, appendChild direto — el.append(...filhos) já aceita os dois tipos.
B5. Implemente um modal: botão que abre, botão "×" que fecha, clique no fundo escurecido que fecha, tecla Escape que fecha. Ao abrir, o foco deve ir para dentro do modal; ao fechar, voltar ao botão que o abriu.
Resultado esperado: o modal funciona só com o teclado (Tab até o botão, Enter abre, Esc fecha) e o foco nunca "some".
Dica
Use o elemento nativo <dialog> com dialog.showModal() e dialog.close() — ele já trata Escape e o foco inicial. Para o clique no fundo, ouça click no próprio dialog e feche se e.target === dialog. Guarde document.activeElement antes de abrir para devolver o foco ao fechar.
B6. Crie um menu suspenso (dropdown) que abre ao clicar, fecha ao clicar fora (use delegação em document), fecha com Escape e navega com as setas do teclado.
Resultado esperado:↓ move o foco para o próximo item, ↑ para o anterior, Esc fecha e devolve o foco ao botão.
Dica
Guarde os itens em [...menu.querySelectorAll("a")] e o índice do item focado. No keydown, calcule o novo índice com (indice + 1) % itens.length (e (indice - 1 + itens.length) % itens.length para subir) e chame .focus(). O padrão de "clique fora" é o mesmo do Mão na massa.
B7. Implemente um contador de caracteres em um <textarea> com limite de 280: exibe os restantes, fica amarelo abaixo de 40 e vermelho abaixo de 10, e desabilita o botão de envio ao ultrapassar.
Resultado esperado: "280 caracteres restantes" ao carregar; ao digitar, o número cai; com 281 caracteres o botão fica disabled e o texto, vermelho.
Dica
Ouça input no textarea, calcule restantes = LIMITE - textarea.value.length e use classList.toggle("alerta", restantes < 40) e classList.toggle("perigo", restantes < 10). botao.disabled = restantes < 0.
B8. Crie uma lista de 20 itens gerados dinamicamente onde cada item tem botões "subir", "descer" e "excluir", todos tratados por um único ouvinte no contêiner (delegação).
Resultado esperado: clicar em "subir" troca o item de posição com o anterior; "excluir" remove; a lista continua funcionando depois de qualquer operação porque é re-renderizada a partir do array.
Dica
Não mova o <li> no DOM — troque as posições no array com [itens[i - 1], itens[i]] = [itens[i], itens[i - 1]] e chame renderizar(). Use data-acao e data-id nos botões e o switch da seção 14.
C1. Kanban com arrastar e soltar. Construa um quadro com três colunas (A fazer, Em andamento, Concluído). Cartões podem ser criados, editados, excluídos e arrastados entre colunas usando a API de Drag and Drop (dragstart, dragover, drop). Contadores por coluna. Deve funcionar também por teclado (botões "mover para →" e "← mover para").
Dica
Cada cartão tem draggable="true" e data-id. No dragstart, guarde o id em e.dataTransfer.setData("text/plain", id). A coluna precisa chamar e.preventDefault() no dragover para aceitar o drop. No drop, leia o id, mude o campo coluna do cartão no array e renderize tudo — o mesmo fluxo estado → renderização de sempre. Os botões de teclado fazem exatamente a mesma alteração no array.
Alguém enviou o código abaixo dizendo que "o contador funciona uma vez e depois para, e às vezes nem começa". Rode-o, reproduza os dois sintomas e encontre os quatro bugs plantados — sem reescrever do zero. Cada um é um erro clássico visto nesta aula.
O contador incrementa a cada clique, indefinidamente, inclusive quando o clique cai no texto "Somar".
"Zerar" volta o contador a 0 apenas quando clicado.
Nenhum erro no console ao carregar a página.
Um comentário acima de cada correção diz qual era o bug e por que acontecia (uma frase cada).
Pistas
Abra o Console antes de qualquer coisa: uma das falhas aparece em vermelho no carregamento e tem a ver com a ordem em que o navegador lê <head> e <body>.
Releia a seção 12 sobre target × currentTarget e pense no <span> dentro do botão.
Uma opção passada ao addEventListener está fazendo exatamente o que promete.
Compare zerar com zerar() — o que está sendo passado como ouvinte?
Uma lista de tarefas é o "olá, mundo" das interfaces dinâmicas — e o exercício que mais revela se você entendeu o fluxo estado → renderização → eventos. Construa a sua, do zero, seguindo a arquitetura da seção 18, sem alterar o DOM "na mão" em nenhum momento: toda mudança na tela nasce de uma mudança no array seguida de renderizar().
Critérios de pronto
Adicionar pelo botão ou pela tecla Enter; texto vazio ou só com espaços é rejeitado com mensagem de erro visível.
Concluir: clicar no checkbox alterna concluida no array e re-renderiza.
Excluir: botão com data-id, tratado por delegação, com confirmação.
Editar: duplo clique no texto o transforma em <input>; Enter confirma, Escape cancela, blur salva.
Filtrar: três botões (Todas / Pendentes / Concluídas) alteram uma variável de estado e re-renderizam; o botão ativo recebe a classe .ativo e aria-pressed="true".
Buscar: campo de texto com input + debounce de 300 ms, filtrando pelo termo.
Ordenar: <select> com as opções mais recentes, mais antigas e alfabética.
Limpar concluídas remove todas de uma vez, com confirmação.
Contadores de total, pendentes e concluídas, sempre atualizados.
Atalhos: Ctrl+Enter adiciona; Escape limpa o campo.
O arquivo js/tarefas.js está dividido nos blocos ESTADO, ELEMENTOS, DADOS, RENDERIZAÇÃO, EVENTOS e INICIALIZAÇÃO, nessa ordem.
Pistas
Comece pelo estado: let tarefas = [], let filtroAtual = "todas", let termoBusca = "", let ordenacao = "recentes". Cada tarefa é { id, texto, concluida, criadaEm } com id: Date.now().
Escreva obterTarefasVisiveis() como uma cadeia: copia o array, aplica filter do filtro, filter da busca e sort da ordenação — e só ela é usada por renderizar().
Para editar, guarde idEmEdicao no estado; renderizar() desenha um <input> em vez do texto quando o id bate. Enter/Escape/blur apenas alteram o estado e renderizam.
Um único addEventListener("click") na <ul> resolve checkbox, excluir e iniciar edição — use closest("[data-acao]") e um switch.
Sites de evento costumam ter um "quiz de conhecimentos" para engajar quem se inscreve. Construa um quiz de 10 perguntas de múltipla escolha sobre a Unidade 3 (você escreve as perguntas), com uma pergunta por tela, cronômetro de 20 segundos por questão, pontuação e um placar final. As perguntas vivem em um array de objetos; a tela é renderizada a partir do estado; não existe uma linha de HTML de pergunta escrita à mão.
Critérios de pronto
Uma pergunta por vez, com quatro alternativas em <button>; responder ou esgotar o tempo avança automaticamente.
Barra de tempo animada com CSS e atualizada por setInterval, que respeita prefers-reduced-motion.
Pontuação: 10 pontos por acerto mais um bônus de 1 ponto por segundo restante; a fórmula está comentada no código.
Navegação completa por teclado: teclas 1–4 selecionam a alternativa; Enter confirma.
Ao terminar, um placar mostra acertos, erros, tempo médio e as questões erradas com a resposta certa.
O quiz dispara um CustomEvent("quizFinalizado", { detail }) com o resultado, e um script separado escuta esse evento para gravar o melhor placar em localStorage.
Nenhum innerHTML recebe texto vindo das perguntas — tudo por textContent.
Pistas
Estado mínimo: indiceAtual, pontuacao, respostas (array), segundosRestantes e o id do setInterval — para poder cancelar com clearInterval ao responder.
Escreva renderizarPergunta() e renderizarPlacar() como funções separadas; avancar() decide qual chamar.
Para as teclas 1–4, ouça keydown em document e use Number(e.key) - 1 como índice da alternativa — mas ignore quando o quiz não estiver em andamento.
localStorage.setItem("quiz:melhor", JSON.stringify(detail)) no ouvinte do evento personalizado; leia com JSON.parse ao carregar. A próxima aula aprofunda o localStorage.
Parte 1 — Leitura (15 min). FLANAGAN, JavaScript: o guia definitivo, capítulos de funções, de scripting de documentos e de eventos. MILETTO & BERTAGNOLLI, Desenvolvimento de software II, capítulo de interação com o usuário. Na MDN em português, o guia "Introdução a eventos" (link em Para aprofundar).
Parte 2 — Entrega (40 min). No seu projeto autoral:
O menu mobile funcionando com clique, Esc e clique fora, com aria-expanded sincronizado (como no Mão na massa).
A listagem principal do seu domínio (produtos, quadras, vagas, pratos, pescarias) renderizada a partir de um array de pelo menos 6 objetos, com estado vazio tratado.
Ao menos um filtro por botões usando delegação de eventos.
Os exercícios B7 (contador de caracteres, aplicado ao seu formulário de contato) e B8 (lista com subir/descer/excluir) em uma pasta exercicios/aula13/.
Critério de pronto: nenhuma página do projeto mostra erro no Console; a listagem some do "Exibir código-fonte" e aparece na aba Elements; o menu abre e fecha só com o teclado.
Parte 3 — Discussão (5 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: explique, com um trecho do seu projeto, um problema que só a delegação de eventos resolve bem — e o que aconteceria sem ela.
Guarde no seu repositório: commit + push (ou a pasta do projeto).
[ ] js/menu.js, js/dados.js e js/app.js carregados com defer em todas as páginas, nessa ordem, sem erro no Console — e nenhum identificador declarado em dois arquivos.
[ ] js/app.jsampliado, não reescrito: a saudação e o console.table da Aula 10 e a contagem regressiva da Aula 11 continuam funcionando.
[ ] Botão hambúrguer com aria-expanded e aria-controls; menu abre/fecha por clique, Esc, clique fora e resize, sem classe .aberto e sem display: none.
[ ] Link da página atual marcado com aria-current="page" pelo JavaScript, e o atributo removido do HTML das cinco páginas.
[ ] Listagem principal renderizada a partir de um array de objetos, com textContent (nunca innerHTML para dados) e estado vazio tratado.
[ ] Filtro por botões com um único ouvinte (delegação) e aria-pressed atualizado.
[ ] Nenhum var, nenhum onclick no HTML, nenhum <div> clicável.
[ ] Código organizado nos blocos ESTADO → ELEMENTOS → DADOS → RENDERIZAÇÃO → EVENTOS → INICIALIZAÇÃO.
WHATWG — DOM Living Standard: https://dom.spec.whatwg.org/#events — a especificação oficial das fases de propagação, para quem quer ver a fonte.
FLANAGAN, David. JavaScript: o guia definitivo. Bookman, 2014 — capítulos sobre funções, scripting de documentos e tratamento de eventos.
STEFANOV, Stoyan. Padrões JavaScript. Novatec, 2010 — padrões de callbacks e de organização de código em módulos.
MILETTO, Evandro M.; BERTAGNOLLI, Silvia C. Desenvolvimento de software II. Bookman, 2014 — capítulo de interação com o usuário.
Na próxima aula, o formulário de inscrição do evento ganha validação campo a campo com mensagens acessíveis, expressões regulares para CPF, telefone e e-mail, e a programação passa a ter busca, filtro e ordenação em tempo real — tudo construído sobre as funções, o DOM e os eventos de hoje.
Capturar os valores de um formulário pelas três vias (querySelector, form.elements e FormData) e converter cada tipo de campo corretamente.
Escolher o momento certo de validar (input, blur, submit) e implementar a estratégia "valide no blur, corrija no input".
Usar a Constraint Validation API (validity, checkValidity, setCustomValidity, novalidate) combinando os atributos do HTML com regras de negócio em JavaScript.
Escrever funções validadoras puras, incluindo a verificação dos dígitos verificadores do CPF.
Ler e escrever expressões regulares para os formatos brasileiros usuais, reconhecendo o que a regex consegue e o que ela não consegue validar.
Exibir mensagens de erro específicas e acessíveis com role="alert", aria-invalid, aria-describedby e aria-live, levando o foco ao primeiro campo inválido.
Implementar busca, filtro e ordenação em tempo real sobre uma listagem, com normalização de acentos, debounce e tratamento do estado vazio.
[ ] O site do evento acadêmico com as cinco páginas responsivas, js/app.js carregado com defer em todas elas e o menu hambúrguer funcionando (Aula 13).
[ ] js/dados.js com os arrays palestras e palestrantes criados na Aula 12.
[ ] Domínio de funções, querySelector, classList, dataset, createElement, addEventListener, preventDefault e delegação de eventos (Aula 13).
[ ] Domínio de filter, map, find, forEach e sort sobre arrays de objetos (Aula 12).
[ ] O formulário de inscricao.html com <label>, <fieldset>, <select> e validação nativa (Aulas 03 e 04).
[ ] DevTools aberto na aba Console e o Live Server rodando.
Na aula passada o JavaScript saiu do console e entrou na página: você escreveu funções, manipulou o DOM, registrou ouvintes e renderizou os palestrantes a partir de um array, com filtro por delegação. Hoje o mesmo conjunto de ferramentas resolve os dois problemas que todo site real tem: impedir que dados errados entrem e deixar o usuário encontrar o que procura. O formulário de inscrição ganha validação campo a campo com mensagens acessíveis, e a programação passa a ter busca, filtro e ordenação em tempo real.
Todo processamento de formulário em JavaScript começa no mesmo lugar: o evento submit do <form> — nunca o click do botão. O motivo você já viu na Aula 13: Enter em qualquer campo de texto envia o formulário sem passar pelo botão, e um ouvinte de click simplesmente não é acionado.
constform=document.querySelector("#form-inscricao");form.addEventListener("submit",(evento)=>{evento.preventDefault();// SEMPRE: sem isso a página recarrega e tudo someconsole.log("Formulário enviado sem recarregar a página");});
O atributo novalidate no <form>desliga a validação nativa do navegador. Fazemos isso quando queremos controlar totalmente as mensagens: as caixas amarelas do Chrome não são personalizáveis, mudam de texto entre navegadores, somem sozinhas depois de alguns segundos e não são lidas de forma confiável por leitores de tela. Mesmo desligando a exibição, mantemos os atributos required, type, minlength e pattern no HTML: eles continuam alimentando a Constraint Validation API (seção 3) e documentam as regras para quem lê o HTML.
⚠️ Atençãonovalidate desliga a exibição das mensagens nativas, não a validação. campo.validity.valueMissing continua funcionando. É por isso que a combinação "atributos no HTML + mensagens em JavaScript" é a arquitetura recomendada: você não reescreve regras que o navegador já sabe aplicar.
constform=document.querySelector("#form-inscricao");// 1. Elemento por elemento — explícito, bom quando você precisa do elemento em siconstnome=document.querySelector("#nome").value.trim();// 2. Pela coleção elements do formulário — usa o atributo nameconstemail=form.elements.email.value.trim();// 3. FormData — a forma mais prática quando você quer o objeto inteiroconstdados=Object.fromEntries(newFormData(form).entries());console.log(dados);// { nome: "Ana Souza", email: "ana@exemplo.br", cpf: "123.456.789-09" }
new FormData(form) monta um objeto iterável com todos os campos que têm name e não estão desabilitados. Object.fromEntries transforma esses pares chave/valor em um objeto JavaScript comum. Duas limitações que valem a pena guardar: FormData ignora campos sem name, e um grupo de checkboxes com o mesmo name fica só com o último valor — para esse caso, use dados.getAll("atividades").
Forma
Vantagem
Quando usar
querySelector("#id").value
Devolve o elemento, não só o valor
Quando você vai mexer em classes e atributos do campo
form.elements.nome.value
Não depende de id, só de name
Validação genérica, percorrendo campos
new FormData(form)
Pega tudo de uma vez, pronto para enviar
Montar o objeto final depois de validado
1.3 Cada tipo de campo devolve uma coisa diferente¶
JavaScript
// Texto: SEMPRE aplique trim() — " " não é um nome preenchidoconstnome=document.querySelector("#nome").value.trim();// Número: value é SEMPRE string, mesmo em type="number"constidade=Number(document.querySelector("#idade").value);// Checkbox único: booleanoconstaceitouTermos=document.querySelector("#termos").checked;// Radio: pegue o que está marcado (pode não haver nenhum)constvinculo=form.querySelector("input[name='vinculo']:checked")?.value??"";// Grupo de checkboxes: transforme a NodeList em array e extraia os valoresconstatividades=[...form.querySelectorAll("input[name='atividades']:checked")].map((caixa)=>caixa.value);// Select simples e select múltiploconstcurso=document.querySelector("#curso").value;constcursos=[...document.querySelector("#cursos").selectedOptions].map((o)=>o.value);// Arquivo: uma lista de File, mesmo sem o atributo multipleconstcomprovante=document.querySelector("#comprovante").files[0];
⚠️ Atenção
O erro clássico, que aparece o tempo todo: input.value de um type="number" retorna a string"25", não o número 25. Fazer idade + 1 produz "251". Converta sempre com Number() — e lembre-se, da Aula 11, de que Number("") é 0, então teste o campo vazio antes de converter.
O ?. no radio (encadeamento opcional, Aula 11) evita o clássico Cannot read properties of null (reading 'value') quando nenhuma opção está marcada; o ?? completa com uma string vazia.
Validar cedo demais irrita; validar tarde demais frustra. O momento certo depende do evento:
Momento
Evento
Custo de escolher errado
Ao digitar
input
Acusa "e-mail inválido" na segunda letra digitada
Ao sair do campo
blur
O usuário só descobre o erro depois de sair
Ao enviar
submit
Tarde demais como único feedback
A estratégia que dá a melhor experiência combina os três, em quatro regras:
Não valide enquanto o usuário digita pela primeira vez. Ele ainda está escrevendo.
Valide no blur, quando ele sai do campo — o momento natural de "terminei este aqui".
A partir do instante em que o campo já mostrou erro, passe a validar no input. Assim o usuário vê a mensagem sumir enquanto corrige, em vez de precisar sair do campo de novo.
Valide tudo no submit e leve o foco ao primeiro campo inválido.
JavaScript
constcampos=[...form.querySelectorAll("input, select, textarea")];campos.forEach((campo)=>{campo.addEventListener("blur",()=>{validarCampo(campo);campo.dataset.tocado="true";// marca: este campo já foi visitado});campo.addEventListener("input",()=>{if(campo.dataset.tocado==="true")validarCampo(campo);});});
O truque está no dataset.tocado (Aula 13): o próprio DOM guarda o estado "este campo já foi visitado", sem precisar de um array paralelo em JavaScript. Como dataset sempre devolve string, a comparação é com "true", não com true.
💡 Dicablur não borbulha, então form.addEventListener("blur", …) não funciona — é por isso que o código acima registra o ouvinte campo a campo. Se você quiser um ouvinte só, use o evento focusout, que é a versão borbulhante do blur.
O navegador já validou o campo antes de você escrever qualquer linha. A Constraint Validation API é a interface que expõe esse resultado — e é ela que faz required, type="email", min, max, minlength e pattern valerem também para o seu código.
constcampo=document.querySelector("#email");campo.validity.valid;// true se o campo passa em todas as restriçõescampo.validity.valueMissing;// required não preenchidocampo.validity.typeMismatch;// type="email" ou type="url" com formato erradocampo.validity.patternMismatch;// não casou com o atributo patterncampo.validity.tooShort;// abaixo do minlength (tooLong: acima do maxlength)campo.validity.rangeUnderflow;// abaixo do min (rangeOverflow: acima do max)campo.validity.customError;// há uma mensagem definida por setCustomValidity
campo.checkValidity();// true/false, silencioso (não mostra nada na tela)campo.reportValidity();// valida E mostra a mensagem nativa do navegadorcampo.setCustomValidity("As senhas não coincidem.");// define um erro seucampo.setCustomValidity("");// limpa o erro personalizado
setCustomValidity merece cuidado: enquanto a mensagem estiver definida, o campo é considerado inválido para sempre, mesmo que o usuário corrija o valor. Você precisa limpá-la (setCustomValidity("")) no início de cada nova validação. Esquecer isso produz o bug "o formulário nunca envia" mais comum da aula.
Em vez de escrever de novo a regra "e-mail precisa ter arroba", leia o diagnóstico que o navegador já fez e devolva um texto em português decente:
JavaScript
functionmensagemNativa(campo){constv=campo.validity;constrotulo=campo.labels[0]?.textContent.replace("*","").trim()||"Este campo";if(v.valueMissing)return`${rotulo} é obrigatório.`;if(v.typeMismatch)return`${rotulo} está em um formato inválido.`;if(v.tooShort)return`${rotulo} deve ter ao menos ${campo.minLength} caracteres.`;if(v.tooLong)return`${rotulo} deve ter no máximo ${campo.maxLength} caracteres.`;if(v.rangeUnderflow)return`${rotulo} deve ser no mínimo ${campo.min}.`;if(v.rangeOverflow)return`${rotulo} deve ser no máximo ${campo.max}.`;if(v.patternMismatch)returncampo.title||`${rotulo} está em um formato inválido.`;return"";}
campo.labels é uma coleção com todos os <label> associados ao campo — mais uma razão para você ter caprichado no for/id da Aula 03. O ?. protege contra campos sem rótulo, e o campo.title aproveita o atributo title do pattern como mensagem, que é exatamente para isso que ele existe.
A arquitetura final combina duas camadas: os atributos HTML declaram as regras simples e servem de documentação; o JavaScript cuida das regras de negócio (dígitos do CPF, idade mínima, senhas coincidentes, ao menos um minicurso escolhido) e da apresentação das mensagens.
🔬 Investigue
Abra inscricao.html com o Live Server e o Console. Digite const e = document.querySelector("#email") e depois e.validity. Expanda o objeto: todos os campos aparecem como false e valid: true porque o campo está vazio e ainda não é required para o navegador… ou aparece valueMissing: true, se você já colocou required. Agora digite e.value = "ana@" e chame e.validity de novo: typeMismatch virou true. Por fim, chame e.reportValidity() e veja a caixa nativa — a mesma que novalidate esconde.
🧠 Você sabia?
O atributo pattern existe desde o HTML5, mas as âncoras ^ e $ são implícitas nele: o navegador sempre exige correspondência total. Escrever pattern="^\d{5}-?\d{3}$" funciona por acidente (as âncoras extras não atrapalham), mas escrever pattern="/\d{5}-?\d{3}/"nunca casa, porque as barras viram caracteres literais a serem procurados. É o erro nº 1 de quem aprendeu regex em JavaScript antes de usar pattern.
A melhor forma de organizar validação é escrever funções puras: recebem um valor, devolvem a mensagem de erro (string) ou "" quando está tudo certo. Elas não tocam no DOM, não dependem de variáveis globais e por isso podem ser testadas no console, reaproveitadas em outro projeto e lidas sem sustos.
JavaScript
// js/inscricao.js — validadores (funções puras: entram dados, sai mensagem)functionvalidarObrigatorio(valor,rotulo="Este campo"){returnvalor.trim()===""?`${rotulo} é obrigatório.`:"";}functionvalidarTamanho(valor,minimo,maximo,rotulo){constv=valor.trim();if(v.length<minimo)return`${rotulo} deve ter ao menos ${minimo} caracteres.`;if(v.length>maximo)return`${rotulo} deve ter no máximo ${maximo} caracteres.`;return"";}functionvalidarNomeCompleto(valor){constpartes=valor.trim().split(/\s+/);// divide por qualquer sequência de espaçosif(partes.length<2)return"Informe nome e sobrenome.";if(partes.some((parte)=>parte.length<2)){return"Cada parte do nome deve ter ao menos 2 letras.";}return"";}functionvalidarEmail(valor){constv=valor.trim();if(v==="")return"O e-mail é obrigatório.";if(!v.includes("@"))return"O e-mail deve conter @.";const[usuario,dominio]=v.split("@");if(!usuario||!dominio)return"Formato de e-mail inválido.";if(!dominio.includes("."))return"O domínio do e-mail está incompleto.";return"";}
Repare no padrão: cada função devolve uma mensagem, a primeira que se aplica. Mensagens boas dizem o que fazer. "Campo inválido" não ajuda ninguém; "A senha deve conter ao menos uma letra maiúscula" resolve o problema do usuário em um segundo.
Validar uma data digitada como 31/02/2000 exige mais do que formato. Duas armadilhas do objeto Date explicam por quê:
JavaScript
constdata=newDate(2000,1,31);// meses vão de 0 a 11: 1 é fevereirodata.getDate();// 2 ← o Date "conserta" a data sozinho, sem erro nenhumdata.getMonth();// 2 ← virou março
A primeira armadilha é que os meses vão de 0 a 11. A segunda é que Date nunca reclama de uma data inexistente: ele a desloca. A defesa é sempre a mesma — reconstruir dia, mês e ano a partir do objeto criado e comparar com o que foi digitado. Se não bate, a data não existe no calendário. A função completa, com o cálculo da idade, está na Mão na massa.
O CPF tem 11 dígitos, sendo os dois últimos dígitos verificadores calculados a partir dos nove primeiros. Isso significa que um CPF digitado errado é detectável sem consultar nenhum servidor — e é por isso que nenhum site sério aceita 111.111.111-11.
JavaScript
functionvalidarCPF(cpf){constnumeros=cpf.replace(/\D/g,"");// \D = tudo que NÃO é dígitoif(numeros.length!==11)return"O CPF deve ter 11 dígitos.";if(/^(\d)\1{10}$/.test(numeros))return"CPF inválido.";// 000…0, 111…1, 999…9constcalcularDigito=(base,pesoInicial)=>{letsoma=0;for(leti=0;i<base.length;i++){soma=soma+Number(base[i])*(pesoInicial-i);}constresto=(soma*10)%11;returnresto===10?0:resto;};constprimeiro=calcularDigito(numeros.slice(0,9),10);constsegundo=calcularDigito(numeros.slice(0,10),11);if(primeiro!==Number(numeros[9])||segundo!==Number(numeros[10])){return"CPF inválido. Confira os números digitados.";}return"";}
Leia a regex /^(\d)\1{10}$/ com calma, porque ela usa um recurso que a seção 5 vai formalizar: (\d) captura um dígito no grupo 1, e \1{10} exige que o mesmo dígito apareça mais dez vezes. É a forma mais curta de dizer "todos os onze dígitos são iguais".
🧠 Você sabia?
O algoritmo do CPF é uma variação do cálculo de dígito verificador módulo 11, o mesmo princípio usado no ISBN de livros, no código de barras dos boletos bancários e no número de matrícula de muitas universidades. A ideia é antiga e simples: some os dígitos multiplicados por pesos decrescentes, tire o resto da divisão por 11, e guarde o resultado no fim do número. Um dígito trocado ou dois dígitos invertidos mudam a soma e o erro é detectado na hora — sem internet, sem banco de dados.
Uma expressão regular (regex) é um padrão que descreve um conjunto de strings. Com ela você responde perguntas como "este texto tem formato de e-mail?", "quais números aparecem aqui?" ou "troque todo espaço duplo por um só". Regex existe em praticamente todas as linguagens e ferramentas — JavaScript, Python, Java, SQL, grep, a busca do VS Code — e é um dos poucos conhecimentos que você leva inteiro para qualquer tecnologia da carreira.
JavaScript
// 1. Forma literal — preferida quando o padrão é fixoconstpadrao=/abc/;// 2. Construtor — quando o padrão é montado em tempo de execuçãoconsttermoDigitado="café";constbusca=newRegExp(termoDigitado,"gi");
No construtor, a string precisa escapar as barras invertidas em dobro ("\\d" em vez de \d), porque a barra invertida também é especial dentro de strings. Use a forma literal sempre que puder.
. qualquer caractere, exceto quebra de linha
\d dígito [0-9] \D NÃO dígito
\w palavra [A-Za-z0-9_] \W NÃO caractere de palavra
\s espaço, tabulação, quebra \S NÃO espaço em branco
[abc] a, b OU c [^abc] qualquer coisa EXCETO a, b, c
[a-z] letra minúscula [A-Z] letra maiúscula
[0-9] dígito [À-ÿ] letras acentuadas da faixa Latin-1
JavaScript
/[A-Za-zÀ-ÿ]/.test("ção");// true — letra com ou sem acento/[^\d]/.test("1234");// false — só há dígitos
* 0 ou mais
+ 1 ou mais
? 0 ou 1 (opcional)
{n} exatamente n
{n,} n ou mais
{n,m} entre n e m
JavaScript
/^\d{3}$/.test("123");// true — exatamente três dígitos/^\d{2,4}$/.test("12345");// false — passou de quatro/colou?r/.test("colour");// true — o "u" é opcional
Gulosos e preguiçosos. Por padrão, quantificadores são gulosos: pegam o máximo possível. Acrescentar ? os torna preguiçosos.
JavaScript
consttexto="<b>negrito</b> e <i>itálico</i>";texto.match(/<.+>/)[0];// "<b>negrito</b> e <i>itálico</i>" ← gulosotexto.match(/<.+?>/)[0];// "<b>" ← preguiçoso
^ início da string (ou da linha, com a flag m)
$ fim da string
\b limite de palavra
\B NÃO limite de palavra
JavaScript
/^abc/.test("abcdef");// true — começa com abc/abc$/.test("xyzabc");// true — termina com abc/^abc$/.test("abc");// true — é exatamente abc/\bgato\b/.test("gatorade");// false — "gato" precisa ser palavra inteira
📌 Vale gravar
A âncora é o que separa busca de validação. /\d{3}/.test("abc1234xyz") é true: encontrou três dígitos em algum lugar. /^\d{3}$/.test("abc1234xyz") é false: a string inteira precisa ser exatamente três dígitos. Toda regex de validação de formato precisa de ^ no início e $ no fim. Essa questão cai todo semestre.
/(abc)+/;// grupo de captura: "abc" repetido/(?:abc)+/;// grupo SEM captura (não guarda o trecho)/(palestra|minicurso|mesa)/;// alternância: um dos três
Lookahead ((?=…) positivo, (?!…) negativo) e lookbehind ((?<=…) e (?<!…)) verificam o que vem depois ou antes sem consumir os caracteres:
JavaScript
// Senha forte: ao menos 1 minúscula, 1 maiúscula, 1 dígito, 1 especial, mínimo 8constsenhaForte=/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$/;/\d+(?=\s*vagas)/.exec("Restam 12 vagas")[0];// "12" — seguido de "vagas"/(?<=R\$\s?)\d+([.,]\d{2})?/.exec("Total: R$ 89,90")[0];// "89,90" — precedido de R$
Cada lookahead da senha forte é uma verificação independente feita a partir do início da string: "olhando para a frente, existe em algum ponto uma minúscula?". Todas precisam ser verdadeiras, e só então [A-Za-z\d@$!%*?&#]{8,} consome a string de fato.
// test — devolve booleano. O mais usado em validação./\d/.test("abc123");// true// match — sem a flag g, devolve os grupos capturados"07/05/1998".match(/(\d{2})\/(\d{2})\/(\d{4})/);// ["07/05/1998", "07", "05", "1998", index: 0, ...]// match — com a flag g, devolve só as ocorrências"a1 b2 c3".match(/\d/g);// ["1", "2", "3"]// matchAll — precisa da flag g; devolve um iterável com os grupos de cada ocorrênciafor(constmof"Ana: 8.5, Bruno: 6.0".matchAll(/(?<nome>\w+): (?<nota>[\d.]+)/g)){console.log(m.groups.nome,m.groups.nota);}// replace — $1, $2 referenciam os grupos capturados; uma função permite calcular" muitos espaços ".replace(/\s+/g," ").trim();// "muitos espaços""07/05/1998".replace(/(\d{2})\/(\d{2})\/(\d{4})/,"$3-$2-$1");// "1998-05-07""restam 100".replace(/\d+/,(n)=>Number(n)*2);// "restam 200"// split aceita regex como separador"nome; idade , curso".split(/\s*[;,]\s*/);// ["nome", "idade", "curso"]
O padrão TELEFONE merece uma leitura guiada: \(?\d{2}\)? aceita o DDD com ou sem parênteses; \s? permite um espaço; 9? torna opcional o nono dígito dos celulares; \d{4}-?\d{4} aceita o traço ou a ausência dele. Assim (66) 99999-9999, 66999999999 e (66) 3511-1000 passam, e 9999-9999 (sem DDD) não passa.
// Suficiente para 99,9% dos casos reaisconstEMAIL=/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
Não existe regex simples que valide e-mail perfeitamente. A especificação que define o formato permite construções tão exóticas (comentários entre parênteses, aspas, caracteres acentuados) que a expressão completa passa de seis mil caracteres e é ilegível. A recomendação profissional é usar uma regex simples de formato e confirmar o endereço por um e-mail de verificação. Uma regex agressiva demais rejeita endereços válidos — problema pior do que aceitar um endereço inválido, porque o usuário desiste do cadastro sem entender por quê.
🧠 Você sabia?
As expressões regulares nasceram em 1951, num artigo do matemático Stephen Kleene sobre linguagens regulares — décadas antes da web. O * que você usa em \d* chama-se, até hoje, estrela de Kleene. A notação chegou à programação nos anos 1960, com o editor qed de Ken Thompson, e de lá foi para o grep do Unix (o nome vem de g/re/p, "global / regular expression / print"). Ou seja: você está usando uma ideia de setenta anos para conferir um CPF.
Combine sempre regex (formato) com validação algorítmica (regra), como você fez no CPF da seção 4.2.
ReDoS — travar o navegador com uma regex. Padrões com quantificador dentro de quantificador podem levar tempo exponencial para concluir que algo não casa:
JavaScript
// Perigosa: quantificador aninhadoconstperigosa=/^(a+)+$/;// perigosa.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaX") pode travar a aba// Reescrita segura, com o mesmo efeito práticoconstsegura=/^a+$/;
Evite (x+)+, (x*)* e (x|x)*. E, quando um padrão passar de uma linha, decomponha-o em pedaços nomeados e monte com new RegExp: você não vai lembrar o que aquela sequência de símbolos faz daqui a duas semanas. Sempre comente uma regex não trivial.
🔬 Investigue
Abra o console e teste: /^\d{3}$/.test("123"), depois /^\d{3}$/.test(" 123") — por que o segundo é false? Agora rode "Ana Paula da Silva".split(/\s+/) e conte os elementos. Por fim, vá a https://regex101.com, cole ^\(?\d{2}\)?\s?9?\d{4}-?\d{4}$ no campo da expressão, escolha o sabor ECMAScript (JavaScript) e teste com (66) 99988-7766, 66999887766 e 9998-7766. O painel da direita explica token por token o que cada símbolo faz — é a melhor forma de aprender regex que existe.
Máscara é a formatação que aparece enquanto o usuário digita: 12345678909 vira 123.456.789-09. Ela reduz erros de digitação e deixa claro qual formato o campo espera.
JavaScript
// js/inscricao.js — máscarasfunctionmascaraCPF(valor){returnvalor.replace(/\D/g,"")// fica só com os dígitos.slice(0,11)// no máximo 11.replace(/(\d{3})(\d)/,"$1.$2")// primeiro ponto.replace(/(\d{3})(\d)/,"$1.$2")// segundo ponto.replace(/(\d{3})(\d{1,2})$/,"$1-$2");// traço antes dos verificadores}// Aplicação: reescreve o valor do campo a cada tecladocument.querySelector("#cpf").addEventListener("input",(evento)=>{evento.target.value=mascaraCPF(evento.target.value);});
A receita é sempre a mesma: limpe tudo o que não é dígito, corte no comprimento máximo e insira os separadores com replace e grupos capturados. As máscaras de telefone e de data seguem exatamente esse molde e estão completas na Mão na massa.
⚠️ Atenção
Máscaras atrapalham quem cola um valor já formatado e quem usa leitor de tela (o campo muda sozinho enquanto a pessoa digita). Duas regras compensam isso: sempre remova a formatação (valor.replace(/\D/g, "")) antes de validar e antes de enviar ao servidor, e nunca use a máscara como única indicação do formato — coloque também um texto de ajuda visível, ligado ao campo por aria-describedby.
Uma mensagem de erro só cumpre seu papel se todo mundo a percebe: quem enxerga, quem não distingue vermelho de verde e quem usa leitor de tela.
JavaScript
// js/inscricao.js — exibição do estado de um campofunctionmostrarErro(campo,mensagem){constalvo=document.querySelector(`#erro-${campo.id}`);if(mensagem){campo.classList.add("invalido");campo.classList.remove("valido");campo.setAttribute("aria-invalid","true");campo.setAttribute("aria-describedby",`erro-${campo.id}`);alvo.textContent=mensagem;}else{campo.classList.remove("invalido");campo.classList.add("valido");campo.setAttribute("aria-invalid","false");campo.removeAttribute("aria-describedby");alvo.textContent="";}}
css/estilo.css — estados do formulário
CSS
/* ===== Formulário validado ===== */.campo{display:grid;gap:0.25rem;margin-bottom:1.25rem;}.campoinput,.camposelect,.campotextarea{width:100%;padding:0.625rem0.75rem;border:2pxsolidvar(--cor-borda,#ccc);border-radius:6px;font:inherit;transition:border-color0.2s;}.campoinput:focus-visible,.camposelect:focus-visible{outline:3pxsolidrgba(26,127,181,0.4);outline-offset:1px;border-color:var(--cor-secundaria,#1a7fb5);}.campo.invalido{border-color:#c0392b;}.campo.valido{border-color:#27ae60;}.erro{display:block;min-height:1.2em;/* reserva espaço: evita o layout "pular" ao exibir o erro */color:#c0392b;font-size:0.875rem;}.erro:not(:empty)::before{content:"⚠ ";/* o erro não depende só da cor */}@media(prefers-reduced-motion:reduce){.campoinput,.camposelect{transition:none;}}
Os cinco requisitos de acessibilidade em mensagens de erro:
role="alert" no elemento da mensagem: leitores de tela anunciam o conteúdo assim que ele aparece, sem o usuário precisar navegar até lá.
aria-invalid="true" no campo: o leitor anuncia "inválido" ao entrar no campo.
aria-describedby ligando o campo à mensagem: o texto do erro é lido junto com o rótulo.
Nunca sinalize erro só pela cor. Some texto e um símbolo (o ::before do CSS acima).
Reserve espaço para a mensagem com min-height. Sem isso, o layout salta quando o erro aparece e o usuário perde a referência visual — e quem usa ampliador de tela se perde de vez.
Note que o forEach valida todos os campos antes de parar: o usuário vê de uma vez tudo o que precisa corrigir, e não um erro por tentativa. O focus() no primeiro inválido é obrigatório para quem navega por teclado — sem ele, a pessoa aperta "Enviar", nada acontece e ela não faz ideia de onde está o problema.
⚠️ Atenção
Toda a validação desta aula acontece no navegador, e o navegador é do usuário. Qualquer pessoa abre o DevTools, apaga o required e envia o formulário; ou envia a requisição direto, sem passar pela sua página. Validação no cliente serve para experiência (avisar cedo, evitar ida e volta ao servidor); a validação que serve para segurança é a do servidor, e você a construirá no Nível 2. Nunca confie em dados que vieram do navegador.
O navegador oferece um armazenamento simples, que sobrevive ao fechamento da aba: o localStorage. Ele guarda pares chave/valor, sempre em texto.
JavaScript
// EscreverlocalStorage.setItem("inscricao:rascunho",JSON.stringify({nome:"Ana",curso:"SI"}));// Ler (devolve null quando a chave não existe)constbruto=localStorage.getItem("inscricao:rascunho");constrascunho=bruto?JSON.parse(bruto):null;// Remover uma chave e limpar tudolocalStorage.removeItem("inscricao:rascunho");localStorage.clear();
Como só há texto, objetos e arrays precisam de JSON.stringify na ida e JSON.parse na volta. Guardar { nome: "Ana" } sem stringify armazena a string "[object Object]" — outro clássico da primeira semana.
Recurso
Vive até
Cabe
localStorage
Ser apagado pelo código ou pelo usuário
Cerca de 5 MB por origem
sessionStorage
A aba ser fechada
Cerca de 5 MB por aba
Variável em JavaScript
A página recarregar
A memória disponível
⚠️ AtençãolocalStorage é legível por qualquer script da mesma origem e fica no computador, muitas vezes compartilhado. Nunca guarde senha, CPF, token de acesso ou dado de cartão. No rascunho da inscrição vamos salvar apenas nome, e-mail e curso — CPF e telefone ficam de fora, de propósito.
🔬 Investigue
Abra qualquer página do site, vá ao DevTools na aba Application (Chrome) ou Armazenamento (Firefox) e clique em Local Storage → a URL do seu site. Rode no console localStorage.setItem("teste", "olá") e veja a linha aparecer na tabela em tempo real. Feche o navegador inteiro, abra de novo, volte à página e rode localStorage.getItem("teste"): o valor continua lá. Agora abra a mesma página em uma janela anônima e repita o getItem: devolve null, porque a janela anônima é outra área de armazenamento.
9. Consultas dinâmicas: busca, filtro e ordenação¶
A segunda metade da aula responde a outra pergunta: como deixar o usuário encontrar o que procura numa lista? A resposta é sempre a mesma arquitetura da Aula 13 — estado → dados → renderização → eventos —, com o estado agora guardando o termo buscado, o filtro escolhido e o critério de ordenação.
A função que produz a lista visível é sempre a mesma sequência: cópia → busca → filtro → ordenação.
JavaScript
// Estado da consulta (os nomes dos campos vêm do js/dados.js da Aula 12)lettermoBusca="";letareaAtual="todas";letordenacaoAtual="hora";functionobterPalestrasVisiveis(){letresultado=[...palestras];// 1. cópia: sort() modifica o array originalif(termoBusca){// 2. busca por textoconsttermo=normalizar(termoBusca);resultado=resultado.filter((p)=>normalizar(p.titulo).includes(termo));}if(areaAtual!=="todas"){// 3. filtro por categoriaresultado=resultado.filter((p)=>p.area===areaAtual);}constordenadores={// 4. ordenaçãohora:(a,b)=>a.dia-b.dia||a.hora.localeCompare(b.hora),titulo:(a,b)=>a.titulo.localeCompare(b.titulo,"pt-BR"),vagas:(a,b)=>b.vagas-b.inscritos-(a.vagas-a.inscritos),};resultado.sort(ordenadores[ordenacaoAtual]);returnresultado;}
Três detalhes que separam código que funciona de código que funciona sempre:
[...palestras] cria uma cópia. sort() reordena o array original; sem a cópia, a ordem dos dados mudaria a cada renderização e o filtro seguinte partiria de outra base. É um bug sutil, difícil de encontrar e frequente.
localeCompare(b, "pt-BR") ordena corretamente palavras acentuadas. O sort() puro compara códigos de caractere e coloca "Ávila" depois de "Zampieri".
a.dia - b.dia || a.hora.localeCompare(b.hora) é ordenação em dois níveis: quando os dias são iguais, a subtração dá 0 (falso) e o || passa para o critério de desempate. Repare no nome do campo: hora, como no dados.js da Aula 12. Inventar um horario aqui daria undefined.localeCompare — TypeError na primeira ordenação.
functionnormalizar(texto){returntexto.normalize("NFD")// separa a letra do acento combinante.replace(/[̀-ͯ]/g,"")// remove os acentos combinantes.toLowerCase().trim();}normalizar("Segurança");// "seguranca"normalizar("CAFÉ");// "cafe"
Na forma de normalização NFD, o caractere "é" é decomposto em "e" mais um acento combinante separado. Esses acentos vivem no intervalo Unicode ̀–ͯ, e uma única regex com a flag g remove todos de uma vez. Aplique normalizar nos dois lados da comparação: no termo digitado e no texto pesquisado. Sem isso, quem digita "seguranca" no celular não encontra "Segurança" — e desiste.
Digitar "acessibilidade" dispara catorze eventos input. Filtrar e redesenhar catorze vezes trava a interface em listas grandes. O debounce da Aula 13 resolve: a função só executa quando os disparos param por um tempo.
Toda listagem filtrável precisa de duas coisas que iniciantes esquecem:
Uma mensagem de estado vazio que diga o que fazer ("Nenhuma atividade encontrada para 'xyz'. Tente outro termo ou limpe os filtros."), e não uma tela em branco.
Um contador anunciado (aria-live="polite") informando quantos resultados apareceram. Quem usa leitor de tela não vê a lista encolher; sem o contador, digitar na busca não produz nenhum retorno perceptível.
🔎 Por baixo do capôaria-live="polite" cria uma região viva: o leitor de tela passa a observar aquele elemento e anuncia mudanças de conteúdo assim que terminar de ler o que estava lendo. assertive interrompe na hora — reserve para erros graves. A região precisa existir no HTML antes de ser preenchida: se você criar o elemento e já colocar o texto dentro, muitos leitores não anunciam nada, porque não havia região para observar. É por isso que o <p> vazio já vem no HTML.
💻 Mão na massa — Inscrição validada e programação com busca¶
Ao fim destes oito passos, o site da Semana Acadêmica de Sistemas de Informação terá um formulário de inscrição que valida campo a campo com mensagens acessíveis, salva rascunho e mostra confirmação; e uma página de programação com busca em tempo real, filtro por área e ordenação — tudo sobre o array palestras do js/dados.js da Aula 12, sem mudar uma linha daquele arquivo.
⚠️ Atençãojs/dados.js é a fonte única de dados do projeto desde a Aula 12, e o esquema dele é lei: palestras com area, hora e palestranteId; o array palestrantes; o dicionário nomesDasAreas. O js/relatorios.js (Aula 12) e o js/palestrantes.js (Aula 13) já dependem desses nomes. Hoje você adapta a consulta aos dados, não os dados à consulta.
Primeiro o <head>, com os quatro scripts na ordem que importa — defer executa na ordem das tags, e inscricao.js precisa que debounce (do app.js, Passo 4) já exista:
inscricao.html — <head> completo
HTML
<head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metaname="description"content="Inscrição gratuita na Semana Acadêmica de Sistemas de Informação."><metaname="author"content="Semana Acadêmica de Sistemas de Informação"><title>Inscrição — Semana Acadêmica de Sistemas de Informação</title><linkrel="stylesheet"href="css/estilo.css"><scriptsrc="js/menu.js"defer></script><scriptsrc="js/dados.js"defer></script><scriptsrc="js/app.js"defer></script><scriptsrc="js/inscricao.js"defer></script></head>
Agora o <main>. A estrutura repete o mesmo bloco para cada campo: <label>, campo, <span class="erro" role="alert">.
⚠️ Atenção
A <section class="vagas"> das Aulas 10 e 11 — com #vagas-restantes, #vagas-totais, #percentual-ocupacao, #aviso-vagas e #valor-taxa — fica onde está, acima do formulário. É ela que mostra o "Últimas vagas!" exigido no Checkpoint da Aula 11, e o código que a alimenta continua no js/inscricao.js (você o amplia no Passo 3, não o substitui). O que muda de lugar é só o <form>.
inscricao.html — conteúdo do <main>
HTML
<mainid="conteudo"tabindex="-1"class="container"><h1>Inscrição</h1><p>Preencha os dados abaixo. Os campos marcados com * são obrigatórios.</p><!-- Seção de vagas das Aulas 10 e 11: continua exatamente como estava --><sectionclass="vagas"><h2>Vagas</h2><p>
Restam <strongid="vagas-restantes">—</strong> de
<spanid="vagas-totais">—</span> vagas
(<spanid="percentual-ocupacao">—</span>% ocupadas).
</p><pid="aviso-vagas"class="aviso"role="status"></p><pclass="taxa">
Taxa de inscrição: <strongid="valor-taxa">—</strong><spanid="observacao-taxa"></span></p></section><pid="aviso-formulario"class="aviso-formulario"role="status"></p><formid="form-inscricao"novalidate><fieldset><legend>Seus dados</legend><divclass="campo"><labelfor="nome">Nome completo *</label><inputtype="text"id="nome"name="nome"autocomplete="name"requiredminlength="5"maxlength="80"><spanclass="erro"id="erro-nome"role="alert"></span></div><divclass="campo"><labelfor="email">E-mail *</label><inputtype="email"id="email"name="email"autocomplete="email"required><spanclass="erro"id="erro-email"role="alert"></span></div><divclass="campo"><labelfor="cpf">CPF *</label><inputtype="text"id="cpf"name="cpf"inputmode="numeric"maxlength="14"placeholder="000.000.000-00"required><spanclass="erro"id="erro-cpf"role="alert"></span></div><divclass="campo"><labelfor="telefone">Telefone com DDD *</label><inputtype="text"id="telefone"name="telefone"inputmode="tel"maxlength="15"placeholder="(66) 99999-9999"required><spanclass="erro"id="erro-telefone"role="alert"></span></div><divclass="campo"><labelfor="nascimento">Data de nascimento *</label><inputtype="text"id="nascimento"name="nascimento"inputmode="numeric"maxlength="10"placeholder="dd/mm/aaaa"required><spanclass="erro"id="erro-nascimento"role="alert"></span></div></fieldset><fieldset><legend>Participação</legend><divclass="campo"><labelfor="curso">Curso ou vínculo *</label><selectid="curso"name="curso"required><optionvalue="">Selecione…</option><optionvalue="si">Sistemas de Informação</option><optionvalue="ads">Análise e Desenvolvimento de Sistemas</option><optionvalue="eng">Engenharias</option><optionvalue="outro">Outro curso</option><optionvalue="externo">Comunidade externa</option></select><spanclass="erro"id="erro-curso"role="alert"></span></div><divclass="campo"><spanclass="rotulo-grupo"id="rotulo-atividades">Atividades de interesse *</span><divclass="grupo-caixas"role="group"aria-labelledby="rotulo-atividades"><label><inputtype="checkbox"name="atividades"value="git"> Minicurso de Git</label><label><inputtype="checkbox"name="atividades"value="acessibilidade"> Minicurso de acessibilidade</label><label><inputtype="checkbox"name="atividades"value="maratona"> Maratona de programação</label><label><inputtype="checkbox"name="atividades"value="palestras"> Somente palestras</label></div><spanclass="erro"id="erro-atividades"role="alert"></span></div><divclass="campo"><labelclass="linha"><inputtype="checkbox"id="termos"name="termos"required>
Li e aceito o regulamento do evento *</label><spanclass="erro"id="erro-termos"role="alert"></span></div></fieldset><divclass="acoes-formulario"><buttontype="submit"class="botao">Enviar inscrição</button><buttontype="button"class="botao botao--contorno"id="limpar-rascunho">Limpar rascunho</button></div></form></main>
Quatro decisões que valem nota: novalidate no formulário (as mensagens são nossas), inputmode nos campos numéricos (o celular abre o teclado certo sem mudar o type, como na Aula 04), o grupo de checkboxes dentro de um role="group" com aria-labelledby — assim o leitor de tela anuncia "Atividades de interesse, grupo" antes das opções — e a classe .aviso-formulario no parágrafo de resposta do envio. Ela não é a .aviso da Aula 11: aquela é a caixa vermelha de "Últimas vagas!", com .aviso:empty { display: none }, e continua sendo usada pelo #aviso-vagas logo acima. Dois componentes diferentes, dois nomes diferentes — reaproveitar o nome faria as duas regras brigarem no mesmo estilo.css.
O js/inscricao.js já existe desde a Aula 10 e cresceu na Aula 11: é ele que calcula as vagas restantes, o percentual de ocupação, o aviso "Últimas vagas!" e o valor da taxa. Esse bloco não sai — ele continua no topo do arquivo, sob o comentário // ===== VAGAS (Aulas 10 e 11) =====, e tudo o que você escreve hoje vem depois dele.
Este arquivo usa a função debounce para salvar o rascunho; ela é declarada no js/app.js no Passo 4, que toda página já carrega antes deste script.
js/inscricao.js (topo do arquivo — o que já existe, apenas identificado com um comentário)
JavaScript
// js/inscricao.js — vagas (Aulas 10 e 11) e validação do formulário (Aula 14)// ===== VAGAS (Aulas 10 e 11) =====// As constantes VAGAS_TOTAIS e INSCRITOS, o cálculo do percentual, o aviso// de "Últimas vagas!" e o valor da taxa continuam exatamente como estavam.// Nada abaixo depende deles, e nada deles depende do que vem abaixo.
js/inscricao.js (acrescente a partir daqui)
JavaScript
// ===== ESTADO =====constCHAVE_RASCUNHO="inscricao:rascunho";constCAMPOS_DO_RASCUNHO=["nome","email","curso"];// sem CPF e sem telefone, de propósito// ===== ELEMENTOS =====constels={form:document.querySelector("#form-inscricao"),aviso:document.querySelector("#aviso-formulario"),limpar:document.querySelector("#limpar-rascunho"),};// ===== PADRÕES =====constPADROES={telefone:/^\(?\d{2}\)?\s?9?\d{4}-?\d{4}$/,email:/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/,data:/^(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/,};// ===== VALIDADORES (funções puras) =====functionvalidarNomeCompleto(valor){constpartes=valor.trim().split(/\s+/);if(valor.trim()==="")return"O nome completo é obrigatório.";if(partes.length<2)return"Informe nome e sobrenome.";if(partes.some((parte)=>parte.length<2)){return"Cada parte do nome deve ter ao menos 2 letras.";}return"";}functionvalidarEmail(valor){constv=valor.trim();if(v==="")return"O e-mail é obrigatório.";if(!PADROES.email.test(v))return"Informe um e-mail no formato nome@dominio.br.";return"";}functionvalidarTelefone(valor){constv=valor.trim();if(v==="")return"O telefone é obrigatório.";if(!PADROES.telefone.test(v))return"Informe o telefone com DDD, como (66) 99999-9999.";return"";}functionvalidarCPF(cpf){constnumeros=cpf.replace(/\D/g,"");if(numeros==="")return"O CPF é obrigatório.";if(numeros.length!==11)return"O CPF deve ter 11 dígitos.";if(/^(\d)\1{10}$/.test(numeros))return"CPF inválido.";constcalcularDigito=(base,pesoInicial)=>{letsoma=0;for(leti=0;i<base.length;i++){soma=soma+Number(base[i])*(pesoInicial-i);}constresto=(soma*10)%11;returnresto===10?0:resto;};constprimeiro=calcularDigito(numeros.slice(0,9),10);constsegundo=calcularDigito(numeros.slice(0,10),11);if(primeiro!==Number(numeros[9])||segundo!==Number(numeros[10])){return"CPF inválido. Confira os números digitados.";}return"";}functioncalcularIdade(dataTexto){constpartes=dataTexto.trim().split("/");if(partes.length!==3)returnnull;constdia=Number(partes[0]);constmes=Number(partes[1]);constano=Number(partes[2]);constdata=newDate(ano,mes-1,dia);constexiste=data.getFullYear()===ano&&data.getMonth()===mes-1&&data.getDate()===dia;if(!existe)returnnull;consthoje=newDate();letanos=hoje.getFullYear()-ano;constjaFez=hoje.getMonth()>mes-1||(hoje.getMonth()===mes-1&&hoje.getDate()>=dia);if(!jaFez)anos=anos-1;returnanos;}functionvalidarNascimento(valor){constv=valor.trim();if(v==="")return"A data de nascimento é obrigatória.";if(!PADROES.data.test(v))return"Use o formato dd/mm/aaaa.";constanos=calcularIdade(v);if(anos===null)return"Essa data não existe no calendário.";if(anos<0)return"A data de nascimento não pode estar no futuro.";if(anos<16)return"É necessário ter ao menos 16 anos para se inscrever.";if(anos>120)return"Confira o ano digitado.";return"";}functionvalidarCurso(valor){returnvalor===""?"Escolha o seu curso ou vínculo.":"";}functionvalidarAtividades(){constmarcadas=els.form.querySelectorAll("input[name='atividades']:checked");returnmarcadas.length===0?"Escolha ao menos uma atividade.":"";}functionvalidarTermos(marcado){returnmarcado?"":"É preciso aceitar o regulamento para se inscrever.";}// ===== MÁSCARAS =====functionmascaraCPF(valor){returnvalor.replace(/\D/g,"").slice(0,11).replace(/(\d{3})(\d)/,"$1.$2").replace(/(\d{3})(\d)/,"$1.$2").replace(/(\d{3})(\d{1,2})$/,"$1-$2");}functionmascaraTelefone(valor){returnvalor.replace(/\D/g,"").slice(0,11).replace(/(\d{2})(\d)/,"($1) $2").replace(/(\d{5})(\d)/,"$1-$2");}functionmascaraData(valor){returnvalor.replace(/\D/g,"").slice(0,8).replace(/(\d{2})(\d)/,"$1/$2").replace(/(\d{2})(\d)/,"$1/$2");}// ===== EXIBIÇÃO =====functionmostrarErro(campo,mensagem){constalvo=document.querySelector(`#erro-${campo.id}`);if(!alvo)return;if(mensagem){campo.classList.add("invalido");campo.classList.remove("valido");campo.setAttribute("aria-invalid","true");campo.setAttribute("aria-describedby",`erro-${campo.id}`);alvo.textContent=mensagem;}else{campo.classList.remove("invalido");campo.classList.add("valido");campo.setAttribute("aria-invalid","false");campo.removeAttribute("aria-describedby");alvo.textContent="";}}functionmostrarAviso(texto,tipo){els.aviso.textContent=texto;els.aviso.className=texto?`aviso-formulario ${tipo}`:"aviso-formulario";}// ===== ORQUESTRAÇÃO =====functionvalidarCampo(campo){letmensagem="";switch(campo.id){case"nome":mensagem=validarNomeCompleto(campo.value);break;case"email":mensagem=validarEmail(campo.value);break;case"cpf":mensagem=validarCPF(campo.value);break;case"telefone":mensagem=validarTelefone(campo.value);break;case"nascimento":mensagem=validarNascimento(campo.value);break;case"curso":mensagem=validarCurso(campo.value);break;case"termos":mensagem=validarTermos(campo.checked);break;default:mensagem="";}if(campo.name==="atividades"){constgrupo=document.querySelector("#erro-atividades");grupo.textContent=validarAtividades();returngrupo.textContent==="";}mostrarErro(campo,mensagem);returnmensagem==="";}functioncamposValidaveis(){return[...els.form.querySelectorAll("#nome, #email, #cpf, #telefone, #nascimento, #curso, #termos")];}// ===== RASCUNHO (localStorage) =====functionsalvarRascunho(){constdados={};CAMPOS_DO_RASCUNHO.forEach((nome)=>{dados[nome]=els.form.elements[nome].value;});localStorage.setItem(CHAVE_RASCUNHO,JSON.stringify(dados));}functionrestaurarRascunho(){constbruto=localStorage.getItem(CHAVE_RASCUNHO);if(!bruto)return;constdados=JSON.parse(bruto);CAMPOS_DO_RASCUNHO.forEach((nome)=>{if(dados[nome])els.form.elements[nome].value=dados[nome];});mostrarAviso("Recuperamos o rascunho que você tinha começado.","sucesso");}functionlimparRascunho(){localStorage.removeItem(CHAVE_RASCUNHO);els.form.reset();camposValidaveis().forEach((campo)=>{campo.classList.remove("valido","invalido");campo.removeAttribute("aria-invalid");mostrarErro(campo,"");});document.querySelector("#erro-atividades").textContent="";mostrarAviso("Rascunho apagado.","sucesso");}// ===== EVENTOS =====functionregistrarEventos(){// Máscarasdocument.querySelector("#cpf").addEventListener("input",(e)=>{e.target.value=mascaraCPF(e.target.value);});document.querySelector("#telefone").addEventListener("input",(e)=>{e.target.value=mascaraTelefone(e.target.value);});document.querySelector("#nascimento").addEventListener("input",(e)=>{e.target.value=mascaraData(e.target.value);});// Valide no blur; depois do primeiro erro, valide também no inputcamposValidaveis().forEach((campo)=>{campo.addEventListener("blur",()=>{validarCampo(campo);campo.dataset.tocado="true";});campo.addEventListener("input",()=>{if(campo.dataset.tocado==="true")validarCampo(campo);});});// Grupo de checkboxes: um ouvinte só, por delegaçãoels.form.addEventListener("change",(e)=>{if(e.target.name==="atividades")validarCampo(e.target);});// Rascunho: salva 500 ms depois de o usuário parar de digitarels.form.addEventListener("input",debounce(salvarRascunho,500));els.limpar.addEventListener("click",limparRascunho);// Envioels.form.addEventListener("submit",(e)=>{e.preventDefault();letprimeiroInvalido=null;camposValidaveis().forEach((campo)=>{constok=validarCampo(campo);campo.dataset.tocado="true";if(!ok&&!primeiroInvalido)primeiroInvalido=campo;});consterroAtividades=validarAtividades();document.querySelector("#erro-atividades").textContent=erroAtividades;if(primeiroInvalido||erroAtividades){mostrarAviso("Confira os campos destacados antes de enviar.","falha");constalvo=primeiroInvalido||document.querySelector("input[name='atividades']");alvo.focus();alvo.scrollIntoView({behavior:"smooth",block:"center"});return;}constdados=Object.fromEntries(newFormData(els.form).entries());dados.atividades=newFormData(els.form).getAll("atividades");dados.cpf=dados.cpf.replace(/\D/g,"");// sem máscara, como o servidor esperadados.telefone=dados.telefone.replace(/\D/g,"");console.log("Inscrição válida:",dados);localStorage.removeItem(CHAVE_RASCUNHO);els.form.reset();camposValidaveis().forEach((campo)=>campo.classList.remove("valido","invalido"));mostrarAviso(`Inscrição de ${dados.nome} registrada! Você receberá a confirmação em ${dados.email}.`,"sucesso");});}// ===== INICIALIZAÇÃO =====functioniniciar(){if(!els.form)return;registrarEventos();restaurarRascunho();}iniciar();
Passo 4 — debounce disponível em todas as páginas¶
O debounce é usado pelo rascunho e, no próximo passo, pela busca. Como todas as páginas carregam js/app.jsantes do script específico (os dois com defer, e defer preserva a ordem das tags), basta declarar a função lá uma vez. Confira que o <script src="js/app.js" defer> está mesmo no <head> das cinco páginas — inclusive em programacao.html, que vai usar o debounce no Passo 7. Sem ele, a busca morre no primeiro caractere digitado com Uncaught ReferenceError: debounce is not defined.
js/app.js — acrescente ao fim
JavaScript
// Utilitário compartilhado: adia a execução até os disparos pararem por X ms.// Como este arquivo é carregado antes dos scripts de página, a função fica// disponível para inscricao.js e programacao.js.functiondebounce(fn,atraso=300){lettemporizador;returnfunction(...args){clearTimeout(temporizador);temporizador=setTimeout(()=>fn.apply(this,args),atraso);};}
Não há arquivo novo aqui: o js/dados.js da Aula 12 já tem tudo de que a consulta precisa. Abra-o e confirme os três nomes que os próximos passos vão usar:
O campo do horário chama hora, não horario. Um p.horario devolve undefined, e undefined.localeCompare(…) derruba a ordenação inteira com TypeError.
A palestra não guarda o nome de quem apresenta, só o palestranteId. Para buscar por nome, você precisa cruzar as duas listas — exatamente o que o relatório 7 da Aula 12 fazia com find. É a mesma ideia de chave estrangeira que você verá no Nível 2.
Se você sentir vontade de "só ajustar um campinho" no dados.js para simplificar o filtro, resista: o js/relatorios.js (Aula 12) e o js/palestrantes.js (Aula 13) leem esse mesmo arquivo e quebram junto. Adaptar o consumidor ao dado é barato; adaptar o dado a um consumidor é como se perde a fonte única.
Passo 6 — a interface de consulta em programacao.html¶
Troque os cartões escritos à mão (Aula 07) por um contêiner vazio e os controles de consulta. Antes, o <head> com os cinco scripts:
programacao.html — <head> completo
HTML
<head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metaname="description"content="Programação completa da Semana Acadêmica de Sistemas de Informação."><metaname="author"content="Semana Acadêmica de Sistemas de Informação"><title>Programação — Semana Acadêmica de Sistemas de Informação</title><linkrel="stylesheet"href="css/estilo.css"><scriptsrc="js/menu.js"defer></script><scriptsrc="js/dados.js"defer></script><scriptsrc="js/app.js"defer></script><scriptsrc="js/relatorios.js"defer></script><scriptsrc="js/programacao.js"defer></script></head>
O relatorios.js da Aula 12 continua aqui: ele só escreve no Console e não atrapalha nada. E repare que js/app.js vem antes de js/programacao.js: é dele que sai o debounce do Passo 4.
programacao.html — conteúdo do <main>
HTML
<mainid="conteudo"tabindex="-1"class="container"><h1>Programação</h1><p>Três dias de palestras, minicursos e maratona. Use a busca e os filtros para achar sua atividade.</p><formclass="consulta"id="consulta-programacao"role="search"><divclass="campo"><labelfor="busca">Buscar por título ou palestrante</label><inputtype="search"id="busca"name="busca"placeholder="Ex.: acessibilidade"autocomplete="off"></div><divclass="campo"><labelfor="area">Área</label><selectid="area"name="area"><optionvalue="todas">Todas as áreas</option><optionvalue="web">Desenvolvimento Web</option><optionvalue="dados">Ciência de Dados</option><optionvalue="seguranca">Segurança</option><optionvalue="ia">Inteligência Artificial</option></select></div><divclass="campo"><labelfor="ordenacao">Ordenar por</label><selectid="ordenacao"name="ordenacao"><optionvalue="hora">Dia e horário</option><optionvalue="titulo">Título (A–Z)</option><optionvalue="vagas">Vagas restantes</option></select></div><buttontype="button"class="botao botao--contorno"id="limpar-consulta">Limpar filtros</button></form><pid="contador-programacao"class="contador"role="status"aria-live="polite"></p><ulid="lista-programacao"class="cartoes"></ul></main>
O <form role="search"> não envia nada: ele existe para agrupar os controles semanticamente (leitores de tela anunciam "busca") e para o reset() do botão "Limpar filtros" funcionar de graça. Os cinco <script> precisam estar na ordem do <head> acima: dados.js declara palestras e palestrantes, app.js declara debounce, e só então programacao.js usa os três.
Ele consome palestras, palestrantes e nomesDasAreas do js/dados.js — sem redeclarar nada — e debounce do js/app.js. Os nomes das constantes e funções levam o sufixo Programacao pelo mesmo motivo da Aula 13: scripts sem type="module" dividem o escopo global, e um els ou um renderizar genérico colide com o do arquivo vizinho no dia em que as duas páginas carregarem os dois.
js/programacao.js
JavaScript
// js/programacao.js — busca, filtro e ordenação da programação.// Depende de js/dados.js (palestras, palestrantes, nomesDasAreas) e de// js/app.js (debounce), ambos carregados antes deste arquivo.// ===== ESTADO =====lettermoBusca="";letareaAtualProgramacao="todas";letordenacaoAtual="hora";// ===== ELEMENTOS =====constelementosProgramacao={form:document.querySelector("#consulta-programacao"),busca:document.querySelector("#busca"),area:document.querySelector("#area"),ordenacao:document.querySelector("#ordenacao"),limpar:document.querySelector("#limpar-consulta"),lista:document.querySelector("#lista-programacao"),contador:document.querySelector("#contador-programacao"),};// ===== FUNÇÕES DE DADOS =====functionnormalizar(texto){returntexto.normalize("NFD").replace(/[̀-ͯ]/g,"").toLowerCase().trim();}/** * Cruza a palestra com o array de palestrantes pelo palestranteId. * É a mesma junção do relatório 7 da Aula 12: `find` + `?.` + `??`. * @param {Object} palestra - item de `palestras` * @returns {string} o nome de quem apresenta, ou "A definir" */functionnomeDoPalestrante(palestra){constpessoa=palestrantes.find((p)=>p.id===palestra.palestranteId);returnpessoa?.nome??"A definir";}functionvagasRestantes(palestra){returnpalestra.vagas-palestra.inscritos;}functionobterPalestrasVisiveis(){letresultado=[...palestras];// cópia: sort() altera o array originalif(termoBusca){consttermo=normalizar(termoBusca);resultado=resultado.filter((p)=>normalizar(p.titulo).includes(termo)||normalizar(nomeDoPalestrante(p)).includes(termo)||normalizar(p.local).includes(termo));}if(areaAtualProgramacao!=="todas"){resultado=resultado.filter((p)=>p.area===areaAtualProgramacao);}constordenadores={hora:(a,b)=>a.dia-b.dia||a.hora.localeCompare(b.hora),titulo:(a,b)=>a.titulo.localeCompare(b.titulo,"pt-BR"),vagas:(a,b)=>vagasRestantes(b)-vagasRestantes(a),};resultado.sort(ordenadores[ordenacaoAtual]);returnresultado;}// ===== RENDERIZAÇÃO =====functioncriarCartaoDePalestra(palestra){constitem=document.createElement("li");item.classList.add("cartao");item.dataset.id=palestra.id;constrestantes=vagasRestantes(palestra);if(restantes===0){constselo=document.createElement("span");selo.classList.add("cartao__selo");selo.textContent="Esgotado";item.appendChild(selo);}consttitulo=document.createElement("h2");titulo.textContent=palestra.titulo;constmeta=document.createElement("p");meta.classList.add("cartao__meta");meta.textContent=`Dia ${palestra.dia} · ${palestra.hora} · ${palestra.local}`;constquem=document.createElement("p");quem.textContent=nomeDoPalestrante(palestra);constetiqueta=document.createElement("span");etiqueta.classList.add("etiqueta");etiqueta.textContent=nomesDasAreas[palestra.area];constvagas=document.createElement("p");vagas.classList.add("cartao__vagas");vagas.textContent=restantes===0?"Sem vagas — entre na lista de espera":`${restantes} vagas restantes`;item.append(titulo,meta,quem,etiqueta,vagas);returnitem;}functionrenderizarProgramacao(){constvisiveis=obterPalestrasVisiveis();elementosProgramacao.lista.innerHTML="";if(visiveis.length===0){constvazio=document.createElement("li");vazio.classList.add("vazio");vazio.textContent=termoBusca?`Nenhuma atividade encontrada para "${termoBusca}". Tente outro termo ou limpe os filtros.`:"Nenhuma atividade nesta área. Escolha outra ou limpe os filtros.";elementosProgramacao.lista.appendChild(vazio);}else{constfragmento=document.createDocumentFragment();visiveis.forEach((p)=>fragmento.appendChild(criarCartaoDePalestra(p)));elementosProgramacao.lista.appendChild(fragmento);}elementosProgramacao.contador.textContent=visiveis.length===1?"1 atividade encontrada":`${visiveis.length} atividades encontradas`;}// ===== EVENTOS =====functionregistrarEventosDaProgramacao(){elementosProgramacao.busca.addEventListener("input",debounce((e)=>{termoBusca=e.target.value;renderizarProgramacao();},300));elementosProgramacao.area.addEventListener("change",(e)=>{areaAtualProgramacao=e.target.value;renderizarProgramacao();});elementosProgramacao.ordenacao.addEventListener("change",(e)=>{ordenacaoAtual=e.target.value;renderizarProgramacao();});elementosProgramacao.limpar.addEventListener("click",()=>{elementosProgramacao.form.reset();termoBusca="";areaAtualProgramacao="todas";ordenacaoAtual="hora";renderizarProgramacao();elementosProgramacao.busca.focus();});}// ===== INICIALIZAÇÃO =====functioniniciarProgramacao(){if(!elementosProgramacao.lista)return;registrarEventosDaProgramacao();renderizarProgramacao();}iniciarProgramacao();
Abra inscricao.html com o Live Server. Clique no campo "Nome completo", saia sem digitar: aparece "O nome completo é obrigatório." em vermelho, com o símbolo de alerta, e a borda fica vermelha. Digite "Ana": a mensagem muda para "Informe nome e sobrenome." enquanto você digita — porque o campo já foi tocado.
Digite qualquer sequência de 11 dígitos no CPF: a máscara aplica pontos e traço sozinha. Teste 111.111.111-11 (rejeitado por repetição) e 123.456.789-00 (rejeitado pelos dígitos verificadores). Um CPF válido de verdade deixa a borda verde.
No campo de nascimento, digite 31/02/2000: "Essa data não existe no calendário." Digite uma data que resulte em menos de 16 anos: "É necessário ter ao menos 16 anos para se inscrever."
Clique em "Enviar inscrição" com o formulário vazio: todos os erros aparecem de uma vez, o foco vai para o primeiro campo inválido, a página rola até ele e o aviso do topo diz "Confira os campos destacados antes de enviar."
Preencha tudo corretamente e envie: o console mostra o objeto com cpf e telefonesem máscara, atividades como array, e a página exibe a mensagem verde de sucesso.
Preencha só o nome e o e-mail e recarregue a página (F5dronize a entrada do validador (por exemplo, sempre o elemento) para não precisar de exceções.
Compare o resultado com o criarValidador do exercício C1: você acabou de construir metade dele.
): os valores voltam e o aviso diz que o rascunho foi recuperado. Confira no DevTools → Application → Local Storage que a chave inscricao:rascunhonão contém CPF nem telefone. Clique em "Limpar rascunho" e recarregue: o formulário volta vazio.
Abra programacao.html: os doze cartões do dados.js aparecem ordenados por dia e horário, e o contador diz "12 atividades encontradas". Cada cartão mostra o nome de quem apresenta — resolvido pelo palestranteId, não digitado.
Digite seguranca (sem cedilha e sem acento) na busca: aparece "Segurança em aplicações web: dez erros comuns". Digite carla: aparecem as duas atividades de Carla Mendes, provando que a busca cruza as duas listas. Digite xyz: a mensagem de estado vazio explica o que fazer.
Mude "Ordenar por" para "Vagas restantes": o minicurso de Git e o de redes neurais (ambos esgotados) vão para o fim e ganham o selo "Esgotado". Mude para "Título (A–Z)" e confira que os acentos ficam na ordem correta. No filtro de área, "Segurança" deixa duas atividades e "Inteligência Artificial", outras duas.
Percorra a página inteira com Tabcê já fez
Tags: acessibilidade, formularios, investigacao, projeto
Um formulário pode passar em todos os testes automáticos e ainda ser impossível de preencher sem o mouse. Nesta missão você vai auditar o formulário do seu projeto autoral como um usuário que não enxerga a tela, corrigir o que encontrar e provar o resultado. Isso entra bem no Marco 3 da unidade.
Critérios de pronto
Um vídeo de até três minutos preenchendo e enviando o formulário só com o teclado, com o monitor desligado ou os olhos fechados, usando o leitor de tela do sistema (NVDA no Windows, Orca no Linux, VoiceOver no macOS).
O formulário tem um resumo de erros no topo, ligado por link a cada campo com problema, que recebe foco ao falhar o envio.
Todo campo tem rótulo associado, texto de ajuda ligado por aria-describedby e mensagem por role="alert".
A aba Lighthouse do DevTools marca 100 em Acessibilidade na página do formulário, e a captura está no relatório.
Um arquivo acessibilidade.md lista os problemas encontrados, a correção aplicada e — o item mais importante — dois problemas que o Lighthouse não apontou e que só a navegação por teclado revelou.
: todo campo tem foco visível, o botão "Limpar filtros" devolve o foco à busca, e o console está sem nenhum erro em vermelho nas cinco páginas — em especial, nenhum ReferenceError: debounce is not defined e nenhum Identifier … has already been declared.
11. Ainda em inscricao.html, confira que a seção de vagas continua no topo, com o número calculado e o aviso "Últimas vagas!" das Aulas 10 e 11 — e que ele é visualmente diferente do aviso verde/vermelho do envio do formulário.
A1. Por que event.preventDefault() é obrigatório no ouvinte de submit quando o formulário é processado em JavaScript? O que acontece exatamente sem ele?
A2. Cite as três formas de ler os valores de um formulário e uma vantagem de cada uma.
A3. O que input.value devolve em um campo type="number"? Escreva a conversão correta e diga o que acontece se o campo estiver vazio.
A4. Diferencie os eventos input, change e blur para fins de validação. Qual deles não borbulha, e qual é a alternativa borbulhante?
A5. Para que serve o atributo novalidate no <form>? Por que mantemos required e pattern no HTML mesmo assim?
A6. O que fazem campo.checkValidity(), campo.reportValidity() e campo.setCustomValidity()? Qual armadilha o terceiro traz?
A7. Por que sinalizar erro apenas pela cor da borda é um problema de acessibilidade? Cite duas formas de resolver.
A8. Para que servem role="alert", aria-invalid e aria-describedby na exibição de erros? O que cada um muda para quem usa leitor de tela?
A9. Reescreva como mensagem útil: "Erro no campo 3". Invente o contexto que preferir e justifique a sua escolha em uma linha.
A10. Por que a validação no navegador não substitui a validação no servidor? Descreva, em duas frases, como alguém burlaria a sua validação.
A11. O que faz Object.fromEntries(new FormData(form).entries())? Cite duas limitações do FormData.
A12. Por que a máscara precisa ser removida antes de validar e de enviar o dado?
A13. O que fazem as flags g, i e m de uma expressão regular?
A14. Qual a diferença entre \d, \w e \s? E entre \d e \D?
A15. O que casa cada padrão: /a+/, /a*/, /a?/, /a{2,4}/? Dê um exemplo de string aceita e uma rejeitada para cada.
A16. Diferencie quantificador guloso de preguiçoso, com um exemplo próprio.
A17. Qual a diferença entre /\d{3}/ e /^\d{3}$/? Por que isso é crítico em validação?
A18. O que faz \b? Dê um exemplo em que a presença dele muda o resultado.
A19. Diferencie (abc) de (?:abc). Quando você usaria o segundo?
A20. O que faz o lookahead (?=…)? Escreva uma regex que aceite apenas strings que contenham ao menos um dígito, em qualquer posição.
A21. Escreva a regex para: (a) somente dígitos; (b) somente letras, com acento; (c) alfanumérico de 6 a 12 caracteres; (d) começa com maiúscula; (e) termina com .pdf.
A22. Diferencie test(), match() e matchAll(). Qual deles exige a flag g?
A23. Escreva o replace que converta "07/05/1998" em "1998-05-07", usando grupos capturados.
A24. Por que a validação de e-mail por regex não deve ser rigorosa demais? Qual é a estratégia profissional recomendada?
A25. O que a regex não consegue validar em um CPF? E em uma data?
A26. O que é ReDoS e qual padrão de escrita costuma causá-lo?
A27. Por que [...palestras] aparece antes do sort() em obterPalestrasVisiveis()? Descreva o bug que surge sem essa cópia.
A28. O que localeCompare(b, "pt-BR") resolve que o sort() puro não resolve? Dê um exemplo com nomes acentuados.
A29. Explique, linha a linha, o que a função normalizar() faz com a string "Segurança".
A30. Por que o <p> do contador de resultados precisa existir vazio no HTML em vez de ser criado pelo JavaScript?
B1. Implemente um formulário de login com validação de e-mail, senha de no mínimo 8 caracteres, botão "mostrar senha" que alterna o type do campo, bloqueio após três tentativas com contagem regressiva de 30 segundos e mensagens acessíveis por campo.
Resultado esperado: as três tentativas erradas desabilitam o botão de envio e mostram "Tente novamente em 30, 29, 28… segundos"; o botão "mostrar senha" tem aria-pressed sincronizado; nenhum erro é sinalizado só por cor.
Dica
Guarde o número de tentativas em uma variável de estado e use setInterval para a contagem, limpando com clearInterval ao chegar a zero. Para alternar o tipo do campo: campo.type = campo.type === "password" ? "text" : "password".
B2. Crie um medidor de força de senha com cinco critérios (comprimento ≥ 8, maiúscula, minúscula, número e caractere especial), exibindo a lista de critérios que ficam verdes conforme atendidos e uma barra de progresso colorida.
Resultado esperado: cada critério é um <li> com um ícone que muda de estado; a barra usa <meter> ou uma div com largura proporcional; o texto do nível ("Fraca", "Razoável", "Forte") é anunciado por uma região aria-live.
Dica
Uma regex por critério: /[A-Z]/, /[a-z]/, /\d/, /[^A-Za-z0-9]/. Some um ponto por critério atendido e penalize repetições com /(.)\1{2,}/. Nunca guarde a senha em localStorage.
B3. Implemente a validação de data de nascimento como um componente independente: o campo calcula a idade exata em tempo real e a exibe ao lado ("Você tem 19 anos"), rejeita datas futuras, datas que não existem no calendário e idade abaixo de 16 anos.
Resultado esperado: digitar 29/02/2001 mostra "Essa data não existe no calendário" (2001 não é bissexto); digitar 29/02/2000 é aceito; a idade aparece assim que a data fica completa e válida.
Dica
Reaproveite calcularIdade da Mão na massa e teste a existência da data reconstruindo dia, mês e ano do objeto Date, como explica a seção 4.1. Para o ano bissexto você não precisa de regra própria: o Date já sabe.
B4. Construa um formulário de agendamento de sala com: data (não pode ser passada nem fim de semana), horário em um <select> gerado dinamicamente de 30 em 30 minutos entre 8h e 18h, duração em horas e observações, exibindo um resumo textual do agendamento antes da confirmação.
Resultado esperado: as opções do <select> são criadas por um laço, não escritas à mão; escolher um sábado mostra "A sala não é liberada aos fins de semana"; o resumo é atualizado a cada alteração e lido por uma região aria-live.
Dica
data.getDay() devolve 0 para domingo e 6 para sábado. Para gerar os horários, um for de 8 a 17 com dois push por hora (:00 e :30) resolve. Use String(hora).padStart(2, "0") para formatar.
B5. Pegue o formulário de contato de contato.html (Unidade 1), que hoje só tem validação nativa, e adicione validação completa em JavaScript sem alterar o HTML além de acrescentar os <span> de erro. Documente cada regra implementada em um comentário no topo do arquivo.
Resultado esperado: um arquivo js/contato.js com validadores puros e reaproveitáveis; o HTML mantém required e type, e ganha apenas os <span class="erro" role="alert">; o console fica limpo.
Dica
Comece copiando a estrutura de js/inscricao.js e apagando o que não se aplica. As funções mostrarErro, validarEmail e validarNomeCompleto saem de lá sem alteração — esse é o ponto de escrever validadores puros.
B6. Escreva e teste expressões regulares para validar: matrícula acadêmica (quatro dígitos de ano seguidos de cinco dígitos), placa Mercosul, código de rastreio dos Correios (duas letras, nove dígitos e BR), IPv4 e cartão com 16 dígitos com ou sem espaços e hífens.
Resultado esperado: uma página exercicios/aula14/regex.html com um campo por padrão, validação em tempo real e, ao lado de cada campo, três exemplos válidos e três inválidos que você testou.
Dica
Ancore tudo com ^ e $. Para o IPv4, cada octeto vai de 0 a 255: (25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d) repetido quatro vezes, separado por \.. Teste cada expressão no https://regex101.com antes de colar no código.
B7. Escreva gerarSlug(titulo) que converta "Minicurso: Acessibilidade na Prática — Nível 1!" em "minicurso-acessibilidade-na-pratica-nivel-1", tratando acentos, símbolos, espaços múltiplos e hífens nas pontas.
Resultado esperado: uma função pura com cinco casos de teste no console, incluindo uma string só com símbolos (que deve devolver string vazia) e uma com espaços nas pontas.
Dica
O caminho é: normalizar (seção 9.2) para tirar acentos, replace(/[^a-z0-9]+/g, "-") para trocar tudo que não é alfanumérico por hífen e replace(/^-+|-+$/g, "") para limpar as pontas.
B8. Implemente um destacador de busca: dado o termo digitado e o texto de cada cartão da programação, envolva todas as ocorrências em <mark>, ignorando maiúsculas e acentos, sem quebrar quando o termo contiver caracteres especiais de regex.
Resultado esperado: buscar web destaca "web" em todos os títulos; buscar ( não gera erro no console; o destaque some ao limpar a busca.
Dica
Escape o termo antes de montar a regex: termo.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"). Como o destaque exige HTML, este é o único lugar da aula em que innerHTML é aceitável — e só porque o texto vem dos seus dados, não do usuário. Sanitize antes.
B9. Acrescente paginação à programação: quatro itens por página, botões "anterior" e "próxima", indicação "Página 2 de 3" e volta automática à página 1 sempre que a busca ou o filtro mudarem.
Resultado esperado: a paginação respeita o resultado filtrado (não o array completo); os botões ficam desabilitados nos extremos; o foco vai para o topo da lista ao trocar de página.
Dica
Guarde paginaAtual no estado e use lista.slice(inicio, inicio + POR_PAGINA). O total de páginas é Math.ceil(lista.length / POR_PAGINA) — e nunca deixe paginaAtual maior que ele depois de um filtro.
C1. Biblioteca de validação configurável. Escreva um js/validador.js genérico, dirigido por um objeto de regras, que funcione em qualquer formulário do seu projeto:
Deve suportar regras predefinidas por tipo, regras personalizadas por função, mensagens customizadas, validação em blur/input/submit, exibição automática das mensagens e um método validador.estaValido(). Demonstre a mesma biblioteca funcionando em três formulários diferentes do seu projeto.
Dica
criarValidador devolve um objeto com métodos — é o padrão de módulo que você reencontrará no Nível 2. Guarde as regras em uma constante e percorra Object.entries(regras) para registrar os ouvintes. Cada tipo é uma entrada em um objeto VALIDADORES_POR_TIPO, o que evita uma cadeia gigante de if.
C2. Painel de consulta completo. Sobre a listagem principal do seu projeto autoral, implemente ao mesmo tempo: busca com debounce e normalização de acentos, dois filtros combináveis (categoria e faixa de valor ou data), ordenação por três critérios, paginação, contagem de resultados anunciada, estado vazio com sugestão útil e persistência da consulta no localStorage (ao recarregar, os filtros voltam como estavam).
Dica
Mantenha um objeto de estado (const consulta = { termo: "", categoria: "todas", ordem: "nome", pagina: 1 }) e uma função aplicarConsulta() que lê esse objeto, filtra, ordena, pagina e renderiza. Salvar e restaurar vira JSON.stringify(consulta) e Object.assign(consulta, JSON.parse(bruto)).
O arquivo abaixo deveria validar uma inscrição, mas aceita nome vazio, e-mail sem arroba e CPF com uma letra no meio — e, quando reclama, ninguém que use leitor de tela fica sabendo. São seis defeitos: quatro de lógica e dois de acessibilidade. Encontre todos usando só o DevTools e o console, sem reescrever o arquivo do zero.
Um espaço em branco (" ") no campo nome passa a ser rejeitado.
"ana@teste" e "ana @teste.br" passam a ser rejeitados, e "ana@teste.br" continua sendo aceito.
O envio passa pelo evento submit do formulário e funciona com Enter em qualquer campo.
Cada mensagem é anunciada por leitor de tela e o campo com erro fica marcado com aria-invalid.
As mensagens dizem o que fazer, não apenas "Inválido".
Um comentário de uma linha acima de cada correção explica o defeito que ela resolve.
Pistas
Compare a regex de e-mail com a da seção 5.10: o que falta nas pontas dela? E o que \w deixa passar?
valor === "" e valor.trim() === "" não são a mesma pergunta.
Releia a Aula 13 sobre por que ouvir click no botão não é o mesmo que ouvir submit no formulário.
innerHTML para texto puro é desnecessário e arriscado; e cor não é mensagem. Volte à seção 7 e liste os cinco requisitos.
⭐⭐
⭐⭐ Busca que ignora acentos, plurais e a ordem das palavras¶
javascriptinvestigacaoperformancedom
Digite "minicurso git" na busca da programação: nada aparece, porque nenhum título contém exatamente essa sequência. Digite "seguranças": também nada. Usuários não digitam do jeito que os seus dados foram escritos — e a sua busca não pode depender disso. Torne a busca tolerante: ela deve encontrar o item quando todas as palavras digitadas aparecerem no texto, em qualquer ordem, ignorando acentos, maiúsculas e o "s" final.
Critérios de pronto
"minicurso git" encontra "Minicurso: Git e GitHub do zero ao primeiro pull request".
"git minicurso" encontra o mesmo item (a ordem das palavras não importa).
"seguranças" encontra "segurança em aplicações web".
A tolerância está isolada em uma função pura, testada no console com pelo menos seis casos.
A busca continua respondendo instantaneamente com 500 itens no array (meça com console.time antes e depois).
Pistas
Divida o termo digitado em palavras com split(/\s+/) e exija que todas passem: palavras.every(...).
String.prototype.normalize("NFD") mais a faixa ̀-ͯ resolvem os acentos; a seção 9.2 tem a receita.
Para o plural ingênuo, remova um s final de cada palavra antes de comparar — e documente em um comentário que essa é uma simplificação, não uma regra do português.
Normalizar o texto de cada item a cada tecla é desperdício. Faça isso uma vez, guardando o texto já normalizado em um campo do próprio objeto ao iniciar.
O validarCampo da Mão na massa tem um switch com sete casos. Funciona, mas cada campo novo exige mexer nessa função — e, num formulário de vinte campos, ela vira um monstro. Existe uma alternativa que os frameworks usam: descrever as regras como dados e deixar uma função genérica aplicá-las. Refatore a validação da inscrição para esse modelo, sem perder nenhuma mensagem nem nenhum comportamento.
Critérios de pronto
Existe um objeto REGRAS mapeando o id de cada campo para uma lista de validadores.
validarCampo(campo) tem no máximo dez linhas e não cita nenhum campo pelo nome.
Acrescentar um campo novo ao formulário exige alterar apenas o HTML e o objeto REGRAS.
Todos os comportamentos do Como testar continuam funcionando, item por item.
Um comentário no topo explica, em três linhas, a vantagem dessa organização.
Pistas
Um validador é uma função (valor) => mensagem. Uma lista deles pode ser percorrida com find para achar a primeira mensagem não vazia.
Regras que precisam de parâmetro (tamanho mínimo, idade mínima) viram funções que devolvem funções: minimo(5) devolve (valor) => ....
O campo termos trabalha com checked, não com value. Padronize a entrada do validador (por exemplo, sempre o elemento) para não precisar de exceções.
Compare o resultado com o criarValidador do exercício C1: você acabou de construir metade dele.
Um formulário pode passar em todos os testes automáticos e ainda ser impossível de preencher sem o mouse. Nesta missão você vai auditar o formulário do seu projeto autoral como um usuário que não enxerga a tela, corrigir o que encontrar e provar o resultado. Isso entra bem no Marco 3 da unidade.
Critérios de pronto
Um vídeo de até três minutos preenchendo e enviando o formulário só com o teclado, com o monitor desligado ou os olhos fechados, usando o leitor de tela do sistema (NVDA no Windows, Orca no Linux, VoiceOver no macOS).
O formulário tem um resumo de erros no topo, ligado por link a cada campo com problema, que recebe foco ao falhar o envio.
Todo campo tem rótulo associado, texto de ajuda ligado por aria-describedby e mensagem por role="alert".
A aba Lighthouse do DevTools marca 100 em Acessibilidade na página do formulário, e a captura está no relatório.
Um arquivo acessibilidade.md lista os problemas encontrados, a correção aplicada e — o item mais importante — dois problemas que o Lighthouse não apontou e que só a navegação por teclado revelou.
Pistas
Comece navegando só com Tab, Shift+Tab, Espaço e Enter. Anote todo momento em que você não soube onde estava.
O resumo de erros é uma <div tabindex="-1"> com uma lista de links href="#a14-id-do-campo"; ao falhar o envio, chame .focus() nela.
Ferramentas automáticas não detectam rótulo errado, ordem de foco ilógica nem mensagem que não diz o que fazer. É aí que estão os seus dois achados.
O leitor de tela do seu sistema já está instalado. No Windows, o Narrador abre com Ctrl+Win+Enter; no Linux, o Orca com Super+Alt+S.
Parte 1 — Leitura (15 min). FLANAGAN, JavaScript: o guia definitivo, capítulo de expressões regulares. MILETTO & BERTAGNOLLI, Desenvolvimento de software II, seção de validação de dados de entrada. Na MDN em português, o artigo "Validação de formulário no lado do cliente" (link em Para aprofundar). Anote duas restrições de validação nativa que não apareceram nesta aula.
Parte 2 — Entrega (40 min). No seu projeto autoral:
O formulário principal com validação completa em JavaScript: pelo menos seis campos, mensagens específicas por campo, ao menos uma validação por expressão regular e uma regra de negócio que a regex não resolve (dígito verificador, idade mínima, data no calendário ou confirmação de senha).
Mensagens acessíveis: role="alert", aria-invalid, aria-describedby, indicação que não dependa só de cor e foco no primeiro campo inválido ao enviar.
A listagem principal do seu domínio com busca (com debounce e normalização de acentos), um filtro e uma ordenação, com contador anunciado e estado vazio tratado.
Os exercícios B2 (medidor de força de senha) e B7 (gerarSlug) em exercicios/aula14/.
Critério de pronto: enviar o formulário vazio destaca todos os campos com erro, leva o foco ao primeiro e não recarrega a página; digitar um termo sem acento encontra o item acentuado; o console fica limpo em todas as páginas.
Parte 3 — Discussão (5 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: traga uma expressão regular sua que falhou em um caso que você não tinha previsto, explique por que falhou e mostre a correção.
Guarde no seu repositório: commit + push (ou a pasta do projeto).
web.dev — Learn Forms: https://web.dev/learn/forms — os módulos de validação e de acessibilidade em formulários, com exemplos interativos.
regex101: https://regex101.com — testa a expressão, explica token por token e mede o desempenho. Escolha o sabor ECMAScript (JavaScript).
Regexr: https://regexr.com — alternativa com uma biblioteca de padrões prontos para comparar com os seus.
FLANAGAN, David. JavaScript: o guia definitivo. Bookman, 2014 — capítulo de expressões regulares e de trabalho com formulários.
STEFANOV, Stoyan. Padrões JavaScript. Novatec, 2010 — o padrão de módulo, base do exercício C1.
MILETTO, Evandro M.; BERTAGNOLLI, Silvia C. Desenvolvimento de software II. Bookman, 2014 — validação de dados de entrada e tratamento de erros.
TERUEL, Evandro C. HTML 5 — Guia Prático. Saraiva, 2014 — capítulo de formulários, para revisar os atributos que alimentam a Constraint Validation API.
Na próxima aula, o site sai da sua máquina e vai para a internet: você vai preparar o projeto para produção, versioná-lo com Git, publicá-lo no GitHub Pages, auditar o resultado com o Lighthouse e fechar o Marco 3 — o site do evento, no ar, com um endereço que qualquer pessoa pode acessar.
Explicar o que são hospedagem, domínio e certificado HTTPS, e como os três se combinam para que um endereço digitado por um estranho chegue ao seu index.html.
Identificar as diferenças entre o ambiente local e um servidor real — sensibilidade a maiúsculas, caminhos e subdiretório na URL — e corrigir o projeto antes de publicar.
Preparar um projeto front-end para produção: limpeza de código, otimização de imagens, metadados de SEO e Open Graph, favicon e página 404.
Versionar o projeto com os comandos essenciais do Git e publicá-lo em um repositório público no GitHub, com .gitignore e README.md adequados.
Publicar o site no GitHub Pages e reconhecer quando a Netlify ou a Vercel são alternativas melhores.
Auditar o site publicado com o validador do W3C e o Lighthouse, interpretar as quatro pontuações e aplicar as correções de maior impacto.
Fechar o Marco 3 com o projeto autoral no ar, acessível por uma URL pública que qualquer pessoa consegue abrir.
[ ] O site do evento acadêmico completo: cinco páginas responsivas, animações, menu hambúrguer acessível, listagens renderizadas por JavaScript, formulário validado e programação com busca, filtro e ordenação (Aulas 01 a 14).
[ ] Console limpo nas cinco páginas, conforme o Checkpoint da Aula 14.
[ ] Git instalado e configurado com o seu nome e e-mail (git --version responde no terminal). Se ainda não estiver, o Capítulo 01 — Caixa de ferramentas do dev web tem o passo a passo por sistema operacional.
[ ] Uma conta gratuita no GitHub, com o e-mail confirmado.
[ ] Navegador Chrome ou Edge atualizado (a aba Lighthouse do DevTools existe neles; no Firefox, use o PageSpeed Insights).
[ ] Uma pasta de projeto chamada site-evento/, com index.html na raiz.
Na aula passada você fechou o ciclo do JavaScript no navegador: o formulário de inscrição ganhou validação campo a campo com mensagens acessíveis, e a programação ganhou busca, filtro e ordenação em tempo real. O site está pronto — e existe para exatamente uma pessoa, porque só roda no http://127.0.0.1:5500 da sua máquina. Hoje ele sai daí: você vai prepará-lo para produção, versioná-lo com Git, publicá-lo no GitHub Pages, auditar o resultado com o Lighthouse e terminar a aula com um endereço que qualquer pessoa do mundo consegue abrir.
Hospedagem, domínio e HTTPS; o que muda entre "funciona aqui" e "está no ar"; preparação para produção
2
50 min
Git essencial, repositório no GitHub, GitHub Pages passo a passo e alternativas
3
50 min
Auditoria com validador do W3C e Lighthouse, correções, README e Marco 3 do projeto
1. O caminho de um site até o navegador de outra pessoa¶
Na Aula 01 você viu o ciclo requisição–resposta: o navegador pede, o servidor responde, o navegador desenha. Naquele momento o servidor era o Live Server, rodando na sua própria máquina. Publicar um site significa trocar esse servidor doméstico por um que fica ligado o tempo todo, com um endereço que existe para o mundo inteiro.
Três peças fazem isso acontecer. Elas são independentes — dá para ter uma sem a outra — e é justamente por isso que confundi-las causa tanta dor de cabeça.
Hospedagem é o computador (ou o conjunto de computadores) que guarda os seus arquivos e os entrega quando alguém pede. Para um site estático como o seu — só HTML, CSS, JavaScript e imagens, sem banco de dados nem código rodando no servidor —, hospedar é barato ao ponto de ser gratuito: o servidor não precisa pensar, só devolver arquivos.
Existem três modelos que você vai encontrar por aí:
Modelo
Como funciona
Quando usar
Hospedagem de sites estáticos
Você envia os arquivos; a plataforma serve e distribui
Sites como o seu, portfólios, documentação
Hospedagem compartilhada
Um servidor com PHP e banco, dividido entre muitos clientes
Sites em WordPress e similares
Servidor próprio (VPS)
Você aluga uma máquina Linux e configura tudo
Aplicações com back-end e requisitos próprios
O primeiro modelo é o desta aula, e o serviço que você vai usar — o GitHub Pages — pertence a ele. O terceiro é assunto do Capítulo 06 — Servidor próprio (VPS) com nginx da trilha Deploy, e vale a leitura quando você quiser entender o que as plataformas escondem de você.
🧠 Você sabia?
Sites estáticos praticamente não custam nada para servir porque a resposta é sempre a mesma para todo mundo — o arquivo pode ser copiado para servidores espalhados pelo planeta (uma CDN) e entregue a partir do mais próximo de quem pediu. É por isso que o GitHub Pages, a Netlify e a Vercel oferecem o serviço de graça: o custo marginal de mais um site pequeno é ínfimo. Um site que gera cada página na hora, consultando um banco, custa ordens de grandeza mais — e essa diferença é o principal motivo do renascimento dos sites estáticos na última década.
Servidores são encontrados por endereço IP (185.199.108.153, por exemplo). Ninguém decora isso. O domínio é um apelido legível — wikipedia.org, github.io, weblab.aprendabit.com — e o DNS é a agenda telefônica mundial que traduz o apelido no número.
Quando alguém digita o endereço do seu site, acontece o seguinte, nesta ordem:
O navegador pergunta ao DNS qual é o IP daquele domínio.
O DNS responde (ou diz que não conhece, e você vê DNS_PROBE_FINISHED_NXDOMAIN).
O navegador abre uma conexão com aquele IP e pede o caminho da URL.
O servidor responde com o arquivo — ou com um status de erro, como o 404 que você já viu na aba Network.
Registrar um domínio .com.br custa por volta de quarenta reais por ano no Registro.br. Para esta trilha você não precisa comprar nada: o GitHub oferece um subdomínio gratuito no formato https://<seu-usuario>.github.io/<nome-do-repositorio>/, e ele é um endereço público de verdade — funciona no celular de qualquer pessoa, na casa da sua avó e em qualquer lugar com internet.
Se, depois desta trilha, você quiser um domínio seu, o Capítulo 04 — Domínios, DNS e HTTPS da trilha Deploy cobre registro, registros A e CNAME, propagação e a configuração no lado da plataforma.
O s de HTTPS significa que a conversa entre o navegador e o servidor é cifrada. Sem ele, qualquer pessoa no mesmo Wi-Fi consegue ler — e alterar — o que trafega. Com ele, o conteúdo é embaralhado por chaves negociadas no início da conexão, e um certificado emitido por uma autoridade confiável garante que o servidor do outro lado é mesmo quem diz ser.
Três consequências práticas para você hoje:
O HTTPS é gratuito. A autoridade certificadora Let's Encrypt emite certificados sem custo, e as plataformas de hospedagem cuidam da emissão e da renovação sozinhas. No GitHub Pages, basta marcar uma caixa.
Recursos misturados não carregam. Uma página servida por HTTPS que tenta buscar uma imagem por http:// recebe do navegador um bloqueio de conteúdo misto, com a mensagem Mixed Content: The page at 'https://…' was loaded over HTTPS, but requested an insecure element. Solução: nunca escreva http:// nas URLs do seu site.
Algumas APIs só existem em HTTPS. Geolocalização, câmera, microfone e notificações são bloqueadas em conexões inseguras. Isso não afeta o site do evento, mas afeta o seu próximo projeto.
⚠️ Atençãolocalhost é tratado pelo navegador como origem segura mesmo em http://. Ou seja: coisas que funcionam na sua máquina podem falhar assim que o site vai ao ar em outro domínio. Depois de publicar, refaça o teste no endereço público, não no Live Server.
Esta aula é uma travessia rápida e completa: no fim dela o seu site está no ar. Ela não é, porém, o assunto inteiro. O WebLab tem uma trilha transversal só para isso, e dois capítulos dela conversam diretamente com o que você vai fazer hoje:
Capítulo 02 — Git e GitHub do zero ao pull request: branches, merge, resolução de conflitos, git stash, tags, revisão de código em pull request e o gh (o GitHub pela linha de comando). Hoje você usa seis comandos; lá estão os outros trinta.
Capítulo 03 — Publicando sites estáticos: GitHub Pages, Netlify e Vercel em profundidade, domínio próprio, cabeçalhos de cache, redirecionamentos, prévia por branch e publicação automática.
Leia os dois quando quiser ir além do "está no ar" e chegar ao "está no ar com processo".
2. O que muda entre "funciona aqui" e "está no ar"¶
Um site que funciona perfeitamente no Live Server pode chegar ao servidor completamente quebrado — sem estilo, sem imagens, com o JavaScript morto. Não é azar: são três diferenças concretas de ambiente.
Diferença
Consequência no ar
O servidor distingue maiúsculas de minúsculas
Estilo.css ≠ estilo.css: a página aparece "crua", sem CSS
Caminhos absolutos da sua máquina
C:/Users/ana/site/img/logo.png não existe em servidor nenhum
O site fica em um subdiretório da URL
Em usuario.github.io/site-evento/, o caminho /img/logo.png aponta para a raiz errada
A primeira diferença é a mais cruel. O Windows e o macOS, por padrão, tratam Foto.JPG e foto.jpg como o mesmo arquivo. O Linux — que roda em praticamente todo servidor do mundo — não. O seu site funciona na sua máquina e morre no servidor, com um 404 para cada arquivo cujo nome você escreveu com uma letra diferente da real.
As três regras que evitam quase todos os problemas:
Tudo em minúsculas, sem espaços e sem acentos, em arquivos e em pastas. foto-do-palestrante.webp, nunca Foto do Palestrante.WEBP.
Sempre caminhos relativos: css/estilo.css, img/logo-sasi.svg, ../index.html. Nunca comece um caminho com / em um site que vai para subdiretório, e nunca use caminho de disco.
O arquivo de entrada se chama index.html e fica na raiz do repositório. É o nome que todo servidor procura quando a URL termina em /.
🔎 Por baixo do capô
Quando você pede https://usuario.github.io/site-evento/, o servidor recebe o caminho /site-evento/ e precisa decidir o que devolver. A convenção, herdada dos primeiros servidores web dos anos 1990, é procurar um arquivo de índice dentro do diretório: index.html. Se não achar, ele responde 404 ou, em servidores configurados para isso, devolve a listagem do diretório — aquela página cinza feia com os nomes dos arquivos. Renomear index.html para home.html é o jeito mais rápido de ver essa tela.
🔬 Investigue
Abra o index.html do site do evento e troque a linha do CSS para <link rel="stylesheet" href="CSS/Estilo.css">. Salve e recarregue no Live Server: no Windows e no macOS, provavelmente nada muda — o site continua estilizado. Agora abra o DevTools na aba Network, marque a caixa Disable cache e recarregue: a linha do CSS aparece com status 200 mesmo com o nome errado. Esse é exatamente o cenário que quebra no servidor Linux, onde o mesmo pedido volta 404. Desfaça a alteração e adote a regra: nomes de arquivo sempre em minúsculas, escritos uma vez e copiados, nunca redigitados.
"Produção" é o nome que se dá ao ambiente onde os usuários de verdade usam o sistema. Preparar para produção é fazer, de uma vez, o que você foi adiando durante o semestre.
HTML na raiz. Colocar as páginas dentro de uma pasta paginas/ quebra todos os caminhos relativos e não traz benefício algum em um site de cinco páginas.
Uma pasta por tipo de recurso: css/, js/, img/. Se as imagens forem muitas, subpastas por seção (img/palestrantes/) — no site do evento elas são poucas e ficam direto em img/, com os nomes numerados que o js/dados.js referencia desde a Aula 12.
Nada de arquivos órfãos.teste.html, estilo-antigo.css, Nova pasta/, index - Cópia.html: apague. Se der medo, é para isso que serve o Git — depois do primeiro commit, nada se perde de verdade.
Percorra esta lista antes de publicar. Ela leva vinte minutos e evita metade dos problemas do Marco 3:
Remova os console.log de depuração. Console limpo é critério de qualidade, e um console cheio de mensagens suas denuncia código não revisado. O truque profissional está na próxima subseção.
Remova blocos de código comentado. Aquele CSS antigo que você "deixou comentado por via das dúvidas" não serve para nada: a versão anterior está no histórico do Git.
Remova arquivos não referenciados. Se nenhum HTML aponta para ele, ele só está ocupando espaço e confundindo quem lê.
Confira todos os links. Nenhum href="#" esquecido, nenhum link para uma página que você renomeou.
Confira todas as imagens. Cada <img> tem alt adequado, e o arquivo existe com exatamente aquele nome.
Confira os textos. Nada de texto de preenchimento sobrando: o conteúdo tem que ser real, ainda que curto.
Apagar todos os console.log na véspera da entrega é péssimo: você perde as ferramentas justo quando mais precisa delas. A solução é uma chave única e uma função que a consulta.
Ela precisa ficar no topo do js/dados.js, não do app.js. Desde a Aula 12, dados.js é o primeiro script do projeto que declara coisas usadas pelos outros — e ele mesmo tem um console.log no fim, que você vai querer trocar por depurar. Se a função morasse no app.js, que é carregado depois, essa troca daria Uncaught ReferenceError: depurar is not defined na primeira linha do site.
site-evento/js/dados.js (acrescente no início do arquivo, antes dos arrays)
JavaScript
// ---------- DEPURAÇÃO ----------// Chave de depuração: deixe true enquanto desenvolve, false antes de publicar.constDEPURAR=false;/** * Escreve no console apenas quando a depuração está ligada. * Use no lugar de console.log em qualquer mensagem de desenvolvimento. * @param {...*} mensagens - o que você quer inspecionar */functiondepurar(...mensagens){if(DEPURAR){console.log("[site-evento]",...mensagens);}}
A partir daí, troque todo console.log de desenvolvimento por depurar — a começar pela última linha do próprio dados.js, que passa a ser depurar(\dados.js carregado: ${palestras.length} atividades, ${palestrantes.length} palestrantes.`)`. Ligar e desligar a depuração inteira vira uma edição de uma linha.
Duas ressalvas importantes:
console.errorfica. Erros de verdade — uma imagem que não carregou, um dado inválido — devem aparecer sempre, inclusive em produção.
depurar precisa estar declarado antes de ser usado pelos outros scripts. Com ele no topo do js/dados.js, isso vale para app.js, relatorios.js, palestrantes.js, inscricao.js e programacao.js — todos carregados depois, e defer garante a ordem das tags. O único que vem antes é o js/menu.js, que não escreve nada no console; se um dia precisar, mova a função para um js/util.js carregado em primeiro lugar.
Em quase todo projeto de estudante, as imagens respondem por 80% dos bytes da página. Uma foto de celular tem cerca de 4000 pixels de largura e pesa 4 MB; exibida em um cartão de 400 pixels, ela entrega cem vezes mais dados do que o necessário — e quem paga a conta é quem abre o seu site em dados móveis.
Ação
Ganho típico
Redimensionar para o tamanho real de exibição
50% a 90%
Converter de JPEG para WebP
25% a 35% a mais
Usar SVG para logos e ícones
Enorme, e escala sem perder qualidade
loading="lazy" no que está abaixo da dobra
Carregamento inicial mais rápido
Declarar width e height
Elimina o "pulo" do layout ao carregar
O caminho prático, sem instalar nada: abra squoosh.app, arraste a imagem, escolha WebP com qualidade em torno de 75, ajuste a largura para o dobro do tamanho de exibição (para telas de alta densidade) e baixe. Uma foto de 4 MB costuma sair com 80 KB e nenhuma diferença visível.
A marcação final de uma imagem otimizada:
HTML
<imgsrc="img/palestrante-03.webp"alt="Carla Mendes, palestrante da área de segurança, sorrindo em frente a um quadro branco"width="400"height="400"loading="lazy">
Os atributos width e height não fixam o tamanho na tela — o CSS continua mandando. Eles informam ao navegador a proporção da imagem, para que ele reserve o espaço correto antes do arquivo chegar. Sem eles, o texto abaixo da imagem "pula" quando ela carrega, e o Lighthouse penaliza isso na métrica de estabilidade visual.
⚠️ Atenção
Não coloque loading="lazy" na imagem principal do topo da página (o herói, o logo do cabeçalho). Ela é justamente a que precisa aparecer primeiro; adiar o seu carregamento piora a experiência e a pontuação. lazy é para o que está abaixo da dobra — fora da primeira tela.
O <head> de cada página carrega informações que não aparecem na tela, mas definem como o seu site é visto pelo mundo: o título na aba, o resumo no resultado de busca, o cartão que aparece quando alguém cola o link em um grupo de WhatsApp.
site-evento/index.html (o <head> completo)
HTML
<head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Semana Acadêmica de Sistemas de Informação</title><metaname="description"content="Três dias de palestras, minicursos e maratona de programação no campus de Sinop. Programação completa, inscrição gratuita e certificado."><metaname="author"content="Semana Acadêmica de Sistemas de Informação"><metaname="theme-color"content="#0b3d5c"><linkrel="icon"href="img/favicon.svg"type="image/svg+xml"><linkrel="canonical"href="https://usuario.github.io/site-evento/"><metaproperty="og:type"content="website"><metaproperty="og:title"content="Semana Acadêmica de Sistemas de Informação"><metaproperty="og:description"content="Palestras, minicursos e maratona de programação. Inscrição gratuita."><metaproperty="og:image"content="https://usuario.github.io/site-evento/img/preview.jpg"><metaproperty="og:url"content="https://usuario.github.io/site-evento/"><metaproperty="og:locale"content="pt_BR"><linkrel="stylesheet"href="css/estilo.css"><scriptsrc="js/menu.js"defer></script><scriptsrc="js/dados.js"defer></script><scriptsrc="js/app.js"defer></script><scriptsrc="js/efeitos.js"defer></script></head>
A ordem dos <script> é a mesma fixada nas Aulas 13 e 14 — menu.js, dados.js, app.js e, por último, o script específico da página (aqui o efeitos.js da Aula 09; em programacao.html seriam relatorios.js e programacao.js, e em inscricao.html o inscricao.js). É por isso que o depurar() da seção 3.3 mora no topo do dados.js: assim ele já existe quando o app.js e os scripts de página rodam.
O que cada bloco faz:
<title> — o texto da aba e o link azul do resultado de busca. Escreva no padrão Assunto da página — Nome do site, com no máximo 60 caracteres. Cada página tem o seu, diferente das outras.
<meta name="description"> — o parágrafo cinza abaixo do link no Google. Até 160 caracteres, escritos para uma pessoa, não para um robô. Cada página tem a sua.
theme-color — colore a barra do navegador no Android. Detalhe pequeno, efeito grande.
<link rel="icon"> — o favicon, o ícone da aba. Um SVG resolve todos os tamanhos.
canonical — declara qual é o endereço oficial da página, evitando que ela seja tratada como conteúdo duplicado quando acessível por mais de uma URL.
og:* — o protocolo Open Graph, criado pelo Facebook e adotado por praticamente todo mundo (WhatsApp, Telegram, LinkedIn, Discord, Slack). É o que gera o cartão com imagem, título e descrição quando alguém compartilha o link. A og:image precisa ser uma URL absoluta — endereço completo, com https:// — porque quem lê essa marcação é um servidor de outra empresa, que não tem como resolver caminho relativo.
A imagem de prévia deve ter 1200 × 630 pixels. Menos que isso e as plataformas mostram um cartão pequeno, sem destaque.
🧠 Você sabia?
Quando você cola um link no WhatsApp, não é o seu celular que gera a prévia: um servidor da plataforma abre a sua página, lê apenas o <head> e monta o cartão. Isso tem duas consequências curiosas. A primeira: se o seu site estiver fora do ar naquele instante, a prévia nunca mais é gerada para aquele link, porque o resultado fica em cache por muito tempo. A segunda: conteúdo inserido por JavaScript não entra na prévia — esses leitores não executam scripts. É por isso que as metatags precisam estar escritas no HTML, e não geradas pelo seu app.js.
Cedo ou tarde alguém vai digitar errado o endereço de uma das suas páginas, ou clicar num link antigo. A resposta padrão do GitHub Pages é uma página em inglês, com o mascote do GitHub — nada a ver com o seu site.
Uma página 404.html na raiz do repositório resolve: o Pages a serve automaticamente sempre que um caminho não existir.
site-evento/404.html
HTML
<!DOCTYPE html><htmllang="pt-BR"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Página não encontrada — Semana Acadêmica de SI</title><metaname="description"content="A página que você procurou não existe neste site."><metaname="robots"content="noindex"><linkrel="icon"href="/site-evento/img/favicon.svg"type="image/svg+xml"><linkrel="stylesheet"href="/site-evento/css/estilo.css"></head><body><aclass="pular-para-conteudo"href="#conteudo">Pular para o conteúdo</a><mainid="conteudo"class="erro-404"><pclass="erro-404__codigo">404</p><h1>Esta página não existe</h1><p>
O endereço que você abriu não corresponde a nenhuma página do site da
Semana Acadêmica. Pode ser um link antigo, um erro de digitação — ou uma
página que ainda vamos criar.
</p><p>Talvez você esteja procurando por:</p><ulclass="erro-404__links"><li><ahref="/site-evento/">Página inicial</a></li><li><ahref="/site-evento/programacao.html">Programação completa</a></li><li><ahref="/site-evento/inscricao.html">Inscrição</a></li><li><ahref="/site-evento/palestrantes.html">Palestrantes</a></li><li><ahref="/site-evento/contato.html">Contato</a></li></ul></main></body></html>
Repare que esta é a única página do projeto com caminhos começando por /site-evento/. O motivo: a 404.html é servida em resposta a qualquer caminho inexistente, inclusive /site-evento/uma/pasta/funda/pagina.html. Um caminho relativo seria resolvido a partir dessa pasta imaginária e apontaria para o nada. Como o prefixo é o nome do repositório, você o ajusta se um dia renomeá-lo.
O validator.w3.org lê o seu HTML e aponta tudo o que está fora da especificação: tag não fechada, atributo inventado, id repetido, hierarquia de títulos quebrada, texto solto onde só cabe elemento.
Três modos de uso, todos gratuitos:
Modo
Quando usar
Address
O site já está publicado: cole a URL e valide a página no ar
File Upload
Ainda local: envie o arquivo .html
Direct Input
Um trecho suspeito: cole o HTML direto na caixa
Valide todas as páginas, não só a inicial. A meta é zero erros. Avisos (warnings) merecem leitura, mas nem todos exigem ação — o aviso sobre <section> sem cabeçalho, por exemplo, costuma ser legítimo em alguns layouts.
📌 Vale gravar
Saiba dizer a diferença entre erro e aviso no validador, e por que HTML inválido pode funcionar no navegador mesmo assim: o algoritmo de análise do HTML é deliberadamente tolerante, e "conserta" a árvore do documento por conta própria. O problema é que cada navegador conserta de um jeito, e o resultado deixa de ser previsível — sem falar que leitores de tela e buscadores dependem da estrutura correta.
Duas conferências rápidas, feitas no próprio DevTools:
Aba Console, nas cinco páginas: zero mensagens em vermelho. Cada Uncaught é um recurso do seu site que não funciona.
Aba Network, com o filtro de status: qualquer linha 404 é um arquivo que o HTML pede e o servidor não tem. Nome errado, arquivo esquecido fora do commit ou caminho equivocado.
Para os links internos, o teste é manual e leva cinco minutos: clique em todos, em todas as páginas, incluindo os do rodapé e os do menu. Depois de publicado, um serviço como o W3C Link Checker faz a varredura sozinho.
O Lighthouse vem embutido no Chrome e no Edge. Ele carrega a sua página simulando um celular modesto em rede 4G, mede dezenas de indicadores e devolve quatro notas de 0 a 100.
Categoria
Meta nesta trilha
O que costuma derrubar
Desempenho
≥ 80
Imagens grandes, fontes pesadas, layout instável
Acessibilidade
≥ 90
Contraste, alt ausente, label ausente, ordem de títulos
Práticas recomendadas
≥ 90
Erros no console, imagens sem proporção, recursos por HTTP
SEO
≥ 90
Sem description, sem title, link sem texto, sem viewport
Como rodar, do jeito certo:
Abra o site publicado (não o localhost) em uma janela anônima — extensões do navegador poluem o resultado e derrubam a nota de práticas recomendadas.
F12 → aba Lighthouse.
Modo Navigation, dispositivo Mobile, as quatro categorias marcadas.
Analyze page load. Aguarde de vinte segundos a um minuto.
Rode duas ou três vezes: a nota de desempenho oscila alguns pontos entre execuções, porque depende da rede e da carga da sua máquina naquele instante. Considere a mediana.
As correções de maior impacto, em ordem de retorno pelo esforço:
Comprimir e converter as imagens para WebP.
Declarar width e height em todas as imagens.
Aplicar loading="lazy" no que está abaixo da dobra.
Reduzir a dois os pesos de fonte importados (por exemplo, 400 e 700 apenas).
Corrigir os contrastes reprovados.
Zerar os erros do console.
Preencher title e description de cada página.
🔬 Investigue
Rode o Lighthouse no site do evento antes de otimizar as imagens e anote as quatro notas. Depois abra a aba Network, marque Disable cache, recarregue e leia a barra de status no rodapé: ela mostra o total transferido. Anote também esse número. Faça a otimização das imagens e repita as duas medições. Na prática, o peso da página inicial costuma cair de 6 MB para menos de 500 KB, e o desempenho salta trinta pontos — com uma tarde de trabalho e nenhuma linha de código. É a melhor relação entre esforço e resultado de toda a trilha.
O Git é um sistema de controle de versão: ele guarda fotografias sucessivas do seu projeto, permite voltar a qualquer uma delas e junta o trabalho de várias pessoas. Ele é também o mecanismo pelo qual o seu código chega ao GitHub e, de lá, ao ar.
gitconfig--globaluser.name"Seu Nome Completo"
gitconfig--globaluser.email"seu-email@exemplo.com"
gitconfig--globalinit.defaultBranchmain
O e-mail precisa ser o mesmo cadastrado no GitHub. É por ele que a plataforma associa cada commit à sua conta — e é assim que fica registrado quem escreveu o quê (inclusive para o professor conferir, se você cursa esta trilha em uma turma). Confira o que ficou gravado:
gitinit# cria o repositório na pasta atual (uma vez por projeto)
gitstatus# o que mudou desde o último commit
gitadd.# prepara todas as mudanças para o próximo commit
gitcommit-m"mensagem"# grava a fotografia com uma descrição
gitlog--oneline# o histórico, uma linha por commit
gitpush# envia os commits para o GitHub
O ciclo normal de trabalho tem três passos, repetidos indefinidamente: editar → git add . → git commit -m "…". Ao fim da sessão, git push.
5.3 Mensagens de commit que servem para alguma coisa¶
Uma mensagem de commit é um bilhete para o seu eu de daqui a três meses. Escreva no imperativo, dizendo o que o commit faz ao projeto:
Terminal
gitcommit-m"Adiciona validação de CPF no formulário de inscrição"
gitcommit-m"Corrige menu que não fechava com Esc no celular"
gitcommit-m"Otimiza imagens dos palestrantes para WebP"
O que não escrever: alteracoes, att, final, final2, agora vai, .. Um histórico assim é indistinguível de nenhum histórico.
Uma boa regra: um commit por ideia. Terminou o menu? Commit. Corrigiu o contraste dos botões? Outro commit. Commits pequenos e frequentes são fáceis de entender e de desfazer.
Alguns arquivos não pertencem ao repositório: lixo do sistema operacional, configuração pessoal do editor, dependências que qualquer um consegue reinstalar e — principalmente — segredos.
site-evento/.gitignore
Texto
# Sistema operacional
.DS_Store
Thumbs.db
desktop.ini
# Editor
.vscode/
.idea/
# Dependências e logs
node_modules/
*.log
# Arquivos temporários e originais pesados
*.tmp
img-originais/
A pasta img-originais/ é uma sugestão prática: guarde nela as fotos em tamanho original, fora do Git, e versione apenas as versões otimizadas que o site usa.
⚠️ Atenção
O .gitignore só funciona para arquivos que ainda não foram commitados. Se você já enviou um arquivo por engano, adicioná-lo ao .gitignore não o remove do repositório nem do histórico. E, uma vez publicado, considere que o conteúdo vazou: um repositório público é lido por robôs em minutos. Nunca coloque senhas, tokens ou chaves de API em um repositório — no seu site do evento não há nenhum, mas no Nível 2 haverá.
O GitHub Pages nasceu como uma plataforma para o gerador de sites Jekyll, e por herança ele ainda processa os arquivos antes de publicá-los. Um efeito colateral: pastas e arquivos que começam com _ são ignorados. Se um dia você criar _imagens/ ou _parciais/, elas simplesmente não vão ao ar.
A solução é um arquivo vazio na raiz, chamado .nojekyll:
Terminal
touch.nojekyll
No Windows, sem o Git Bash, o mesmo efeito com o PowerShell:
O GitHub Pages transforma um repositório em um site. A cada git push, ele pega os arquivos da branch escolhida e os serve em um endereço público, com HTTPS incluído. É gratuito para repositórios públicos e não exige cartão de crédito.
Entre em github.com e clique no + no canto superior direito → New repository.
Repository name:site-evento (minúsculas, com hífen, sem acento e sem espaço — esse nome vai aparecer na URL).
Description: uma frase sobre o projeto.
Public. Obrigatório aqui: em repositório privado, o GitHub Pages só está disponível nos planos pagos.
Não marque nada em "Initialize this repository": nem README, nem .gitignore, nem licença. Você já tem esses arquivos localmente, e um repositório remoto com commits próprios complica o primeiro push.
Create repository.
A página seguinte mostra os comandos para conectar um repositório local existente. São estes:
git remote add origin <url> — apelida a URL do repositório remoto como origin.
git branch -M main — garante que a branch local se chama main (o nome que o GitHub espera).
git push -u origin main — envia os commits e memoriza o destino; a partir daí, git push sozinho basta.
Na primeira vez, o Git pedirá autenticação. O navegador abre e você autoriza — é o fluxo padrão do Git Credential Manager. Se preferir chave SSH ou o GitHub CLI, o Capítulo 02 cobre os dois.
Para o repositório site-evento da usuária ana-souza, por exemplo: https://ana-souza.github.io/site-evento/.
Existe um caso especial: se você nomear o repositório exatamente como <seu-usuario>.github.io, o site é publicado na raiz do subdomínio, sem subpasta. É a escolha certa para um portfólio pessoal — e cada conta só pode ter um.
🧠 Você sabia?
O github.io é um domínio separado do github.com por um motivo de segurança, não de estética. Se as páginas dos usuários fossem servidas em github.com/paginas/…, qualquer JavaScript publicado por qualquer pessoa rodaria na mesma origem do GitHub — e poderia, pela política de mesma origem, ler os cookies de sessão de quem estivesse logado. Isolando os sites em outro domínio, o navegador impede a leitura. A mesma preocupação explica por que o Google usa googleusercontent.com e a Microsoft usa sharepointonline.com para conteúdo enviado por usuários.
Este é o erro que mais aparece na primeira publicação. O site funciona no Live Server, sobe para o Pages e chega sem estilo nenhum.
A causa: caminhos que começam com barra.
HTML
<!-- Funciona no Live Server, quebra no GitHub Pages --><linkrel="stylesheet"href="/css/estilo.css">
No Live Server, a raiz é a pasta do projeto, e /css/estilo.css resolve certo. No Pages, a raiz é https://usuario.github.io/, e a barra manda o navegador buscar https://usuario.github.io/css/estilo.css — um lugar onde o seu CSS não está.
HTML
<!-- Funciona nos dois --><linkrel="stylesheet"href="css/estilo.css">
Regra prática para o seu site: nenhum href ou src interno começa com /, exceto os da 404.html, pelo motivo já explicado na seção 3.6. Localize os culpados de uma vez com a busca do VS Code (Ctrl+Shift+F) procurando por href="/ e src="/.
gitadd.
gitcommit-m"Corrige caminhos das imagens para publicação"
gitpush
De trinta segundos a dois minutos depois, o site no ar reflete a mudança. A aba Actions do repositório mostra a publicação em andamento; um visto verde significa concluída.
Se a alteração não aparecer, o culpado quase sempre é o cache do navegador: recarregue com Ctrl+Shift+R (ou Cmd+Shift+R no macOS), ou abra em janela anônima. Antes de concluir que "o Pages está com problema", confirme no próprio GitHub que o commit chegou: se o arquivo mudou lá, o problema está do seu lado da conexão.
O GitHub Pages resolve o caso desta trilha. Duas alternativas gratuitas resolvem casos que ele não cobre.
Netlify. Em netlify.com, há dois caminhos. O primeiro é literalmente arrastar a pasta do projeto para a área indicada no painel: em vinte segundos o site está no ar, sem Git nenhum. O segundo, recomendado, é Import from Git → autorizar o GitHub → escolher o repositório → deixar o build command vazio e o publish directory como . → Deploy. O endereço sai como nome-aleatorio.netlify.app e pode ser renomeado nas configurações do site.
Vercel. Em vercel.com, o fluxo é praticamente idêntico ao da Netlify. Para um site estático, o resultado é o mesmo; a Vercel brilha quando há um framework moderno envolvido.
Recurso
GitHub Pages
Netlify
Vercel
Custo e HTTPS automático
Gratuito, sim
Gratuito, sim
Gratuito, sim
Prévia automática por branch
Não
Sim
Sim
Formulários sem back-end
Não
Sim
Não
Redirecionamentos e cabeçalhos
Limitado
Sim
Sim
A prévia por branch é o recurso mais transformador dos três da tabela: cada branch enviada ganha uma URL própria, o que permite mostrar uma versão em teste sem tocar no site oficial. Os formulários da Netlify recebem envios de um <form> HTML puro e mostram as respostas no painel — resolvendo, sem back-end, o formulário de contato que hoje só existe visualmente no seu site.
Para o Marco 3, qualquer uma das três serve. Publique em uma; se sobrar tempo, publique nas três e compare (é o exercício B4).
Você não precisa de domínio próprio para o marco. Mas se quiser um — para o portfólio, por exemplo —, o caminho é curto:
Registre. No Registro.br para .com.br (exige CPF e custa por volta de quarenta reais por ano); em registradores internacionais para .com, .dev, .me.
Aponte o DNS. No painel do registrador, crie os registros que a plataforma de hospedagem indicar: quatro registros A para o GitHub Pages (com os IPs listados na documentação oficial) ou um CNAME apontando para <seu-usuario>.github.io.
Declare o domínio na plataforma. No GitHub: Settings → Pages → Custom domain. Isso cria um arquivo CNAME na raiz do repositório — não o apague.
Ative o HTTPS. Marque Enforce HTTPS. O certificado é emitido automaticamente, em minutos ou algumas horas.
A propagação do DNS leva de minutos a algumas horas; enquanto isso, é normal ver o site ora pelo endereço antigo, ora pelo novo. O Capítulo 04 — Domínios, DNS e HTTPS explica cada tipo de registro, o TTL e como diagnosticar problemas com dig e nslookup.
O README.md é a primeira coisa que o GitHub mostra ao abrir o repositório — e, muitas vezes, a única que um avaliador apressado lê. Ele é escrito em Markdown, a mesma linguagem desta apostila.
site-evento/README.md
Markdown
# Semana Acadêmica de Sistemas de Informação
Site do evento acadêmico do curso de Sistemas de Informação, desenvolvido com o
[WebLab](https://weblab.aprendabit.com), trilha Introdução ao Desenvolvimento Web.
**Site publicado:** https://usuario.github.io/site-evento/
## Sobre
Site de cinco páginas que divulga a programação do evento, apresenta os
palestrantes e recebe inscrições. O objetivo é concentrar em um só lugar as
informações que hoje se perdem em grupos de mensagens: o que acontece, quando,
onde e como participar.
O visitante encontra a programação completa com busca e filtro por trilha, a
lista de palestrantes e um formulário de inscrição validado no navegador.
## Funcionalidades-Menu de navegação responsivo, acessível por teclado, com indicação da página atual
-Programação renderizada a partir de dados em JavaScript, com busca, filtro por
trilha e ordenação
-Formulário de inscrição com validação campo a campo, incluindo CPF, telefone e
e-mail, com mensagens acessíveis
-Rascunho da inscrição salvo no navegador com localStorage
-Layout responsivo em três larguras, com animações que respeitam
prefers-reduced-motion
## Tecnologias
HTML5 semântico, CSS3 (variáveis, Flexbox, Grid, media queries) e JavaScript
(ES2015+), sem frameworks nem bibliotecas externas.
## Estrutura-`index.html`, `programacao.html`, `inscricao.html`, `palestrantes.html`,
`contato.html` — as cinco páginas
-`404.html` — página de erro
-`css/estilo.css` — folha de estilo única, organizada por seções comentadas
-`js/menu.js` — abre e fecha o menu hambúrguer (Aulas 08, 09 e 13)
-`js/efeitos.js` — revelação ao rolar com IntersectionObserver (Aula 09)
-`js/dados.js` — fonte única de dados: palestras, palestrantes e áreas
-`js/app.js` — comportamento comum a todas as páginas (contagem regressiva,
`aria-current` e `debounce`)
-`js/relatorios.js` — relatórios do evento no console (Aula 12)
-`js/palestrantes.js` — lista de palestrantes com filtro por área (Aula 13)
-`js/inscricao.js` — vagas restantes e validação do formulário
-`js/programacao.js` — busca, filtro e ordenação da programação
-`img/` — imagens otimizadas em WebP e SVG
## Como executar localmente1. Clone o repositório: `git clone https://github.com/usuario/site-evento.git`2. Abra a pasta no VS Code
3. Clique com o botão direito em `index.html` e escolha "Open with Live Server"
## Auditoria
Lighthouse (mobile, janela anônima, no site publicado):
desempenho 92, acessibilidade 100, práticas recomendadas 100, SEO 100.
HTML validado sem erros no validator.w3.org em todas as páginas.
## Autoria
Desenvolvido por Nome Sobrenome, estudante de Sistemas de Informação.
Sete seções, nenhuma opcional na prática: o que é, onde está no ar, o que faz, com o que foi feito, como está organizado, como rodar e quem fez. Um README com o link do site no topo economiza o tempo de quem avalia — e isso conta.
💡 Dica
No Markdown do GitHub, três coisas melhoram muito o README com pouco esforço: uma captura de tela do site logo abaixo do título (), listas de tarefas com - [x] para mostrar o que já está pronto, e blocos de código com a linguagem declarada. Evite badges decorativos: eles enchem o topo e não dizem nada sobre um trabalho acadêmico.
Ao fim deste roteiro, o site do evento acadêmico estará acessível em https://<seu-usuario>.github.io/site-evento/, auditado e documentado. Reserve os cinquenta minutos do segundo bloco e os primeiros vinte do terceiro.
Ctrl+Shift+F e procure por console.log. Substitua cada ocorrência de depuração por depurar(, depois de acrescentar o bloco da seção 3.3 ao topo do js/dados.js. Mantenha os console.error.
Procure por href="/ e por src="/. Cada resultado é um caminho absoluto que vai quebrar no Pages: remova a barra inicial.
Procure por href="#". Todo link de menu ou de rodapé precisa apontar para uma página real.
Apague arquivos órfãos: testes, cópias, folhas antigas, pastas de rascunho.
Renomeie para minúsculas, sem espaço e sem acento, qualquer arquivo que ainda esteja fora do padrão — e corrija as referências no HTML e no CSS.
Abra as cinco páginas no Live Server e confirme: console limpo, nenhuma linha vermelha na aba Network.
Cada página recebe title e description próprios. Copie o <head> completo da seção 3.5 para o index.html e ajuste as demais:
site-evento/programacao.html
HTML
<title>Programação — Semana Acadêmica de Sistemas de Informação</title><metaname="description"content="Palestras, minicursos e mesas-redondas dos três dias do evento, com busca por título e filtro por trilha.">
site-evento/inscricao.html
HTML
<title>Inscrição — Semana Acadêmica de Sistemas de Informação</title><metaname="description"content="Inscreva-se gratuitamente na Semana Acadêmica de Sistemas de Informação e garanta o seu certificado de participação.">
site-evento/palestrantes.html
HTML
<title>Palestrantes — Semana Acadêmica de Sistemas de Informação</title><metaname="description"content="Conheça quem vai palestrar no evento: professores, profissionais do mercado e egressos do curso.">
site-evento/contato.html
HTML
<title>Contato — Semana Acadêmica de Sistemas de Informação</title><metaname="description"content="Fale com a organização da Semana Acadêmica: e-mail, telefone e endereço do campus de Sinop.">
As metatags og: ficam iguais em todas as páginas, exceto og:title e og:url, que acompanham a página. Se ainda não tiver a imagem de prévia, gere uma de 1200 × 630 com o logo e o nome do evento e salve como img/preview.jpg.
Crie 404.html na raiz com o conteúdo da seção 3.6 e acrescente ao fim de css/estilo.css o bloco .erro-404. Teste depois de publicar: abrir https://<seu-usuario>.github.io/site-evento/pagina-que-nao-existe deve mostrar a sua página de erro.
Crie a pasta img-originais/ e mova para lá as fotos em tamanho original.
Para cada imagem, abra o squoosh.app, arraste o arquivo, escolha WebP com qualidade 75 e ajuste a largura para o dobro da exibição (uma foto exibida em 400 px sai com 800 px de largura).
Salve o resultado em img/, com nome em minúsculas e hífens.
Atualize os src no HTML e acrescente width, height e loading="lazy" no que estiver abaixo da dobra:
As fotos dos palestrantes não estão mais no HTML desde a Aula 13 — quem as escreve é o js/palestrantes.js, a partir do campo foto do js/dados.js. Então o ajuste acontece em um lugar só: no array de dados, trocando a extensão dos seis caminhos de .jpg para .webp. Nada mais do arquivo muda — os nomes, as áreas, os id e os palestranteId são os mesmos desde a Aula 12, e o relatorios.js e o programacao.js continuam lendo os mesmos campos.
site-evento/js/dados.js (trecho: só o campo foto de cada pessoa)
JavaScript
constpalestrantes=[{id:1,nome:"Ana Lúcia Ferreira",instituicao:"Universidade Estadual — Sinop",area:"ia",tema:"Redes neurais para prever a safra de soja",foto:"img/palestrante-01.webp"},{id:2,nome:"Bruno Takahashi",instituicao:"Startup AgroData",area:"dados",tema:"Dashboards que os produtores realmente usam",foto:"img/palestrante-02.webp"},{id:3,nome:"Carla Mendes",instituicao:"UFMT",area:"seguranca",tema:"O que um ataque de phishing ensina sobre UX",foto:"img/palestrante-03.webp"},{id:4,nome:"Diego Nascimento",instituicao:"Prefeitura de Sinop",area:"web",tema:"Acessibilidade em portais públicos: erros que vimos",foto:"img/palestrante-04.webp"},{id:5,nome:"Eduarda Ribeiro",instituicao:"Universidade Estadual — Sinop",area:"web",tema:"Do HTML ao deploy: o caminho do estudante",foto:"img/palestrante-05.webp"},{id:6,nome:"Felipe Arruda",instituicao:"Cooperativa Coopercana",area:"ia",tema:"Visão computacional no controle de pragas",foto:"img/palestrante-06.webp"},];
Recarregue palestrantes.html: os seis cartões continuam lá, agora com as fotos em WebP. Se algum ficar com o ícone de imagem quebrada, o nome do arquivo em img/ não bate com o do array — o Linux do GitHub Pages diferencia maiúsculas de minúsculas, e o seu Windows não.
Compare o peso da página na aba Network antes e depois. Anote os dois números: eles entram na atividade assíncrona.
Crie os três arquivos na raiz do projeto: o .gitignore da seção 5.4, o .nojekyll vazio e o README.md da seção 9, com os seus dados. Deixe o campo do Lighthouse em branco por enquanto — você o preenche no Passo 10.
No terminal integrado do VS Code (Ctrl+'), dentro da pasta site-evento/:
Terminal
gitinit
gitstatus
O git status lista todos os arquivos como não rastreados. Confirme que img-originais/ não aparece — se aparecer, o .gitignore está com o nome errado ou fora da raiz.
Agora o primeiro commit:
Terminal
gitadd.
gitcommit-m"Site do evento academico completo, pronto para publicacao"
E confira:
Terminal
gitlog--oneline
Uma linha com um código de sete caracteres e a sua mensagem. Esse código é o identificador do commit.
Pegue o celular, desligue o Wi-Fi e abra o endereço público usando dados móveis. Navegue pelas cinco páginas, abra o menu, use a busca da programação e preencha o formulário de inscrição.
Se tudo funcionar aí, o seu site está no ar de verdade. Mande o link para alguém que não viu o projeto antes e peça para abrir.
A1. Cite as três diferenças entre o ambiente local e o servidor que mais quebram sites recém-publicados, e a regra que evita cada uma.
A2. Por que o arquivo de entrada precisa se chamar index.html e ficar na raiz do repositório? O que o servidor faz quando não encontra esse arquivo?
A3. Explique, para alguém que nunca ouviu falar do assunto, a diferença entre hospedagem, domínio e certificado HTTPS. Dê um exemplo de cada um no endereço https://ana.github.io/site-evento/.
A4. Qual a diferença entre href="/css/estilo.css" e href="css/estilo.css" em um site publicado em usuario.github.io/site-evento/? Qual dos dois funciona, e por quê?
A5. Para que serve o .gitignore? Cite três entradas típicas e explique o que aconteceria sem elas.
A6. Escreva, na ordem correta, a sequência completa de comandos para transformar uma pasta com o site em um repositório publicado no GitHub — do git init ao git push -u origin main.
A7. Quais são as quatro categorias avaliadas pelo Lighthouse e qual é a meta de cada uma nesta trilha? Cite duas causas comuns de nota baixa em cada uma.
A8. Por que a auditoria do Lighthouse deve ser feita no site publicado, em janela anônima e no modo Mobile? O que cada uma dessas três escolhas evita?
A9. O que fazem os atributos width, height e loading="lazy" em uma tag <img>? Em qual imagem da sua página inicial você não deve usar loading="lazy", e por quê?
A10. Para que servem as metatags og:? Por que a og:image precisa de URL absoluta, enquanto o src de uma <img> pode ser relativo?
B1. Publique o site do evento no GitHub Pages e produza um registro do processo: uma captura de tela de cada etapa (repositório criado, primeiro push, tela do Pages, site no ar) e um parágrafo descrevendo um problema que você encontrou e como o resolveu.
Resultado esperado: um arquivo publicacao.md no repositório, com as quatro capturas e o relato; o site acessível pela URL pública em janela anônima.
Dica
No GitHub, arraste a imagem para dentro do editor de arquivos: ele faz o upload e insere o Markdown sozinho. Se preferir versionar as capturas, crie img/capturas/ e referencie com caminho relativo.
B2. Otimize todas as imagens do projeto: redimensione, converta para WebP, aplique loading="lazy" onde couber e declare width e height em todas. Registre o peso total da página inicial antes e depois, com as capturas da aba Network.
Resultado esperado: redução de pelo menos 60% no peso total transferido da página inicial, sem perda visível de qualidade; nenhuma imagem sem alt, width e height.
Dica
Na aba Network, o rodapé mostra transferred (o que veio pela rede) e resources (o tamanho descompactado). Compare o primeiro. Marque Disable cache antes de medir, senão a segunda medição vem do cache e não significa nada.
B3. Rode o Lighthouse no site publicado, registre as quatro pontuações iniciais, aplique todas as correções sugeridas pelo relatório e registre as pontuações finais. Escreva um parágrafo sobre qual correção teve o maior impacto e por quê.
Resultado esperado: uma tabela antes/depois com as quatro notas no README.md e as metas da trilha atingidas.
Dica
Abra cada item reprovado no relatório: o Lighthouse mostra exatamente quais elementos causaram o problema e estima quantos milissegundos você ganha ao corrigir. Comece pelos de maior estimativa.
B4. Publique o mesmo projeto nas três plataformas (GitHub Pages, Netlify e Vercel) e compare: passos necessários, tempo até ficar no ar, o que cada painel oferece e o que faltou em cada um.
Resultado esperado: três URLs funcionais e uma tabela comparativa de no máximo quatro colunas, com sua conclusão sobre qual usar em cada situação.
Dica
Na Netlify e na Vercel, importe o mesmo repositório do GitHub — não recrie o projeto. Como o site é estático, o campo de comando de build fica vazio e o diretório de publicação é a raiz.
B5. Escreva o README.md completo do projeto seguindo o modelo da seção 9, acrescentando uma captura de tela da página inicial logo abaixo do título e uma lista de tarefas com - [x] mostrando o que já está pronto e o que ficou para depois.
Resultado esperado: o README renderizado no GitHub, sem link quebrado e sem imagem faltando, legível por alguém que nunca viu o projeto.
Dica
Teste o resultado renderizado, não o texto-fonte: uma imagem referenciada com caminho errado aparece como um ícone quebrado. Caminhos no README são relativos à raiz do repositório.
C1. Faça uma auditoria cruzada: troque URLs com outra pessoa que esteja estudando esta trilha (um colega de turma, alguém de um grupo de estudos, ou uma troca combinada em algum fórum de desenvolvimento) e cada um audita o site do outro. Sem ninguém para trocar? Audite o site de um projeto público que você admire, com os mesmos critérios. Produza um relatório de uma página com as quatro notas do Lighthouse (mobile e desktop), o resultado da validação W3C de todas as páginas, a navegação completa por teclado (o que quebrou), o teste com zoom em 200% e uma lista priorizada de cinco problemas com a correção sugerida para cada um.
Dica
Auditar site alheio é mais fácil que auditar o próprio: você não tem os atalhos mentais de quem construiu. Comece pelo teclado — desconecte o mouse de verdade — e depois rode as ferramentas. Escreva os problemas de forma acionável: "o botão de filtro não recebe foco visível" vale mais do que "acessibilidade ruim".
C2. Configure o repositório com um fluxo de trabalho por branch: crie uma branch com uma melhoria (por exemplo, um modo escuro no CSS), publique-a, abra um pull request descrevendo a mudança, peça a revisão de outra pessoa (ou revise você mesmo com calma, no dia seguinte) e só então faça o merge na main. Documente o processo com as URLs do PR e das revisões.
Dica
Os comandos são git switch -c melhoria/modo-escuro, os commits normais e git push -u origin melhoria/modo-escuro. O GitHub oferece o botão "Compare & pull request" sozinho quando detecta a branch nova. O Capítulo 02 da trilha Deploy tem o fluxo completo com revisão.
O repositório abaixo quase funciona no Live Server do Windows e chega ao GitHub Pages sem estilo, sem imagens e com o JavaScript morto. São cinco linhas com defeito, e nenhum defeito é erro de sintaxe: todos são consequência das diferenças entre a sua máquina e um servidor Linux servindo o site em um subdiretório. Encontre as cinco lendo apenas as abas Console e Network do DevTools, sem baixar o repositório.
Os cinco defeitos estão listados, cada um com o sintoma exato que aparece no DevTools (mensagem literal do console ou status da aba Network).
Cada defeito tem a correção escrita, com o antes e o depois da linha.
Um parágrafo explica por que quatro dos cinco passam despercebidos no Live Server do Windows — e identifica qual é o quinto, que falha em qualquer ambiente.
A regra geral que evitaria os cinco de uma vez está enunciada em uma frase.
Pistas
Duas abas bastam: no Console aparecem os erros de script; na Network, o status de cada arquivo pedido.
Compare, letra a letra, o nome do arquivo no HTML e o nome real no repositório. Sistemas de arquivos do Linux não perdoam.
Qual é a raiz de usuario.github.io/site-evento/? Onde uma barra inicial faz o navegador procurar?
Um dos cinco defeitos nem chega a virar requisição HTTP: o navegador se recusa a buscar aquele endereço a partir de uma página web.
O Lighthouse dá nota 100 em acessibilidade para páginas que um usuário de leitor de tela não consegue usar — ele testa o que é automatizável, e isso é menos da metade do problema. Neste desafio você faz as duas coisas: tira 100 na ferramenta e prova que o site funciona sem mouse e sem enxergar a tela.
Critérios de pronto
As cinco páginas do site publicado marcam 100 em acessibilidade no Lighthouse, em Mobile e em Desktop.
Um vídeo de até três minutos mostra a navegação completa de uma tarefa (abrir a programação, buscar uma palestra, ir à inscrição e preencher o formulário) usando apenas o teclado, com o foco visível em cada parada.
O mesmo percurso é feito com um leitor de tela (NVDA no Windows, VoiceOver no macOS ou Orca no Linux) e relatado em texto: o que foi anunciado, o que ficou mudo, o que confundiu.
Um relatório lista pelo menos três problemas que o Lighthouse não detectou e as correções aplicadas.
Todas as correções estão publicadas no site no ar, não apenas no repositório local.
Pistas
Comece pelo relatório do Lighthouse: a seção "Additional items to manually check" lista justamente o que a ferramenta não consegue testar sozinha.
Ordem de tabulação, foco preso dentro de um menu aberto, mensagens que aparecem sem serem anunciadas e imagens decorativas com alt descritivo são os quatro problemas invisíveis mais comuns.
No NVDA, Insert+F7 abre a lista de elementos da página: se os seus títulos e links não fizerem sentido fora do contexto visual, o problema aparece ali imediatamente.
Uma região aria-live só anuncia mudanças se já existir no HTML quando a página carrega. Criar o elemento junto com a mensagem faz o leitor de tela ignorá-la.
A página inicial de um site de evento não deveria pesar mais que um aplicativo de celular. Neste desafio, a meta é objetiva e medida por ferramenta: a página inicial do seu site, carregada pela primeira vez em um navegador sem cache, transfere no máximo 500 KB no total — HTML, CSS, JavaScript, fontes e imagens somados — mantendo o mesmo visual e as mesmas funcionalidades.
Critérios de pronto
A aba Network, com Disable cache marcado e o perfil de rede em Fast 4G, mostra total transferido ≤ 500 KB na primeira carga da página inicial publicada.
Uma tabela de no máximo quatro colunas mostra o peso por tipo de recurso (documento, estilo, script, imagem, fonte), antes e depois.
O desempenho no Lighthouse mobile é ≥ 90, e a métrica de estabilidade visual (CLS) fica abaixo de 0,1.
Nenhuma funcionalidade foi removida para atingir a meta, e nenhuma imagem ficou visivelmente pior. As decisões de compressão estão justificadas em um parágrafo.
Um parágrafo final explica qual foi a otimização de maior impacto e quantos KB ela economizou.
Pistas
Ordene a aba Network pela coluna de tamanho, em ordem decrescente. Os três primeiros itens costumam responder por 80% do peso; comece por eles.
Fontes do Google Fonts entram com vários pesos por padrão. Cada peso é um arquivo. Dois pesos bastam para quase todo projeto — e uma fonte do sistema custa zero byte.
<img> aceita várias fontes com srcset e sizes: o navegador escolhe o arquivo certo para o tamanho da tela, e o celular deixa de baixar a imagem do desktop.
Ícones em SVG inline no HTML economizam uma requisição inteira cada. Se você usa uma biblioteca de ícones inteira por causa de cinco ícones, esse é o corte mais fácil da lista.
Este é o desafio que fecha a Unidade 3 e esta trilha. A regra é simples e desconfortável: um tema novo, do zero, publicado e auditado em três horas. Sem reaproveitar o código do site do evento nem o do seu projeto autoral — só o que está na sua cabeça e a documentação aberta.
Escolha um tema que você nunca usou (catálogo de plantas do Pantanal, agenda de quadras do bairro, mural de estágios, brechó, controle de pescarias, cardápio de restaurante) e construa um site de três páginas que exercite tudo o que esta trilha cobriu: HTML semântico, CSS com sistema de design e responsividade, JavaScript com listagem renderizada a partir de dados, busca e formulário validado. Marque o tempo do início ao fim.
Esta é a simulação mais honesta do que se pede em um teste técnico de estágio — e a prova, para você mesmo, de que o conteúdo entrou de verdade.
Critérios de pronto
Três páginas interligadas, HTML5 semântico, zero erros no validador do W3C nas três.
CSS externo com variáveis, layout em Grid ou Flexbox, responsivo em pelo menos duas larguras e estados :hover, :focus-visible e :active nos elementos interativos.
Uma listagem renderizada por JavaScript a partir de um array de objetos, com busca funcionando e estado vazio tratado.
Um formulário com validação em JavaScript, incluindo pelo menos uma regra por expressão regular e mensagens de erro acessíveis por campo.
Publicado no GitHub Pages, em repositório público próprio, com README.md, .gitignore e página 404.html.
Lighthouse no site publicado: acessibilidade ≥ 90 e SEO ≥ 90.
Um RELATO.md no repositório com o tempo total gasto, o que você conseguiu de memória, o que precisou consultar e o que faria diferente com mais uma hora.
Pistas
Planeje quinze minutos antes de escrever a primeira linha: as três páginas, os campos do formulário e os cinco atributos dos objetos da listagem. Quem começa pelo CSS não termina.
Comece pelo HTML das três páginas inteiro, depois o CSS, depois o JavaScript. Trocar de camada o tempo todo custa mais do que parece.
Publique antes de terminar — no primeiro commit que renderiza alguma coisa. Um site incompleto no ar vale mais que um site completo que quebrou na publicação e não deu tempo de investigar.
Guarde os últimos trinta minutos para auditar: validador, Lighthouse, teclado e o teste no celular. É onde os pontos aparecem.
Para ir além: publique o repositório com um tópico (topic) weblab no GitHub e compare o seu resultado com o de outras pessoas que fizeram o mesmo desafio.
Parte 1 — Leitura (15 min). MILETTO & BERTAGNOLLI, Desenvolvimento de software II, capítulo sobre implantação. TERUEL, HTML 5 — Guia Prático, capítulo de publicação. Na documentação oficial do GitHub Pages, a página "Configuring a publishing source for your GitHub Pages site". Anote uma diferença entre o processo descrito no livro e o que você fez hoje.
Parte 2 — Produção (40 min). No seu projeto autoral:
Publique o projeto autoral no GitHub Pages, com repositório público e README.md completo conforme o modelo da seção 9.
Otimize todas as imagens e registre no README o peso total da página inicial antes e depois, com as duas capturas da aba Network.
Rode o Lighthouse no site publicado (Mobile, janela anônima), registre as quatro notas no README e aplique as correções necessárias até atingir as metas deste material (veja o Marco 3, logo abaixo).
Valide as páginas no validador do W3C até zero erros e crie a página 404.html personalizada.
Critério de pronto: o link público abre o site em qualquer máquina, em janela anônima; o console está limpo; o validador não aponta erros; o README traz o link do site, as quatro notas do Lighthouse e a comparação de peso.
Parte 3 — Discussão (5 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: poste o link do seu projeto autoral publicado, com uma frase sobre o problema mais difícil da publicação. Se puder, peça a outra pessoa para abrir o link, rodar o Lighthouse e devolver um elogio específico e uma sugestão acionável.
Guarde no seu repositório: o link do site publicado, registrado no README.
Escopo. Ao fim da Unidade 3, o seu projeto autoral — o mesmo dos Marcos 1 e 2, com as correções apontadas já aplicadas — precisa estar dinâmico e interativo com JavaScript (eventos, validação de formulários e consultas dinâmicas) e publicado na internet.
Requisitos.
#
Requisito
Onde foi estudado
1
JavaScript externo, carregado com defer, organizado em funções nomeadas
Aula 10
2
Código estruturado nos blocos ESTADO → ELEMENTOS → DADOS → RENDERIZAÇÃO → EVENTOS → INICIALIZAÇÃO
Aula 13
3
Dados do domínio em um array de objetos, em arquivo próprio
Aula 12
4
Listagem renderizada a partir desse array, com textContent, e estado vazio tratado
Aulas 12 e 13
5
Menu responsivo funcional em JavaScript, acessível por teclado, com aria-expanded
Aula 13
6
Delegação de eventos em pelo menos uma lista dinâmica
Aula 13
7
Busca com normalização de acentos e debounce, mais um filtro e uma ordenação
Aula 14
8
Formulário validado em JavaScript, com pelo menos uma regra por expressão regular
Aula 14
9
Mensagens de erro específicas por campo, com role="alert", aria-invalid e aria-describedby
Aula 14
10
Uso de condicionais, laços e operadores com valores calculados na página
Aulas 11 e 12
11
Zero erros no console durante o uso normal, nas páginas todas
Aulas 13 a 15
12
Repositório público no GitHub, com .gitignore, README.md e ao menos cinco commits descritivos
Aula 15
13
Site publicado, acessível por URL pública com HTTPS, e 404.html personalizada
Aula 15
14
Imagens otimizadas, metatags de SEO e Open Graph em todas as páginas
Aula 15
15
Zero erros no validador do W3C e Lighthouse com acessibilidade ≥ 90 no site publicado
Aula 15
Frameworks e bibliotecas de JavaScript (jQuery, React, Vue e similares) não entram aqui — o objetivo deste marco é demonstrar domínio da linguagem pura. O site publicado é o que fecha o marco: um projeto que só existe na sua máquina ainda não chegou lá.
Checklist de qualidade.
Interatividade construída com eventos, delegação e manipulação do DOM, não com gambiarras.
Formulário validado com regras claras, regex corretas e mensagens acessíveis (não só cor).
Consultas dinâmicas (busca, filtro, ordenação) com estados vazios tratados com uma mensagem de verdade, não uma tela em branco.
Código JavaScript organizado e legível — você deveria conseguir reabri-lo em seis meses e entender na hora.
Publicação completa: repositório com histórico de commits que conta uma história, README que orienta um estranho e site no ar com HTTPS.
Qualidade auditada: validador W3C e Lighthouse sem alertas ignorados.
Coerência do projeto como um todo — o visitante não percebe onde uma aula terminou e a outra começou.
Como saber que está pronto.
Abra o site publicado em uma janela anônima, em outro computador se possível: se abrir e funcionar sem nada da sua máquina, está no ar de verdade.
Rode o Lighthouse (Mobile) no site publicado: desempenho ≥ 80; acessibilidade, boas práticas e SEO ≥ 90.
No Console, use o site inteiro (busca, filtro, formulário) e confirme zero linhas vermelhas.
Peça para alguém preencher o formulário tentando errar de propósito: as mensagens de erro precisam fazer sentido para quem não escreveu o código.
Use IA para tirar dúvida ou revisar uma abordagem — não para gerar a lógica inteira. Se você não souber explicar por que os blocos do seu app.js estão naquela ordem, ainda não é seu.
Git — Livro Pro Git em português: https://git-scm.com/book/pt-br/v2 — leia os capítulos 1 e 2; são as duas horas mais bem investidas da sua vida de programador.
Quinze aulas atrás você abriu um arquivo vazio e escreveu <!DOCTYPE html> sem saber direito por quê. Hoje você tem, no ar, um site de cinco páginas com estrutura semântica, um sistema de design em CSS, layout responsivo, animações que respeitam quem prefere menos movimento, listagens geradas por JavaScript, busca, filtro, ordenação e um formulário que valida CPF de verdade. E, o mais importante: você entende cada linha, porque escreveu cada uma.
Guarde o repositório. Ele já é peça de portfólio — o tipo de link que se coloca em um currículo de estágio e que sobrevive a qualquer pergunta de entrevista, porque você sabe explicar o que fez.
O Nível 2 — Desenvolvimento Web tem esta trilha como pré-requisito e continua exatamente de onde você parou. Lá, o CSS escrito à mão ganha a companhia de frameworks (Bootstrap e Tailwind) e de SVG; o js/dados.js com o array fixo vira um fetch que busca dados de uma API real, com async/await, estados de carregamento e tratamento de erro; as cinco páginas HTML viram uma SPA, uma aplicação de página única com navegação sem recarregamento. E, na terceira unidade, o JavaScript sai do navegador: com Node.js e Express você escreve o servidor que responde às requisições, adiciona login com conta Google e constrói um CRUD completo, com dados que sobrevivem ao fechar do navegador.
O projeto fio-condutor de lá é o Café Cerrado, uma cafeteria fictícia que percorre o mesmo caminho: site estático, depois dinâmico, depois full-stack.
A trilha Deploy, para quando publicar virar rotina¶
Hoje você publicou clicando em botões. A trilha Deploy & Ferramentas é transversal e transforma isso em processo: o Capítulo 02 leva o seu Git de seis comandos a branches, conflitos e pull requests revisados; o Capítulo 03 aprofunda a publicação de sites estáticos com prévia por branch, cache e redirecionamentos; o Capítulo 09 automatiza tudo com GitHub Actions, para que cada git push valide, audite e publique sozinho.
Refaça o site do evento com outro tema, do zero. É o Boss desta aula, e é o exercício que mais consolida.
Contribua com um projeto de código aberto. Comece por uma correção de documentação em português: é uma contribuição real, e o fluxo de pull request é o mesmo dos projetos grandes.
Leia o código dos sites que você usa.Ctrl+U mostra o HTML de qualquer página. Você já entende boa parte dele — e a parte que não entende é a sua próxima lista de estudo.
Publique tudo o que fizer. Um projeto no ar vale mais que dez pastas na sua máquina. O hábito de terminar e publicar é, sozinho, uma vantagem competitiva.
Na próxima aula da sua trajetória — a Aula 01 do Nível 2 — Desenvolvimento Web — o Café Cerrado começa com um index.html vazio, exatamente como este começou. A diferença é que, desta vez, você já sabe o que fazer com ele. Bons deploys a todos.