Table of Contents
Programação Web 1¶
Author: Leo Fernandes, PhD
Version: 1.0
License: CC BY-NC-ND 4.0
Bem-vindo à versão aberta de Programação Web 1.
Este site é gerado a partir de arquivos Markdown e é otimizado para leitura em:
- 📱 Smartphones
- 💻 Navegadores de computador
- 🧾 Impressão / saída em PDF (via “Imprimir / Salvar PDF” no menu)
Como Navegar¶
- Comece pela Capa e Folha de Rosto.
- Em seguida, leia o Prefácio.
- Depois, siga os capítulos na ordem pela barra de navegação.
Lendo como PDF¶
Para gerar um PDF:
- Clique em “Imprimir / Salvar PDF” na navegação.
- O navegador abrirá uma versão do livro em página única.
- Use a função Imprimir do navegador:
- Destino: Salvar como PDF
- Layout: Retrato
- Margens: Padrão (ou “Nenhuma” para página cheia)
Isso funciona tanto em navegadores de desktop quanto de dispositivos móveis.
Sobre o Projeto¶
Este é um recurso educacional aberto.
Você pode ler, fazer fork, adaptar e contribuir por meio de pull requests, mas não pode usar este conteúdo comercialmente (ver detalhes da licença).
Se quiser contribuir:
- Faça fork deste repositório no GitHub.
- Edite ou adicione arquivos Markdown na pasta
docs/. - Envie um pull request descrevendo suas alterações.
📘 Programação Web 1¶
Subtitle: Uma Introdução à Web com HTML, CSS e Javascript
Author: Leo Fernandes, PhD
Institution: Instituto Federal de Educação, Ciência e Tecnologia de Alagoas (IFAL)
“Se eu vi mais longe, foi por estar sobre ombros de gigantes.” Isaac Newton
Programação Web 1¶
Uma Introdução à Web com HTML, CSS e Javascript
Author
Your Leo Fernandes, PhD
IFAL
leonardo.fernandes@ifal.edu.br
Edition: 1st Edition
Year: 2026
License: CC BY-NC-ND 4.0
Ela garante:
- o autor mantém todos os direitos autorais,
- ninguém pode vender este material,
- ninguém pode modificar diretamente (apenas através de contribuição, ou PR),
- e ainda assim o conteúdo é aberto para leitura e estudo.
:material-book-open-page-variant: Go to Preface
:material-arrow-left: Back to Cover
Prefácio¶
Este livro aberto foi criado para estudantes do curso [Técnico em Desenvolvimento de Sistemas do IFAL].
Ele foi projetado para ser:
- Aberto: hospedado no GitHub e livremente acessível.
- Vivo: atualizado conforme o curso evolui.
- Multimodal: combinando texto, código, imagens, vídeo e áudio.
Como Usar Este Livro¶
- Leia os capítulos na ordem se você é iniciante no assunto.
- Use o apêndice como referência rápida.
- Explore os links para vídeos e podcasts para um entendimento mais profundo.
:material-book-open-page-variant: Start Chapter 1 – Introduction
:material-arrow-left: Back to Title Page
Chapters
0. Como Usar Este Material¶
Este capítulo explica como você, estudante, deve consumir este material ao longo da disciplina.
Nosso objetivo é que você aprenda de forma eficiente, prática e moderna, combinando estudo individual, atividades guiadas e projetos reais.
0.1 Modelo de Aprendizagem da Disciplina¶
A disciplina utiliza três pilares pedagógicos:
- Ensino Híbrido
- Sala de Aula Invertida (Flipped Classroom)
- Aprendizagem Baseada em Problemas (PBL)
Essas abordagens tornam o processo mais ativo, prático e conectado ao mundo real do desenvolvimento web.
0.2 Ensino Híbrido¶
O ensino híbrido combina:
- Estudo online assíncrono (antes da aula)
- Atividades práticas presenciais (durante a aula)
Isso significa que:
- Você estuda o conteúdo teórico no seu ritmo, usando este livro, vídeos e exemplos.
- Você vem para a aula presencial para praticar, tirar dúvidas, programar e trabalhar em projetos.
O foco da aula presencial é fazer, não assistir a explicações longas.
0.3 Sala de Aula Invertida (Flipped Classroom)¶
A lógica é simples:
- Antes da aula:
- Ler o capítulo indicado
- Assistir aos vídeos curtos
-
Fazer pequenos exercícios de aquecimento
-
Durante a aula:
- Resolver problemas reais
- Programar em dupla ou em grupo
- Trabalhar no projeto da disciplina
-
Receber orientação do professor
-
Depois da aula:
- Revisar o conteúdo
- Registrar dúvidas
- Avançar no projeto
Essa abordagem aumenta sua autonomia e melhora a qualidade do tempo em sala.
0.4 Aprendizagem Baseada em Problemas (PBL)¶
Ao longo da disciplina, você será exposto a problemas reais, como:
- Criar uma página responsiva
- Consumir uma API
- Construir uma aplicação completa (pelo menos a parte de frontend)
Cada problema exige:
- Pesquisa
- Testes
- Discussão
- Tomada de decisão
- Implementação
Você aprende fazendo, não apenas lendo.
0.5 Materiais Multimodais¶
Este livro combina diferentes formatos:
- Texto**
- Exemplos de código
- Vídeos curtos
- Exercícios interativos
- Quizzes
- Editores de código embutidos
- Links para ferramentas externas
Cada formato tem um propósito:
- Texto → compreensão conceitual
- Código → prática imediata
- Vídeo → demonstração rápida
- Quizzes → autoavaliação
- Editores → experimentação
- Ferramentas externas → simulação do ambiente profissional
Como Consumir o Material Semana a Semana¶
1. Leia o capítulo indicado¶
O capítulo apresenta:
- Conceitos essenciais
- Exemplos
- Boas práticas
- Exercícios guiados
Leia com calma, marque dúvidas e teste os códigos.
2. Assista aos vídeos curtos¶
Os vídeos reforçam:
- Conceitos-chave
- Demonstrações práticas
- Passo a passo de ferramentas
Eles são curtos para facilitar revisão e consulta.
3. Faça os exercícios interativos¶
Você encontrará:
- Quizzes
- Caixas de código editáveis
- Mini-desafios
Esses exercícios ajudam a fixar o conteúdo antes da aula.
4. Venha para a aula preparado¶
A aula presencial é 100% prática.
Você vai:
- Programar
- Resolver problemas
- Trabalhar em projetos
- Tirar dúvidas
- Receber feedback
Se você não estudar antes, terá dificuldade em acompanhar.
Ferramentas Utilizadas na Disciplina¶
Todos os alunos devem:
- Usar o e-mail institucional
- Criar conta no GitHub
- Criar conta nas demais ferramentas quando necessário
A seguir, um guia rápido de cada ferramenta.
Google Classroom¶
Usado para:
- Comunicados
- Entregas formais
- Links para capítulos e vídeos
- Organização semanal
Você deve acessar o Classroom toda semana.
GitHub Classroom¶
Usado para:
- Entregar exercícios de programação
- Receber feedback automático
- Versionar código
- Trabalhar em repositórios individuais ou em grupo
Você precisa ter:
- Conta no GitHub
- SSH configurado (opcional, mas recomendado)
CodePen / JSFiddle¶
Usados para:
- Testar HTML, CSS e JavaScript rapidamente
- Criar protótipos
- Compartilhar exemplos
- Experimentar sem instalar nada
Essas ferramentas são perfeitas para iniciantes.
GitHub Projects¶
Usado para:
- Organizar tarefas
- Acompanhar o progresso do projeto
- Trabalhar com Kanban
- Aprender fluxo de trabalho profissional
Você usará GitHub Projects em equipe.
Figma¶
Usado para:
- Prototipação de interfaces
- Wireframes
- Design de telas
- Colaboração visual
Antes de programar, você vai desenhar.
Postman / Insomnia¶
Usados para:
- Testar APIs
- Enviar requisições
- Depurar erros
- Documentar endpoints
Essenciais para a disciplina de backend.
Checklist do Estudante¶
Antes de começar o semestre, você deve:
- Criar conta no GitHub
- Criar conta no Google Classroom
- Criar conta no Figma
- Criar conta no CodePen ou JSFiddle
- Instalar Postman ou Insomnia
- Configurar seu ambiente de desenvolvimento
- Ler este capítulo completo
Conclusão¶
Este material foi pensado para que você aprenda de forma:
- Autônoma
- Prática
- Guiada
- Profissional
Se você seguir o fluxo recomendado — estudar antes, praticar durante, revisar depois — terá um excelente aproveitamento e construirá projetos reais para o seu portfólio.
Se quiser, posso criar uma versão com cards, uma versão com quizzes, ou uma versão com vídeos embutidos para deixar esse capítulo ainda mais interativo.
Capítulo 1 — Introdução à Web e Ferramentas¶
Vídeo curto explicativo
(link será adicionado posteriormente)
1.1 — O que é a Web e como ela funciona¶
Vídeo:O que é e como funciona a internet
A Web é uma das maiores invenções tecnológicas da história humana. Ela conecta pessoas, empresas, governos, dispositivos e sistemas em escala global. Para uma pessoa desenvolvedora, compreender como a Web funciona por dentro não é apenas útil — é essencial. Sem esse entendimento, o desenvolvimento se torna limitado, superficial e dependente de “receitas prontas”. Com esse entendimento, o desenvolvedor ganha autonomia, capacidade de diagnóstico, visão arquitetural e domínio técnico.
A World Wide Web (WWW), frequentemente confundida no senso comum com a própria Internet, constitui, na realidade, um vasto sistema de informações globais que opera como uma camada de abstração de serviço sobre a infraestrutura física de redes. Enquanto a Internet refere-se estritamente à interconexão física global de computadores (hardware, cabos, roteadores) e aos protocolos de transporte de dados de baixo nível (como o TCP/IP), a Web é fundamentada em um conceito de hipermídia distribuída. Neste ecossistema digital, documentos e recursos — sejam eles textos, imagens ou aplicações — são identificados de forma única através de URIs (Uniform Resource Identifiers) e interconectados por meio de hiperlinks, criando uma "teia" complexa e não linear de informações navegáveis que transcendem as fronteiras geográficas dos servidores onde estão hospedados.
Do ponto de vista operacional, o funcionamento da Web baseia-se na arquitetura cliente-servidor, regida majoritariamente pelo protocolo de aplicação HTTP (Hypertext Transfer Protocol). O ciclo de vida de uma interação na Web inicia-se quando um "agente de usuário" (o cliente, tipicamente um navegador), submete uma requisição a um servidor remoto solicitando um recurso específico; este servidor processa o pedido e retorna uma resposta contendo o conteúdo solicitado — geralmente estruturado semanticamente em HTML e estilizado visualmente via CSS. O navegador, então, interpreta esses códigos recebidos para renderizar a interface gráfica final para o usuário, ocultando toda a complexidade da troca de dados subjacente.
Por que entender a arquitetura da Web é importante para uma pessoa desenvolvedora?¶
A Web é construída sobre uma série de camadas, protocolos e padrões que trabalham juntos para permitir que páginas, aplicações e serviços funcionem. Quando você entende essa arquitetura:
- consegue diagnosticar erros (404, 500, DNS, CORS, cache, etc.);
- compreende como otimizar desempenho (cache, compressão, CDN);
- entende como garantir segurança (HTTPS, certificados, cookies, headers);
- desenvolve aplicações mais robustas, escaláveis e acessíveis;
- consegue dialogar com equipes de backend, infraestrutura e segurança.
Em outras palavras: quem domina a arquitetura da Web domina o desenvolvimento moderno.
📜 Breve Histórico da Web¶
A gênese da World Wide Web remonta a março de 1989, nas instalações do CERN (Organização Europeia para a Pesquisa Nuclear), próximo a Genebra. Foi neste cenário que o cientista da computação britânico Sir Tim Berners-Lee redigiu a proposta inicial para um sistema de gestão de informações baseado em hipertexto, visando resolver a dificuldade de compartilhamento de dados entre cientistas de diferentes universidades. Em 1990, utilizando um computador NeXT, Berners-Lee desenvolveu as pedras angulares da Web: a linguagem HTML, o protocolo HTTP e o primeiro navegador (chamado WorldWideWeb). A materialização deste projeto ocorreu quando o primeiro website da história foi publicado, servindo como uma página explicativa sobre o próprio projeto. Em 1993, o CERN colocou o software da Web em domínio público, catalisando a explosão da Internet comercial. Quando criada, a web definia três tecnologias fundamentais: - HTML (HyperText Markup Language) — linguagem de marcação para documentos;
- HTTP (HyperText Transfer Protocol) — protocolo de comunicação;
- URL (Uniform Resource Locator) — identificador de recursos na Web. Essas três tecnologias continuam sendo a base da Web moderna.Com o tempo, novas tecnologias surgiram: - CSS (1996) — estilo e layout;
- JavaScript (1995) — interatividade;
- AJAX (2005) — páginas dinâmicas sem recarregar;
- APIs REST (anos 2000) — comunicação entre sistemas;
- HTML5 (2014) — multimídia, canvas, storage;
- WebAssembly (2017) — alto desempenho no navegador.Referência: CERN - The birth of the Web
1.1.1 — Cliente, Servidor e Navegador¶
A arquitetura da Web é fundamentada em um modelo de distribuição de tarefas conhecido como Cliente-Servidor (ver Figura Cliente-Servidor).
Para compreender o funcionamento da rede em um nível de engenharia de software, é imperativo dissociar os papéis funcionais de cada componente, entendendo que a comunicação entre eles é estritamente protocolada.

O Cliente (Client)¶
No contexto técnico, o cliente é a entidade ativa que inicia a comunicação. Ele não se define pelo hardware (o computador ou smartphone), mas sim pelo software que submete uma requisição de serviço. Na terminologia do protocolo HTTP, o cliente é frequentemente referido como User Agent (Agente de Usuário). Sua função primária é formatar mensagens de solicitação (Requests) seguindo padrões definidos — especificando método, cabeçalhos e corpo — e enviá-las através da rede para um endereço específico. Embora o navegador seja o exemplo mais comum, scripts de automação (como crawlers ou bots), aplicações móveis e interfaces de linha de comando (como cURL) também atuam como clientes.
O Servidor (Server)¶
O termo servidor possui uma dualidade semântica na informática. Fisicamente, refere-se ao hardware: computadores de alto desempenho, otimizados para operar ininterruptamente (24/7), equipados com redundância de armazenamento (RAID) e conexão de banda larga de alta capacidade. Logicamente, e mais importante para o desenvolvimento web, refere-se ao software servidor (como Apache, Nginx ou IIS). Este software atua como um processo daemon (processo de segundo plano) que "escuta" (listening) portas específicas da rede — tradicionalmente a porta 80 para HTTP e 443 para HTTPS. Ao receber uma requisição do cliente, o software servidor processa a lógica necessária, acessa bancos de dados se preciso, e devolve o recurso ou uma mensagem de erro.
O Navegador (Browser)¶
O navegador é uma implementação específica de um cliente HTTP, projetado para interação humana. Sua complexidade técnica reside no Motor de Renderização (Rendering Engine), um componente de software responsável por receber o fluxo de dados brutos do servidor (texto HTML, regras CSS, scripts JS) e transformá-los em uma representação visual interativa. O navegador compila esses dados na memória do dispositivo construindo a DOM (Document Object Model), uma árvore estrutural de objetos que o usuário pode visualizar e manipular. Exemplos de motores de renderização incluem o Blink (usado no Chrome e Edge), Gecko (Firefox) e WebKit (Safari).
1.1.2 — Requisições e Respostas (HTTP)¶
O protocolo HTTP (Hypertext Transfer Protocol) é o alicerce da comunicação entre clientes e servidores na Web. Embora muitas vezes invisível ao usuário final, ele é o mecanismo que possibilita a transferência de documentos, imagens, scripts, dados estruturados e praticamente qualquer tipo de recurso digital. Para uma pessoa desenvolvedora, compreender o funcionamento do HTTP não é apenas desejável — é indispensável. Sem esse entendimento, torna‑se impossível diagnosticar problemas de rede, otimizar desempenho, implementar segurança ou construir APIs robustas.
HTTP é um protocolo baseado em texto, sem estado (stateless) e orientado a requisições. Isso significa que cada interação entre cliente e servidor é independente, e o servidor não mantém memória das requisições anteriores, a menos que mecanismos adicionais sejam utilizados (cookies, tokens, sessões, etc.). Essa característica, embora simples, é fundamental para a escalabilidade da Web moderna. Cada troca de dados é tratada como uma transação independente e isolada, composta invariavelmente por dois elementos estruturais: uma Requisição (Request) enviada pelo cliente e uma Resposta (Response) devolvida pelo servidor.
A Estrutura de uma Requisição HTTP¶
Quando o navegador precisa obter um recurso — seja uma página HTML, um arquivo CSS, um script JavaScript ou uma imagem — ele envia uma requisição HTTP ao servidor. Essa requisição é composta por três partes principais:
1. Linha de requisição (Request Line)
Contém:
- Método HTTP (GET, POST, PUT, DELETE, etc.)
- Caminho do recurso
- Versão do protocolo
Exemplo:
GET /produtos HTTP/1.1
2. Cabeçalhos (Headers)
Os cabeçalhos fornecem metadados sobre a requisição, como:
- tipo de conteúdo aceito (
Accept) - idioma preferido (
Accept-Language) - informações do navegador (
User-Agent) - cookies
- autenticação
- cache
Exemplo:
Host: www.exemplo.com
User-Agent: Mozilla/5.0
Accept: text/html
3. Corpo da requisição (Body)
Nem toda requisição possui corpo.
Métodos como GET não enviam corpo, enquanto POST e PUT frequentemente enviam dados (formulários, JSON, arquivos).
A Estrutura de uma Resposta HTTP¶
Após processar a requisição, o servidor devolve uma resposta HTTP, composta por:
1. Linha de status (Status Line)
Inclui:
- versão do protocolo
- código de status
- mensagem textual
Exemplo:
HTTP/1.1 200 OK
2. Cabeçalhos de resposta
Informam:
- tipo de conteúdo (
Content-Type) - tamanho (
Content-Length) - políticas de cache (
Cache-Control) - cookies (
Set-Cookie) - segurança (
Strict-Transport-Security,X-Frame-Options)
3. Corpo da resposta
Contém o recurso solicitado: HTML, JSON, imagem, vídeo, etc.
Códigos de Status HTTP¶
Os códigos de status são fundamentais para diagnóstico e controle de fluxo. Eles são divididos em classes:
| Classe | Significado | Exemplos |
|---|---|---|
| 1xx | Informacional | 100 Continue |
| 2xx | Sucesso | 200 OK, 201 Created |
| 3xx | Redirecionamento | 301 Moved Permanently, 302 Found |
| 4xx | Erro do cliente | 400 Bad Request, 404 Not Found |
| 5xx | Erro do servidor | 500 Internal Server Error, 503 Service Unavailable |
Para desenvolvedores, compreender essas classes é essencial para depuração (localizar e corrigir erros ou bugs no software) e para a construção de APIs.
HTTP como Protocolo Stateless¶
A característica stateless significa que cada requisição é independente.
Isso traz vantagens:
- escalabilidade;
- simplicidade;
- paralelismo.
Mas também traz desafios:
- autenticação precisa ser reenviada;
- estado da aplicação deve ser mantido no cliente ou em mecanismos externos;
- sessões precisam de cookies ou tokens.
Essa limitação levou ao surgimento de tecnologias como:
- JWT (JSON Web Tokens)
- Cookies de sessão
- LocalStorage / SessionStorage
- APIs RESTful com autenticação stateless
📜 Evolução do HTTP¶
O HTTP passou por várias versões:
HTTP/1.1 (1997)
- Conexões persistentes
- Cabeçalhos mais ricos
- Amplamente utilizado até hojeHTTP/2 (2015)
- Multiplexação
- Compressão de cabeçalhos
- Server Push
- Melhor desempenhoHTTP/3 (2022)
- Baseado em QUIC (UDP)
- Redução de latência
- Melhor performance em redes instáveisA Web moderna está migrando gradualmente para HTTP/3, especialmente em serviços de grande escala (Google, Cloudflare, Meta).
1.1.3 — Endereçamento e Infraestrutura¶
Para que o ciclo de Requisição e Resposta (HTTP) ocorra com êxito, é necessário transpor uma barreira fundamental de comunicação: a localização exata do servidor na vasta topologia da rede global. A infraestrutura da Internet opera sobre um sistema numérico rigoroso, invisível ao usuário comum, mas essencial para o roteamento de dados: o Endereço IP (Internet Protocol).
Cada dispositivo conectado à rede, seja ele um servidor de alto desempenho ou um smartphone, recebe um identificador numérico único, análogo a uma coordenada geográfica ou um número telefônico.
Atualmente, coexistem dois padrões principais: o IPv4 (composto por quatro octetos, ex: 192.168.1.1) e o IPv6 (uma sequência hexadecimal mais longa, desenvolvida para suprir a escassez de endereços do padrão anterior).
É através destes endereços que os roteadores e switches sabem exatamente para onde direcionar os pacotes de dados.
No entanto, a memorização de sequências numéricas complexas é inviável para a cognição humana. Para solucionar este problema de usabilidade, foi implementada uma camada de abstração hierárquica e distribuída denominada DNS (Domain Name System). O DNS atua como uma lista telefônica dinâmica e descentralizada da Internet.
Quando um usuário digita um domínio mnemônico (como www.exemplo.com.br) na barra de endereços, o navegador inicia um processo denominado Resolução de Nomes. O sistema consulta servidores DNS recursivos e autoritativos em uma cadeia hierárquica até encontrar o Endereço IP correspondente àquele domínio. Somente após obter essa "tradução" do nome para o número IP é que o navegador consegue estabelecer a conexão TCP/IP real com o servidor e enviar a requisição HTTP. Todo esse processo complexo ocorre em milissegundos, tornando a experiência de navegação fluida e transparente.
O que acontece quando você digita uma URL no navegador?
Imagine que o usuário digita:
https://www.exemplo.com/produtos
-
Verificação do Cache Local
Antes de ir à web, o navegador tenta economizar tempo e banda verificando se já possui uma cópia recente do recurso solicitado.
Ele consulta cabeçalhos como:
- Cache-Control
- Expires
- ETag
Se o navegador encontrar uma versão válida no cache, ele não precisa acessar o servidor. Se não encontrar, ele segue para a próxima etapa.
-
Resolução de nomes (DNS)
O navegador precisa transformar o nome do domínio:
www.exemplo.comEm um endereço IP, como:
- IPv4 →
192.0.2.1 - IPv6 →
2001:db8::1
Essa conversão é feita pelo DNS (Domain Name System).
Como funciona o DNS?
- O navegador pergunta ao SO: “Você sabe o IP de www.exemplo.com?”
- Se o sistema não souber, consulta o servidor DNS configurado (provedor, Google, etc).
- O servidor DNS segue a cadeia hierárquica (Root → TLD → Authoritative).
- O servidor autoritativo responde com o IP correto.
- O navegador armazena a resposta (TTL).
DNS usa UDP ou TCP?
- Normalmente UDP porta 53 (rápido e leve).
- Em casos específicos, TCP (respostas grandes, DNSSEC).
- IPv4 →
-
Protocolo IP e suas versões
O endereço IP identifica dispositivos na rede.
IPv4
- 32 bits
- ~4 bilhões de endereços
- Exemplo:
192.168.0.1
IPv6
- 128 bits
- Quantidade praticamente infinita
- Exemplo:
2001:0db8:85a3::8a2e...
A Web moderna funciona com ambos, mas o IPv6 está crescendo rapidamente.
-
Estrutura da URL
Uma URL possui três partes principais:
https://www.exemplo.com/produtos- 1. Protocolo: Define a comunicação (`http://` ou `https://`).
- 2. Domínio: Nome registrado que aponta para um servidor (`www.exemplo.com`).
- 3. Caminho: Indica o recurso solicitado (`/produtos`).
-
Cliente envia requisição ao servidor
Com o IP em mãos, o navegador abre uma conexão (TCP ou QUIC) e envia a requisição:
GET /produtos HTTP/1.1 Host: www.exemplo.com -
Servidor responde
O servidor processa a requisição e devolve:
- Código de status (200, 404, 500…)
- Cabeçalhos
- Corpo da resposta (HTML, JSON, imagem, etc.)
-
Navegador renderiza a página
O processo final de renderização:
- Lê o HTML.
- Baixa recursos externos (CSS, JS, Imagens).
- Monta a árvore DOM.
- Aplica estilos e executa scripts.
- Exibe a página ao usuário.
Atividade de Revisão — Seção 1.1¶
1. Qual é a diferença fundamental entre a Internet e a World Wide Web (WWW)?
2. No contexto de uma requisição HTTP, o que indica um Código de Status da classe 4xx (como o 404)?
3. Antes de enviar uma requisição HTTP, o navegador precisa traduzir o nome do domínio (ex: www.site.com) em um endereço IP. Qual sistema é responsável por isso?
1.2 — Ferramentas Essenciais para Desenvolvimento Web¶
O desenvolvimento Web moderno exige mais do que apenas conhecer linguagens como HTML, CSS e JavaScript. Ele demanda um conjunto de ferramentas que ampliam a produtividade, facilitam o diagnóstico de problemas, automatizam tarefas e permitem versionar e compartilhar código de forma profissional. Nesta seção, exploraremos as ferramentas fundamentais que todo desenvolvedor Web deve dominar desde o início da sua formação.
1.2.1 — Navegadores e DevTools¶
Os navegadores modernos — como Google Chrome, Mozilla Firefox, Microsoft Edge e Safari — são muito mais do que simples programas para acessar páginas. Eles são verdadeiros ambientes de execução para aplicações Web, contendo motores de renderização, interpretadores JavaScript, mecanismos de segurança e ferramentas avançadas de inspeção.
Motores de Renderização Cada navegador utiliza um motor responsável por interpretar HTML, CSS e JavaScript:
- Blink (Chrome, Edge, Opera)
- Gecko (Firefox)
- WebKit (Safari)
Esses motores convertem código em interfaces visuais, manipulam o DOM (Document Object Model), aplicam estilos e executam scripts. Entender como eles funcionam ajuda a diagnosticar problemas de compatibilidade e desempenho.
DevTools: o laboratório do desenvolvedor
Vídeo: O que é DevTools e como ele pode te ajudar
As Ferramentas de Desenvolvedor (DevTools) são um conjunto de utilitários integrados ao navegador que permitem:
- Inspecionar e editar o DOM em tempo real
- Visualizar e modificar CSS dinamicamente
- Monitorar requisições HTTP
- Analisar desempenho (Performance)
- Depurar JavaScript (Debugging)
- Verificar acessibilidade
- Simular dispositivos móveis
- Monitorar armazenamento local (LocalStorage, Cookies, IndexedDB)
O DevTools é indispensável para qualquer desenvolvedor Web. Ele transforma o navegador em um ambiente de experimentação e diagnóstico, permitindo compreender o comportamento da aplicação em detalhes.
Para abrir o DevTools (Ferramentas do Desenvolvedor) no Chrome ou Firefox, utilize os atalhos universais F12 ou Ctrl+Shift+I (Windows/Linux) e Cmd+Opt+I (Mac). Alternativamente, clique com o botão direito em qualquer página e selecione "Inspecionar" ou acesse o menu de três pontos > "Mais Ferramentas" > "Ferramentas do desenvolvedor
![]()
1.2.2 — Editor de Texto - Opção Atual: VS Code¶
Vídeo: Como usar o VS CODE para programar?
O Visual Studio Code (VS Code) é hoje o editor de código mais utilizado no mundo. Ele combina leveza, extensibilidade e uma interface moderna, tornando-se ideal tanto para iniciantes quanto para profissionais.
Por que o VS Code é tão popular?
- Suporte nativo a HTML, CSS e JavaScript
- Terminal integrado
- Git integrado
- Depurador embutido
- Extensões para praticamente qualquer tecnologia
- Autocompletar inteligente (IntelliSense)
- Suporte a snippets e formatação automática
1.2.3 — Git e GitHub (visão inicial)¶
Vídeo: O QUE É GIT E GITHUB? - definição e conceitos importantes
Vídeo: COMO USAR GIT E GITHUB NA PRÁTICA! - desde o primeiro commit até o pull request!
O Git é um sistema de controle de versão distribuído. Ele permite que desenvolvedores acompanhem mudanças no código, revertam erros, criem ramificações (branches) e colaborem em projetos de forma segura e eficiente.
Por que aprender Git desde o início?
- Evita perda de código
- Permite trabalhar em equipe
- Facilita a organização de projetos
- É exigido em praticamente todas as vagas de TI
- É a base do GitHub Classroom, usado na disciplina
GitHub: a plataforma social do código
O GitHub é um serviço baseado em Git que permite:
- Hospedar repositórios
- Criar issues
- Fazer pull requests
- Criar wikis
- Automatizar tarefas com GitHub Actions
- Trabalhar em equipe
- Criar portfólio profissional
1.2.4 — Ambientes online (CodePen, JSFiddle)¶
Vídeo: Por dentro da ferramenta de programação CodePen
Ambientes online como CodePen, JSFiddle, JSBin e StackBlitz permitem testar código HTML, CSS e JavaScript diretamente no navegador, sem necessidade de instalar nada.
Por que usar esses ambientes?
- Ideal para experimentação rápida
- Perfeito para iniciantes
- Facilita o compartilhamento de exemplos
- Permite testar ideias sem criar arquivos locais
- Útil para depurar pequenos trechos de código
Atividades — Seção 1.2¶
- Quiz: Ferramentas e DevTools (link será adicionado)
- GitHub Classroom: Criar repositório inicial e enviar
hello.html(link será adicionado)
TODO - Revisar esta seção¶
1.3 — Estrutura de um Projeto Web¶
Vídeo curto explicativo
(link será adicionado posteriormente)
A organização de arquivos e pastas em um projeto Web é uma decisão prática que facilita desenvolvimento, correção de erros e entrega. Para estudantes iniciantes do curso de Sistemas de Informação, adotar uma estrutura simples e consistente desde os primeiros exercícios reduz o atrito ao trabalhar com código, permite executar o projeto localmente com facilidade e prepara o aluno para colaborar em repositórios. Nesta seção apresentamos princípios básicos e exemplos mínimos, sem entrar em conceitos avançados.
1.3.1 — Arquivos e pastas essenciais¶
Um projeto Web básico costuma agrupar artefatos por tipo. Cada grupo tem uma função clara:
- HTML — arquivos
.htmlque definem a estrutura das páginas; - CSS — arquivos
.cssque definem aparência e layout; - JavaScript — arquivos
.jsque adicionam interatividade; - assets — recursos estáticos como imagens e fontes;
- documentação —
README.mdcom instruções de execução e descrição do projeto.
Organizar dessa forma torna mais simples localizar onde alterar um texto, um estilo ou um comportamento, e facilita a configuração de ferramentas básicas (servidor local, controle de versão).
Exemplo de estrutura mínima:
meu-projeto/
├── index.html
├── css/
│ └── style.css
├── js/
│ └── script.js
└── assets/
├── images/
└── fonts/
1.3.2 — Estrutura mínima prática e como executar localmente¶
Para as primeiras atividades da disciplina, adote a estrutura mínima acima. Além dos arquivos, inclua:
README.md— instruções curtas: como abrir o projeto no navegador e dependências (se houver);.gitignore— para evitar versionar arquivos desnecessários (ex.:node_modules/se usar Node).
Como abrir localmente (modo simples):
- Abra a pasta do projeto no editor (por exemplo, VS Code).
- Clique com o botão direito em
index.htmle escolha “Open with Live Server” (se a extensão estiver instalada) ou abra o arquivo diretamente no navegador. - Se usar apenas o arquivo,
index.htmlfunciona sem servidor; para funcionalidades que exigem requisições (fetch), use um servidor local simples (Live Server,python -m http.server, etc.).
Exemplo mínimo de index.html:
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Projeto Exemplo</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<header>
<h1>Projeto Exemplo</h1>
</header>
<main>
<section>
<h2>Introdução</h2>
<p>Conteúdo inicial do projeto.</p>
</section>
</main>
<footer>
<p>© IFAL — Programação Web 1</p>
</footer>
<script src="js/script.js" defer></script>
</body>
</html>
Observações técnicas simples:
- Use defer ao incluir scripts para garantir que o HTML seja carregado antes da execução do JavaScript.
- Mantenha o lang no elemento <html> e o meta viewport para acessibilidade e responsividade básicas.
1.3.3 — Boas práticas simples e justificadas¶
Apresente-se ao hábito de seguir práticas que tornam o trabalho mais claro e profissional, mesmo em projetos iniciais:
- Nomes claros e sem espaços: use
kebab-case(ex.:meu-projeto,style.css). Evite acentos e espaços. - Separar por tipo: HTML em raiz ou
pages/, estilos emcss/, scripts emjs/, imagens emassets/images/. Isso facilita localizar arquivos. - Evitar código inline: prefira arquivos externos (
css/style.css,js/script.js) em vez de estilos e scripts dentro do HTML. Facilita leitura e reaproveitamento. - Comentários sucintos: comente trechos não óbvios para facilitar revisão (ex.:
/* função que atualiza a lista */). - README básico: inclua objetivo do projeto e instruções para abrir localmente; isso ajuda avaliadores e colegas.
- Não versionar arquivos gerados: se usar ferramentas que geram pastas (ex.:
dist/,node_modules/), inclua-as em.gitignore. - Otimizar imagens: use imagens com tamanho adequado; para exercícios, prefira formatos leves (JPEG/PNG otimizados) e nomes descritivos (
logo.png).
Essas práticas são simples, mas têm impacto direto: reduzem erros ao mover arquivos, evitam conflitos em sistemas de arquivos diferentes e tornam o projeto mais legível para quem for avaliá‑lo.
Atividades — Seção 1.3¶
- Quiz: Estrutura de projeto (link será adicionado)
- GitHub Classroom: Criar repositório com a estrutura mínima (
index.html,css/style.css,js/script.js,assets/) e incluirREADME.mdcom instruções de execução (link será adicionado)
:material-arrow-left: Back to Preface :material-arrow-right: Go to Chapter 2 – First Steps
Capítulo 2 — Fundamentos do HTML¶
Vídeo curto explicativo (link será adicionado posteriormente)
2.1 — Introdução ao HTML¶
Vídeo curto explicativo (link será adicionado posteriormente)
O HTML (HyperText Markup Language) constitui a linguagem de marcação que fundamenta toda a estrutura da Web. Antes de explorar seus elementos e regras sintáticas, é necessário compreender o que essa linguagem é — e, igualmente importante, o que ela não é — no contexto do desenvolvimento web moderno.
2.1.1 — O que é HTML¶
O HTML é uma linguagem de marcação (markup language), não uma linguagem de programação. Esta distinção é tecnicamente precisa e conceitualmente importante: linguagens de programação possuem estruturas de controle de fluxo (condicionais, laços de repetição), gerenciamento de estado e capacidade de realizar cômputo generalizado. O HTML, por sua vez, serve a um propósito distinto e mais específico: descrever a estrutura e o significado do conteúdo de um documento hipermídia.
O termo hipertexto (do inglês hypertext) refere-se à capacidade de interligar documentos por meio de referências navegáveis — os hiperlinks —, rompendo a linearidade característica dos documentos impressos e criando a "teia" não linear que deu nome à World Wide Web. O termo marcação (markup) remete à prática editorial de anotar manuscritos com instruções de formatação; no contexto digital, essas anotações são as tags HTML, que delimitam e descrevem os fragmentos de conteúdo.
Do ponto de vista histórico, o HTML foi concebido por Tim Berners-Lee em 1991 como uma aplicação simplificada do SGML (Standard Generalized Markup Language), norma ISO para estruturação de documentos técnicos. A versão que fundamenta o desenvolvimento web contemporâneo é o HTML5, cuja especificação foi publicada pelo W3C (World Wide Web Consortium) em 2014 e é mantida atualmente pelo WHATWG (Web Hypertext Application Technology Working Group) sob o modelo de living standard — uma especificação em evolução contínua, sem versionamento fixo.
Referência: WHATWG — HTML Living Standard
É fundamental compreender que o HTML não controla a aparência visual das páginas — essa responsabilidade pertence ao CSS — nem o comportamento interativo, que é domínio do JavaScript. O HTML define exclusivamente o quê o conteúdo é: um título, um parágrafo, uma lista, uma imagem, um link. Esta separação de responsabilidades é um dos princípios arquiteturais mais importantes do desenvolvimento web moderno.
📜 Breve Histórico do HTML¶
- HTML 1.0 (1991) — Proposta inicial de Berners-Lee; cerca de 18 tags.
- HTML 2.0 (1995) — Primeira especificação formal pelo IETF.
- HTML 3.2 (1997) — Incorporou tabelas, applets e elementos de apresentação.
- HTML 4.01 (1999) — Introduziu separação entre estrutura e apresentação; adoção do CSS.
- XHTML 1.0 (2000) — Reformulação do HTML 4 com sintaxe XML estrita.
- HTML5 (2014) — Revisão profunda: semântica, multimídia nativa, APIs JavaScript, formulários avançados.
- HTML Living Standard (atual) — Mantido pelo WHATWG; atualizado continuamente.
2.1.2 — Estrutura básica (<!DOCTYPE>, <html>, <head>, <body>)¶
Todo documento HTML válido obedece a uma estrutura hierárquica mínima, composta por quatro elementos obrigatórios. Compreender a função de cada um é o ponto de partida para qualquer prática de desenvolvimento web.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Título do Documento</title>
</head>
<body>
<p>Conteúdo visível da página.</p>
</body>
</html>
Cada componente desta estrutura desempenha um papel específico e insubstituível:
<!DOCTYPE html>
A declaração DOCTYPE (Document Type Declaration) não é uma tag HTML propriamente dita, mas uma instrução de processamento dirigida ao navegador. Ela informa ao motor de renderização que o documento deve ser interpretado segundo a especificação do HTML5, ativando o chamado standards mode (modo de conformidade com os padrões). Na ausência desta declaração, o navegador pode ativar o quirks mode — um modo de compatibilidade retroativa que emula comportamentos de navegadores antigos —, produzindo resultados de renderização imprevisíveis. Por esta razão, <!DOCTYPE html> deve ser sempre a primeira linha de qualquer documento HTML.
<html lang="...">
O elemento <html> é o elemento raiz (root element) do documento: todos os demais elementos são seus descendentes. O atributo lang declara o idioma primário do conteúdo textual do documento, utilizando códigos definidos pela norma BCP 47 (ex.: pt-BR para português brasileiro, en-US para inglês americano). Esta declaração é imprescindível para que leitores de tela utilizem o mecanismo de síntese de voz correto, para que ferramentas de tradução automática identifiquem o idioma de origem e para a correta aplicação de regras tipográficas (hifenização, aspas, etc.).
<head>
O elemento <head> constitui o cabeçalho do documento — distinto do cabeçalho visual da página. Ele contém metadados: informações sobre o documento que não são exibidas diretamente ao usuário, mas são consumidas pelo navegador, por mecanismos de busca e por outras aplicações. Os metadados mais comuns incluem:
| Elemento / Atributo | Função |
|---|---|
<meta charset="UTF-8"> |
Define a codificação de caracteres do documento. UTF-8 é o padrão universal e suporta praticamente todos os sistemas de escrita. |
<meta name="viewport" ...> |
Controla o comportamento do viewport em dispositivos móveis. Essencial para o design responsivo. |
<title> |
Define o título do documento, exibido na aba do navegador e nos resultados de busca. |
<link rel="stylesheet" href="..."> |
Vincula uma folha de estilos CSS externa ao documento. |
<meta name="description" content="..."> |
Fornece uma descrição resumida do conteúdo para mecanismos de busca. |
<body>
O elemento <body> contém todo o conteúdo que será efetivamente renderizado e exibido ao usuário: textos, imagens, links, formulários, tabelas, vídeos e todos os demais elementos visuais e interativos. É dentro do <body> que a maior parte do trabalho de marcação HTML ocorre.
Boa prática: O atributo
langno elemento<html>e a meta tagcharset="UTF-8"devem ser considerados obrigatórios em qualquer documento HTML, não opcionais. Sua omissão constitui uma falha técnica com impacto direto em acessibilidade e internacionalização.
2.1.3 — Tags, atributos e elementos¶
A compreensão precisa da terminologia HTML é fundamental para evitar ambiguidades na comunicação técnica e para interpretar corretamente a documentação especializada.
Tags
Uma tag é a unidade sintática básica do HTML. Ela é delimitada pelos caracteres < e > e pode ser de dois tipos:
- Tag de abertura:
<p>— indica o início de um elemento. - Tag de fechamento:
</p>— indica o término de um elemento, sendo distinguida pela barra/após o caractere<. - Tag vazia (void element):
<img />,<br />,<meta />— elementos que não possuem conteúdo interno e, portanto, não requerem tag de fechamento.
Elementos
Um elemento HTML é a unidade semântica completa, composta pela tag de abertura, pelo conteúdo (quando presente) e pela tag de fechamento:
<p>Este é o conteúdo do parágrafo.</p>
↑ ↑
Tag de abertura Tag de fechamento
└──────────── Elemento completo ────────────┘
Os elementos podem ser aninhados (nested), ou seja, um elemento pode conter outros elementos, desde que o aninhamento seja realizado de forma correta — sem sobreposição de tags:
<!-- Aninhamento CORRETO -->
<p>Texto com <strong>ênfase forte</strong> no meio.</p>
<!-- Aninhamento INCORRETO — tags sobrepostas -->
<p>Texto com <strong>ênfase</p></strong>
Atributos
Os atributos fornecem informações adicionais sobre um elemento. Eles são declarados dentro da tag de abertura, na forma de pares nome="valor":
<a href="https://www.exemplo.com" target="_blank" rel="noopener noreferrer">
Visitar exemplo
</a>
No exemplo acima, href, target e rel são atributos do elemento <a>. Cada atributo modifica ou complementa o comportamento e o significado do elemento ao qual pertence. Alguns atributos são globais — aplicáveis a qualquer elemento HTML (ex.: id, class, lang, title, hidden) — enquanto outros são específicos de determinados elementos (ex.: href é exclusivo de <a> e <link>; src é exclusivo de <img>, <script>, <iframe>, entre outros).
Nota técnica: Em HTML5, as aspas ao redor dos valores de atributos são tecnicamente opcionais para valores sem espaços. Contudo, sua utilização é fortemente recomendada como boa prática, pois garante consistência sintática e previne ambiguidades em casos como
<input type=text name=meu campo>, ondemeu camposeria interpretado incorretamente.
Árvore DOM (Document Object Model)
Quando o navegador carrega um documento HTML, ele não o interpreta como uma sequência linear de texto, mas o converte em uma estrutura de dados hierárquica em memória denominada DOM (Document Object Model). O DOM representa o documento como uma árvore de nós, em que cada elemento, atributo e fragmento de texto corresponde a um nó. Esta representação é o que permite ao JavaScript manipular dinamicamente o conteúdo e a estrutura da página.
document
└── html
├── head
│ ├── meta (charset)
│ └── title
│ └── "Título"
└── body
└── p
└── "Conteúdo visível."
A compreensão do DOM é essencial não apenas para o JavaScript, mas também para a correta interpretação de seletores CSS e para o diagnóstico de problemas estruturais via DevTools.
Atividades — Seção 2.1¶
1. Qual é a função principal do elemento <head> em um documento HTML?
2. O que ocorre quando um documento HTML é carregado sem a declaração <!DOCTYPE html>?
3. Qual das alternativas apresenta a diferença correta entre uma tag e um elemento HTML?
- GitHub Classroom: Criar página HTML mínima (link será adicionado)
2.2 — Tags Essenciais¶
Vídeo curto explicativo (link será adicionado posteriormente)
Com a estrutura básica do documento compreendida, é possível avançar para os elementos responsáveis pela marcação do conteúdo em si. As chamadas tags essenciais são aquelas de uso mais frequente no desenvolvimento web e constituem o vocabulário fundamental que todo desenvolvedor deve dominar.
2.2.1 — Títulos, parágrafos e textos¶
Títulos (<h1> a <h6>)
O HTML define seis níveis de títulos, numerados de 1 a 6, em ordem decrescente de importância hierárquica. O elemento <h1> representa o título principal do documento ou da seção de maior nível, enquanto <h6> representa o subtítulo de menor relevância hierárquica.
<h1>Título principal da página</h1>
<h2>Seção principal</h2>
<h3>Subseção</h3>
<h4>Subseção de terceiro nível</h4>
<h5>Subseção de quarto nível</h5>
<h6>Subseção de quinto nível</h6>
Atenção crítica: Os títulos não devem ser escolhidos com base em seu tamanho visual padrão — isso é responsabilidade do CSS. Eles devem refletir fielmente a hierarquia lógica do conteúdo. Um erro frequente entre iniciantes é utilizar
<h3>porque "o tamanho parece adequado" em vez de<h2>, violando a estrutura hierárquica. Leitores de tela utilizam a hierarquia de títulos como mecanismo primário de navegação pelo documento; saltar níveis (ex.: de<h1>diretamente para<h3>) constitui uma falha de acessibilidade.
Parágrafos (<p>)
O elemento <p> delimita um parágrafo de texto. O navegador aplica, por padrão, margens verticais entre parágrafos consecutivos. É importante destacar que quebras de linha inseridas no código-fonte HTML (tecla Enter) não produzem quebras de linha no documento renderizado — o HTML ignora espaços em branco e quebras de linha extras no código.
<p>Este é o primeiro parágrafo. O HTML ignora
quebras de linha no código-fonte
e trata o texto como contínuo.</p>
<p>Este é o segundo parágrafo, separado do anterior por uma margem automática.</p>
Para forçar uma quebra de linha dentro de um parágrafo, utiliza-se o elemento vazio <br>. Entretanto, seu uso deve ser restrito a situações em que a quebra de linha faz parte do conteúdo (ex.: endereços postais, estrofes de poemas), e não como mecanismo de espaçamento — para isso, utiliza-se CSS.
Formatação semântica de texto
O HTML oferece um conjunto de elementos para conferir significado semântico a trechos de texto. É fundamental distinguir os elementos semânticos dos puramente visuais:
| Elemento | Significado semântico | Renderização padrão |
|---|---|---|
<strong> |
Importância ou urgência elevada | Negrito |
<em> |
Ênfase (muda o sentido da frase) | Itálico |
<mark> |
Trecho relevante para o contexto atual | Destaque amarelo |
<small> |
Texto de menor relevância (notas, direitos autorais) | Fonte menor |
<del> |
Conteúdo removido ou obsoleto | Tachado |
<ins> |
Conteúdo inserido ou acrescentado | Sublinhado |
<code> |
Fragmento de código-fonte | Fonte monoespaçada |
<abbr> |
Abreviação ou sigla | (variável) + tooltip |
<cite> |
Título de obra referenciada | Itálico |
<blockquote> |
Citação longa de fonte externa | Recuo |
<q> |
Citação curta, inline | Aspas automáticas |
<b> |
Destaque tipográfico sem importância semântica | Negrito |
<i> |
Voz alternativa, termos técnicos, estrangeirismos | Itálico |
<strong>vs.<b>e<em>vs.<i>: Esta é uma das distinções semânticas mais importantes do HTML5.<strong>indica que o trecho possui importância elevada no contexto do documento;<b>indica apenas destaque tipográfico convencional, sem implicação semântica. Da mesma forma,<em>indica ênfase que modifica o sentido da sentença, enquanto<i>marca texto em voz alternativa (termos técnicos, palavras estrangeiras, títulos de obras em linha). Leitores de tela podem alterar a entonação de voz para elementos<strong>e<em>, mas não para<b>e<i>.
<!-- Uso correto de strong e em -->
<p>É <strong>obrigatório</strong> fazer backup antes de prosseguir.</p>
<p>Ela <em>realmente</em> disse isso? (a ênfase muda o sentido)</p>
<!-- Uso correto de b e i -->
<p>O termo <i lang="la">lorem ipsum</i> é amplamente utilizado em tipografia.</p>
<p>Pressione o botão <b>Salvar</b> para confirmar.</p>
<!-- Uso correto de abbr -->
<p>A linguagem <abbr title="HyperText Markup Language">HTML</abbr> é mantida pelo WHATWG.</p>
2.2.2 — Links e navegação¶
O elemento de âncora <a> (anchor) é o mecanismo fundamental de navegação hipertextual — aquele que, conceitualmente, define a própria natureza da Web como uma rede de documentos interconectados.
Anatomia do elemento <a>
<a href="https://www.exemplo.com" target="_blank" rel="noopener noreferrer">
Texto do link
</a>
Os atributos mais relevantes do elemento <a> são:
| Atributo | Descrição |
|---|---|
href |
(Hypertext REFerence) Especifica o destino do link. Pode ser uma URL absoluta, uma URL relativa, uma âncora na mesma página (#id) ou um protocolo especial (mailto:, tel:). |
target |
Define onde o recurso vinculado será aberto. O valor _blank abre em nova aba; _self (padrão) abre na mesma aba. |
rel |
Descreve a relação entre o documento atual e o documento vinculado. |
download |
Indica que o recurso deve ser baixado em vez de navegado. |
hreflang |
Declara o idioma do documento de destino. |
URLs absolutas e relativas
<!-- URL absoluta: inclui protocolo e domínio completo -->
<a href="https://www.ifal.edu.br">Site do IFAL</a>
<!-- URL relativa: relativa ao documento atual -->
<a href="sobre.html">Sobre</a>
<a href="../index.html">Início</a>
<a href="/contato.html">Contato</a> <!-- relativa à raiz do site -->
<!-- Âncora interna: navega para elemento com id específico -->
<a href="#secao-2">Ir para a Seção 2</a>
<!-- Protocolos especiais -->
<a href="mailto:contato@exemplo.com">Enviar e-mail</a>
<a href="tel:+558200000000">Ligar agora</a>
Segurança com target="_blank"
A abertura de links em nova aba com target="_blank" requer atenção a uma questão de segurança: por padrão, a nova página pode acessar e manipular o objeto window da página de origem via window.opener. Para neutralizar este vetor de ataque (reverse tabnapping), é imperativo combinar target="_blank" com rel="noopener noreferrer":
<!-- SEGURO: noopener bloqueia acesso ao window.opener -->
<a href="https://site-externo.com" target="_blank" rel="noopener noreferrer">
Link externo seguro
</a>
Acessibilidade em links
O texto de um link deve ser descritivo e autoexplicativo fora de contexto, pois usuários de leitores de tela frequentemente navegam pela lista de links do documento sem ler o texto ao redor. Textos genéricos como "clique aqui" ou "saiba mais" constituem falhas de acessibilidade:
<!-- INCORRETO: texto não descritivo -->
<a href="/artigo.html">Clique aqui</a>
<!-- CORRETO: texto descritivo -->
<a href="/artigo.html">Leia o artigo completo sobre HTML semântico</a>
<!-- Quando necessário, use aria-label para complementar -->
<a href="/artigo.html" aria-label="Leia o artigo completo sobre HTML semântico">
Saiba mais
</a>
2.2.3 — Imagens e mídias¶
O elemento <img>
O elemento <img> incorpora imagens ao documento. Trata-se de um elemento vazio (void element): não possui tag de fechamento nem conteúdo interno.
<img
src="assets/images/diagrama-cliente-servidor.png"
alt="Diagrama da arquitetura cliente-servidor mostrando o fluxo de requisição e resposta HTTP"
width="800"
height="450"
loading="lazy"
/>
Os atributos fundamentais do elemento <img> são:
| Atributo | Obrigatoriedade | Descrição |
|---|---|---|
src |
Obrigatório | Caminho (URL absoluta ou relativa) do arquivo de imagem. |
alt |
Obrigatório | Texto alternativo que descreve a imagem. Exibido quando a imagem não pode ser carregada; lido por leitores de tela. |
width / height |
Recomendado | Dimensões em pixels. Previnem o layout shift durante o carregamento da página. |
loading |
Recomendado | lazy ativa o carregamento adiado (lazy loading), melhorando o desempenho. |
srcset |
Opcional | Define imagens alternativas para diferentes densidades de tela ou tamanhos de viewport. |
O atributo alt: importância e uso correto
O atributo alt merece atenção especial. Ele não é uma legenda — é uma descrição funcional da imagem para contextos em que ela não pode ser percebida visualmente. Seu uso correto segue as seguintes regras:
<!-- Imagem informativa: alt descreve o conteúdo e a função -->
<img src="grafico-vendas.png" alt="Gráfico de barras mostrando crescimento de 40% nas vendas do 2º semestre de 2025" />
<!-- Imagem decorativa: alt vazio (não omitido) indica que deve ser ignorada por leitores de tela -->
<img src="decoracao-fundo.png" alt="" />
<!-- INCORRETO: alt omitido — o leitor de tela lerá o nome do arquivo -->
<img src="grafico-vendas.png" />
<!-- INCORRETO: alt redundante — não acrescenta informação -->
<img src="logo.png" alt="imagem" />
Imagens com legenda: <figure> e <figcaption>
Quando uma imagem requer uma legenda visível associada, o padrão semântico correto utiliza os elementos <figure> e <figcaption>:
<figure>
<img
src="figures/01_cliente_servidor.png"
alt="Diagrama da arquitetura cliente-servidor com múltiplos dispositivos conectados a um servidor central"
width="700"
height="400"
/>
<figcaption>
Figura 1 — Arquitetura cliente-servidor: múltiplos clientes submetem requisições
a um servidor centralizado, que processa e retorna as respostas correspondentes.
</figcaption>
</figure>
O elemento <figure> representa conteúdo autônomo — frequentemente com legenda — que é referenciado pelo fluxo principal do documento, mas que poderia ser removido sem comprometer sua continuidade. <figcaption> fornece a legenda ou descrição do conteúdo do <figure>.
Formatos de imagem para a Web
A escolha do formato de imagem tem impacto direto no desempenho da página:
| Formato | Características | Uso recomendado |
|---|---|---|
| JPEG / JPG | Compressão com perdas; não suporta transparência | Fotografias e imagens com gradientes complexos |
| PNG | Compressão sem perdas; suporta transparência | Logotipos, ícones, imagens com texto, screenshots |
| SVG | Vetor escalável; código XML; muito leve | Ícones, logotipos, ilustrações, diagramas |
| WebP | Compressão superior ao JPEG/PNG; suporta transparência | Substituto moderno para JPEG e PNG |
| AVIF | Compressão ainda mais eficiente que WebP | Imagens de alta qualidade com menor tamanho |
| GIF | Suporta animações simples; paleta limitada | Animações simples (prefira <video> ou CSS) |
Vídeo e áudio nativos (<video> e <audio>)
O HTML5 introduziu suporte nativo a vídeo e áudio, eliminando a dependência de plugins externos como o Adobe Flash:
<!-- Elemento de vídeo com múltiplas fontes para compatibilidade -->
<video
controls
width="800"
height="450"
poster="thumbnail.jpg"
preload="metadata"
>
<source src="aula-intro.mp4" type="video/mp4" />
<source src="aula-intro.webm" type="video/webm" />
<!-- Fallback para navegadores sem suporte -->
<p>Seu navegador não suporta reprodução de vídeo.
<a href="aula-intro.mp4">Baixe o vídeo aqui</a>.
</p>
</video>
2.2.4 — Listas (ordenadas e não ordenadas)¶
O HTML define três tipos de listas, cada uma com semântica específica:
Lista não ordenada (<ul> — Unordered List)
Utilizada quando a ordem dos itens não é relevante para o significado do conteúdo. O navegador renderiza cada item (<li>) com um marcador visual (por padrão, um ponto preenchido), que pode ser personalizado via CSS.
<ul>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
</ul>
Lista ordenada (<ol> — Ordered List)
Utilizada quando a ordem dos itens é semanticamente significativa — instruções passo a passo, classificações, procedimentos, etc. O navegador renderiza os itens numerados sequencialmente por padrão.
<ol>
<li>Verificar o cache local do navegador</li>
<li>Realizar a resolução DNS do domínio</li>
<li>Estabelecer a conexão TCP com o servidor</li>
<li>Enviar a requisição HTTP</li>
<li>Receber e renderizar a resposta</li>
</ol>
O elemento <ol> aceita atributos que controlam a numeração:
<!-- Começar a contagem a partir de 5 -->
<ol start="5">
<li>Quinto item</li>
<li>Sexto item</li>
</ol>
<!-- Contagem regressiva -->
<ol reversed>
<li>Terceiro lugar</li>
<li>Segundo lugar</li>
<li>Primeiro lugar</li>
</ol>
<!-- Tipo de marcador: A, a, I, i, 1 (padrão) -->
<ol type="A">
<li>Opção A</li>
<li>Opção B</li>
</ol>
Lista de definições (<dl>, <dt>, <dd> — Description List)
Utilizada para pares termo–descrição (glossários, metadados, dicionários):
<dl>
<dt>HTTP</dt>
<dd>Protocolo de transferência de hipertexto; define as regras de comunicação entre clientes e servidores na Web.</dd>
<dt>DNS</dt>
<dd>Sistema de nomes de domínio; traduz nomes de domínio legíveis por humanos em endereços IP.</dd>
<dt>DOM</dt>
<dd>Modelo de objeto do documento; representação em árvore de um documento HTML em memória.</dd>
</dl>
Listas aninhadas
Listas podem ser aninhadas para representar hierarquias:
<ul>
<li>Frontend
<ul>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
</ul>
</li>
<li>Backend
<ul>
<li>Node.js</li>
<li>Python</li>
</ul>
</li>
</ul>
Atividades — Seção 2.2¶
1. Qual é a diferença semântica entre os elementos <strong> e <b>?
2. Por que o atributo alt em imagens é tecnicamente obrigatório, mesmo em imagens decorativas?
3. Em qual situação o uso de <ol> é semanticamente mais apropriado do que <ul>?
- GitHub Classroom: Criar página com textos, links, imagens e listas (link será adicionado)
2.3 — Estruturação de Conteúdo¶
Vídeo curto explicativo (link será adicionado posteriormente)
A marcação de elementos individuais — títulos, parágrafos, links, imagens — é apenas o primeiro nível do trabalho de estruturação em HTML. O segundo nível, igualmente importante, consiste em agrupar e organizar esses elementos em regiões lógicas e coerentes que reflitam a arquitetura informacional da página. É neste domínio que reside a essência do HTML semântico — conceito que será aprofundado no Capítulo 3, mas cujos fundamentos são introduzidos nesta seção.
2.3.1 — Divs e seções¶
O elemento <div>
O <div> (division) é um elemento de contêiner genérico, desprovido de qualquer significado semântico intrínseco. Seu único propósito é agrupar outros elementos para fins de estilização via CSS ou manipulação via JavaScript. Quando nenhum elemento semântico adequado existe para representar um determinado agrupamento, o <div> é a escolha correta.
<!-- Uso legítimo de div: contêiner para layout, sem semântica específica -->
<div class="grid-container">
<div class="coluna-principal">
<!-- conteúdo principal -->
</div>
<div class="coluna-lateral">
<!-- conteúdo lateral -->
</div>
</div>
Contudo, um erro recorrente — denominado pejorativamente de divitis na comunidade de desenvolvimento — consiste em utilizar <div> para todo e qualquer agrupamento, ignorando os elementos semânticos disponíveis no HTML5. Esta prática produz documentos estruturalmente opacos, inacessíveis e de difícil manutenção.
O elemento <span>
O <span> é o equivalente inline do <div>: um contêiner genérico sem semântica, utilizado para aplicar estilos ou comportamentos a fragmentos de texto dentro de um parágrafo ou outro elemento inline.
<p>
O código de status
<span class="destaque-codigo">404</span>
indica que o recurso solicitado não foi encontrado.
</p>
Quando usar <div> e <span>
A regra de uso é direta: utilize <div> e <span> somente quando nenhum elemento semântico for apropriado. Se existe um elemento HTML que descreva com precisão o conteúdo agrupado — <article>, <section>, <nav>, <aside>, etc. — esse elemento deve ser preferido.
2.3.2 — Cabeçalho, rodapé e navegação¶
O HTML5 introduziu um conjunto de elementos de seção estrutural (sectioning elements) que permitem delimitar as regiões funcionais canônicas de uma página web. Estes elementos não apenas organizam visualmente o layout, mas comunicam a função de cada região a navegadores, tecnologias assistivas e mecanismos de busca.
Importante: Os elementos de seção estrutural não possuem hierarquia visual automática — eles não empurram conteúdo, não aplicam espaçamento, não criam colunas. Toda a apresentação visual continua sendo responsabilidade do CSS. O que esses elementos oferecem é significado semântico sobre a função de cada região.
<header> — Cabeçalho
O elemento <header> representa o conteúdo introdutório de sua seção de escopo. Tipicamente contém o título principal, logotipo, subtítulo e/ou navegação primária. Importante: <header> não se limita ao cabeçalho global da página — ele pode ser usado como cabeçalho de um <article> ou <section> específico.
<!-- Cabeçalho global da página -->
<header>
<img src="logo.svg" alt="Logotipo IFAL" width="120" height="60" />
<h1>Programação Web 1</h1>
<nav>
<!-- navegação principal -->
</nav>
</header>
<!-- Cabeçalho de um artigo específico -->
<article>
<header>
<h2>Introdução ao HTML Semântico</h2>
<p>Publicado em <time datetime="2026-03-01">1º de março de 2026</time></p>
</header>
<p>Conteúdo do artigo...</p>
</article>
<nav> — Navegação
O elemento <nav> delimita um conjunto de links de navegação — seja a navegação principal do site, um menu de seções, um sumário ou links de paginação. Não é necessário marcar todos os grupos de links com <nav>; o elemento deve ser reservado para blocos de navegação de importância significativa.
<nav aria-label="Navegação principal">
<ul>
<li><a href="/">Início</a></li>
<li><a href="/capitulos">Capítulos</a></li>
<li><a href="/atividades">Atividades</a></li>
<li><a href="/sobre">Sobre</a></li>
</ul>
</nav>
<footer> — Rodapé
O elemento <footer> representa o rodapé de sua seção de escopo. Tipicamente contém informações como autoria, direitos autorais, links secundários, dados de contato e metadados sobre o conteúdo. Assim como <header>, pode ser usado tanto no nível global da página quanto no nível de um <article> ou <section>.
<footer>
<p>© 2026 IFAL — Instituto Federal de Alagoas</p>
<nav aria-label="Links do rodapé">
<a href="/politica-privacidade">Política de Privacidade</a>
<a href="/acessibilidade">Acessibilidade</a>
<a href="/contato">Contato</a>
</nav>
</footer>
2.3.3 — Agrupamento de conteúdo¶
Além do cabeçalho, navegação e rodapé, o HTML5 disponibiliza elementos para estruturar o corpo do conteúdo principal com precisão semântica.
<main> — Conteúdo principal
O elemento <main> delimita o conteúdo principal e único da página — o conteúdo diretamente relacionado ao seu tópico central, excluindo elementos repetidos em outras páginas (cabeçalho, navegação, rodapé, sidebars). Deve existir apenas um <main> por página e ele não deve ser descendente de <article>, <aside>, <header>, <footer> ou <nav>.
<article> — Conteúdo autônomo
O elemento <article> representa um conteúdo independente e autossuficiente — um fragmento que faria sentido publicado de forma isolada, como uma postagem de blog, um artigo jornalístico, um comentário de usuário ou uma ficha de produto.
<section> — Seção temática
O elemento <section> delimita uma seção temática genérica de um documento ou aplicação, tipicamente acompanhada de um título. Deve ser utilizado quando o conteúdo pode ser identificado como um grupo temático distinto, mas não constitui um conteúdo autônomo (que justificaria <article>).
<aside> — Conteúdo tangencial
O elemento <aside> representa conteúdo tangencialmente relacionado ao conteúdo ao seu redor — informações que poderiam ser removidas sem comprometer o fluxo principal. Exemplos: notas de rodapé, caixas de destaque, listas de links relacionados, publicidade, perfis de autor.
Estrutura de página completa com elementos semânticos
O exemplo a seguir ilustra a composição de uma página típica utilizando todos os elementos discutidos nesta seção:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Programação Web 1 — Capítulo 2</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<!-- Cabeçalho global da página -->
<header>
<a href="/" aria-label="Ir para a página inicial">
<img src="assets/images/logo.svg" alt="IFAL" width="80" height="40" />
</a>
<h1>Programação Web 1</h1>
<!-- Navegação principal -->
<nav aria-label="Navegação principal">
<ul>
<li><a href="/capitulos/01">Capítulo 1</a></li>
<li><a href="/capitulos/02" aria-current="page">Capítulo 2</a></li>
<li><a href="/capitulos/03">Capítulo 3</a></li>
</ul>
</nav>
</header>
<!-- Conteúdo principal: único por página -->
<main>
<!-- Artigo: conteúdo autônomo e identificável -->
<article>
<header>
<h2>Capítulo 2 — Fundamentos do HTML</h2>
<p>
Atualizado em
<time datetime="2026-03-01">1º de março de 2026</time>
</p>
</header>
<!-- Seção temática dentro do artigo -->
<section aria-labelledby="titulo-intro">
<h3 id="titulo-intro">2.1 — Introdução ao HTML</h3>
<p>O HTML é a linguagem de marcação que estrutura o conteúdo da Web...</p>
</section>
<section aria-labelledby="titulo-tags">
<h3 id="titulo-tags">2.2 — Tags Essenciais</h3>
<p>As tags essenciais constituem o vocabulário fundamental do HTML...</p>
<!-- Figura com legenda semântica -->
<figure>
<img
src="assets/figures/estrutura-html.png"
alt="Diagrama da estrutura hierárquica de um documento HTML"
width="600"
height="350"
loading="lazy"
/>
<figcaption>
Figura 2 — Representação hierárquica da estrutura de um documento HTML5.
</figcaption>
</figure>
</section>
<footer>
<p>
Autoria: Prof. [Nome] —
<a href="/licenca">Licença Creative Commons BY-SA 4.0</a>
</p>
</footer>
</article>
<!-- Conteúdo tangencial: relacionado mas não central -->
<aside aria-label="Recursos complementares">
<h2>Recursos complementares</h2>
<ul>
<li><a href="https://developer.mozilla.org/pt-BR/docs/Web/HTML">MDN Web Docs — HTML</a></li>
<li><a href="https://html.spec.whatwg.org/">WHATWG — HTML Living Standard</a></li>
<li><a href="https://validator.w3.org/">W3C Markup Validation Service</a></li>
</ul>
</aside>
</main>
<!-- Rodapé global da página -->
<footer>
<p>© 2026 IFAL — Instituto Federal de Alagoas. Material didático de uso livre.</p>
<nav aria-label="Links institucionais">
<a href="/acessibilidade">Acessibilidade</a>
<a href="/contato">Contato</a>
</nav>
</footer>
<script src="js/script.js" defer></script>
</body>
</html>
Mapa visual da estrutura semântica
<body>
├── <header> ← Cabeçalho global (logo, h1, nav principal)
│ └── <nav> ← Navegação principal
├── <main> ← Conteúdo principal (único por página)
│ ├── <article> ← Conteúdo autônomo (o capítulo em si)
│ │ ├── <header> ← Cabeçalho do artigo (h2, metadados)
│ │ ├── <section> ← Seção 2.1
│ │ ├── <section> ← Seção 2.2
│ │ └── <footer> ← Rodapé do artigo (autoria, licença)
│ └── <aside> ← Conteúdo tangencial (links relacionados)
└── <footer> ← Rodapé global (copyright, links inst.)
Validação do documento HTML
O W3C disponibiliza o Markup Validation Service, uma ferramenta online que verifica a conformidade sintática de documentos HTML com a especificação. A validação regular do código é uma prática profissional essencial e deve ser incorporada ao fluxo de desenvolvimento desde as primeiras atividades.
Referência: https://validator.w3.org/
Atividades — Seção 2.3¶
1. Qual é a distinção semântica entre os elementos <article> e <section>?
2. Por que o elemento <main> deve ocorrer apenas uma vez por página?
3. Qual dos cenários a seguir justifica semanticamente o uso do elemento <aside>?
- GitHub Classroom: Criar layout simples com
<header>,<main>e<footer>(link será adicionado)
:material-arrow-left: Voltar ao Capítulo 1 — Introdução à Web e Ferramentas :material-arrow-right: Ir ao Capítulo 3 — HTML Semântico e Acessibilidade
Capítulo 3 — HTML Semântico¶
Vídeo curto explicativo (link será adicionado posteriormente)
3.1 — O que é semântica no contexto do HTML¶
Vídeo curto explicativo (link será adicionado posteriormente)
A palavra semântica deriva do grego sēmantikós, que significa "relativo ao significado". No contexto da linguagem HTML, o termo designa a prática de escolher elementos de marcação não com base em sua aparência visual padrão, mas com base no significado que eles atribuem ao conteúdo que delimitam.
Esta distinção — aparentemente sutil — tem consequências técnicas profundas. Considere dois trechos de código que produzem resultado visual idêntico no navegador:
<!-- Abordagem não semântica -->
<div style="font-size: 2em; font-weight: bold;">Bem-vindo ao curso</div>
<!-- Abordagem semântica -->
<h1>Bem-vindo ao curso</h1>
Para o olho humano, o resultado renderizado pode ser indistinguível. Para um leitor de tela, para um mecanismo de busca e para o próprio navegador, contudo, a diferença é fundamental: o elemento <h1> comunica que aquele texto é o título principal do documento, enquanto o <div> estilizado comunica apenas uma instrução de formatação visual — sem qualquer significado estrutural intrínseco.
O HTML semântico, portanto, não é uma preferência estética ou uma convenção opcional: é a aplicação correta da linguagem conforme sua especificação. O WHATWG (Web Hypertext Application Technology Working Group), responsável pelo HTML Living Standard, define para cada elemento não apenas sua renderização padrão, mas sua semântica — isto é, o tipo de conteúdo que ele representa e o papel que desempenha na estrutura do documento.
3.1.1 — A separação entre estrutura, apresentação e comportamento¶
Um dos princípios arquiteturais mais importantes do desenvolvimento web moderno é a separação de responsabilidades entre as três tecnologias fundamentais da Web:
- HTML — define a estrutura e o significado do conteúdo
- CSS — define a apresentação visual
- JavaScript — define o comportamento interativo
O HTML semântico é a realização plena desse princípio na camada de marcação. Quando se utiliza um <h2> para marcar um subtítulo, está-se comunicando estrutura e significado — não aparência. O tamanho visual do <h2> pode ser alterado livremente via CSS; o que não muda é seu papel semântico no documento.
A violação desse princípio — utilizando <div> e <span> para tudo, e CSS para simular a aparência de títulos, listas e seções — produz documentos tecnicamente funcionais para usuários sem necessidades especiais, mas estruturalmente opacos: inacessíveis para tecnologias assistivas, pouco indexáveis por mecanismos de busca e de difícil manutenção.
3.1.2 — Por que a semântica importa: quatro dimensões¶
Acessibilidade
Tecnologias assistivas como leitores de tela (screen readers) — software utilizado por pessoas com deficiência visual para navegar na Web de forma auditiva ou tátil (via display Braille) — dependem inteiramente da estrutura semântica do documento para funcionar corretamente. Um leitor de tela constrói uma representação auditiva da página interpretando os elementos HTML: ele anuncia "título de nível 1", "lista com 5 itens", "link", "botão". Sem a semântica correta, essa representação se torna incoerente ou inacessível.
As diretrizes WCAG 2.1 (Web Content Accessibility Guidelines), publicadas pelo W3C, estabelecem que o uso correto de elementos semânticos é requisito de conformidade nos níveis A e AA — os níveis mínimos exigidos por legislações de acessibilidade digital em vários países, incluindo o Brasil (Lei Brasileira de Inclusão, Lei nº 13.146/2015).
Indexabilidade (SEO)
Os algoritmos dos mecanismos de busca modernos utilizam a estrutura semântica do documento para inferir a hierarquia e a relevância do conteúdo. Um <h1> possui peso semântico significativamente superior ao de um parágrafo; o conteúdo dentro de um <article> é tratado como conteúdo editorial primário; o conteúdo em um <nav> é reconhecido como navegação e não como conteúdo relevante para indexação. A semântica correta, portanto, influencia diretamente o posicionamento nos resultados de busca.
Manutenibilidade
Um documento HTML semanticamente estruturado é autoexplicativo. Ao analisar a hierarquia de elementos, qualquer desenvolvedor compreende imediatamente a arquitetura da página sem precisar interpretar classes CSS arbitrárias. Isso reduz o tempo de integração de novos membros em equipes e diminui a probabilidade de erros em manutenções futuras.
Interoperabilidade
Navegadores, leitores de RSS, aplicações de leitura (read-it-later apps), ferramentas de raspagem de dados (web scraping) e sistemas de síntese de voz todos se beneficiam de documentos semanticamente corretos para extrair e apresentar o conteúdo de forma adequada.
📜 A evolução da semântica no HTML¶
O HTML original de 1991 era fortemente orientado à apresentação: elementos como
<font>,<center>e<b>misturavam estrutura e aparência de forma indissociável. Com a adoção do CSS em 1996 e a publicação do HTML 4.01 em 1999, iniciou-se a separação entre estrutura e apresentação — mas a linguagem ainda carecia de elementos para descrever regiões funcionais de uma página.O HTML5, publicado pelo W3C em 2014, representou o salto semântico mais significativo da história da linguagem. Foram introduzidos elementos como
<header>,<nav>,<main>,<article>,<section>,<aside>e<footer>, que permitem descrever não apenas o conteúdo, mas sua função na arquitetura da página — algo que antes só era possível por convenção de classes CSS (.header,.nav,.footer).Referência: WHATWG — HTML Living Standard: Semantics
3.2 — Elementos de seção e sua semântica¶
Vídeo curto explicativo (link será adicionado posteriormente)
O HTML5 introduziu um conjunto de elementos de seção (sectioning elements) cujo propósito é delimitar as regiões funcionais canônicas de um documento web. Esses elementos não substituem o CSS na definição do layout visual — eles comunicam o papel funcional de cada região do documento.
É fundamental compreender que os elementos de seção não produzem efeitos visuais automáticos: não empurram conteúdo, não criam colunas, não aplicam espaçamento. Toda a apresentação visual continua sendo responsabilidade exclusiva do CSS. O que esses elementos oferecem é significado semântico interpretável por agentes de usuário, tecnologias assistivas e mecanismos de busca.
3.2.1 — <header>: cabeçalho¶
O elemento <header> representa o conteúdo introdutório de sua seção de escopo. Em seu uso mais comum — como filho direto do <body> — representa o cabeçalho global da página, tipicamente contendo logotipo, título principal e navegação primária. Contudo, <header> não é exclusivo do cabeçalho global: ele pode ser usado como cabeçalho de um <article> ou <section> específico, introduzindo o conteúdo daquele bloco.
<!-- Cabeçalho global da página -->
<header>
<img src="logo.svg" alt="Logotipo da instituição" />
<h1>Programação Web 1</h1>
<nav>
<ul>
<li><a href="/">Início</a></li>
<li><a href="/capitulos">Capítulos</a></li>
</ul>
</nav>
</header>
<!-- Cabeçalho de um artigo específico -->
<article>
<header>
<h2>Introdução ao HTML Semântico</h2>
<p>Publicado em <time datetime="2026-03-01">1º de março de 2026</time></p>
</header>
<p>Conteúdo do artigo...</p>
</article>
Restrição importante: o elemento <header> não pode ser descendente de <address>, <footer> ou de outro <header>.
3.2.2 — <nav>: navegação¶
O elemento <nav> delimita um conjunto de links de navegação de importância significativa. Não é necessário — nem recomendado — marcar todos os grupos de links da página com <nav>; o elemento deve ser reservado para blocos de navegação primária: menu principal, sumário de capítulo, navegação de paginação, índice de seções.
O atributo aria-label é especialmente importante quando uma página contém múltiplos elementos <nav>, pois permite que leitores de tela distingam entre eles:
<!-- Navegação principal -->
<nav aria-label="Navegação principal">
<ul>
<li><a href="/">Início</a></li>
<li><a href="/sobre">Sobre</a></li>
</ul>
</nav>
<!-- Navegação secundária: sumário do capítulo -->
<nav aria-label="Sumário do capítulo">
<ol>
<li><a href="#secao-1">3.1 — O que é semântica</a></li>
<li><a href="#secao-2">3.2 — Elementos de seção</a></li>
</ol>
</nav>
3.2.3 — <main>: conteúdo principal¶
O elemento <main> delimita o conteúdo principal e único da página — o conteúdo diretamente relacionado ao seu tópico central, excluindo elementos que se repetem em outras páginas (cabeçalho, navegação, rodapé, barras laterais).
Duas regras fundamentais regem seu uso:
- Deve existir apenas um
<main>por página — múltiplas instâncias criariam ambiguidade sobre qual região contém o conteúdo central. - Não deve ser descendente de
<article>,<aside>,<header>,<footer>ou<nav>— é sempre filho direto do<body>ou de um elemento de divisão genérico.
O <main> é especialmente relevante para acessibilidade: leitores de tela e tecnologias assistivas o utilizam como ponto de salto direto para o conteúdo principal, permitindo que o usuário ignore a navegação repetida no topo.
<body>
<header>...</header>
<nav>...</nav>
<main>
<!-- Todo o conteúdo central da página vai aqui -->
<h1>Título da página</h1>
<p>Conteúdo principal...</p>
</main>
<footer>...</footer>
</body>
3.2.4 — <article>: conteúdo autônomo¶
O elemento <article> representa um fragmento de conteúdo autônomo e independente — um bloco que faria sentido existir isoladamente, sem o contexto do restante da página. O critério de autonomia é a chave para seu uso correto: se o conteúdo poderia ser publicado de forma independente (em outro site, num feed RSS, num e-mail), ele é um candidato ao <article>.
Exemplos de uso apropriado: postagens de blog, artigos jornalísticos, avaliações de produtos, comentários de usuários, fichas de produtos em um e-commerce, episódios de podcast.
<main>
<!-- Um artigo de blog -->
<article>
<header>
<h2>O que é o protocolo HTTP?</h2>
<p>Por Prof. Silva —
<time datetime="2026-02-15">15 de fevereiro de 2026</time>
</p>
</header>
<p>O HTTP (<em>Hypertext Transfer Protocol</em>) é o protocolo
de comunicação que fundamenta a transferência de dados na Web...</p>
<footer>
<p>Categorias: <a href="/fundamentos">Fundamentos da Web</a></p>
</footer>
</article>
<!-- Outro artigo independente -->
<article>
<h2>Introdução ao CSS</h2>
<p>...</p>
</article>
</main>
<article> pode ser aninhado: um artigo pode conter artigos filhos que representam conteúdo relacionado mas autônomo, como os comentários de uma postagem:
<article>
<h2>Postagem principal</h2>
<p>Conteúdo da postagem...</p>
<section>
<h3>Comentários</h3>
<!-- Cada comentário é um artigo autônomo -->
<article>
<header>
<p>Comentário de <strong>Maria</strong></p>
</header>
<p>Excelente explicação!</p>
</article>
<article>
<header>
<p>Comentário de <strong>João</strong></p>
</header>
<p>Muito didático, obrigado.</p>
</article>
</section>
</article>
3.2.5 — <section>: seção temática¶
O elemento <section> delimita uma seção temática genérica de um documento ou aplicação. Seu critério de uso é distinto do <article>: enquanto <article> pressupõe autonomia (o conteúdo faz sentido sozinho), <section> pressupõe pertencimento — é uma parte de um todo maior, identificada por um tema específico.
A especificação do WHATWG recomenda que <section> seja utilizado apenas quando o conteúdo do bloco seria listado explicitamente no sumário do documento. Se o bloco não merece uma entrada no sumário — se é apenas um contêiner para fins de layout —, deve-se usar <div>.
Uma regra prática amplamente adotada: <section> deve sempre conter um título (<h2> a <h6>), pois é o título que identifica a seção como uma unidade temática distinta.
<article>
<h2>Guia completo de HTML</h2>
<section>
<h3>História do HTML</h3>
<p>O HTML foi criado por Tim Berners-Lee em 1991...</p>
</section>
<section>
<h3>Estrutura básica de um documento</h3>
<p>Todo documento HTML possui uma estrutura mínima...</p>
</section>
<section>
<h3>Elementos semânticos</h3>
<p>O HTML5 introduziu um conjunto de elementos...</p>
</section>
</article>
<article> vs. <section>: a distinção fundamental
Esta é uma das dúvidas mais comuns entre desenvolvedores iniciantes. A forma mais objetiva de distingui-los:
- Use
<article>quando o conteúdo faz sentido sozinho, fora do contexto da página. - Use
<section>quando o conteúdo é parte de um todo e está agrupado por tema. - Use
<div>quando o agrupamento é puramente estrutural ou de layout, sem semântica.
3.2.6 — <aside>: conteúdo tangencial¶
O elemento <aside> representa conteúdo tangencialmente relacionado ao conteúdo ao seu redor — informações que complementam, mas não são essenciais ao fluxo principal. A remoção do <aside> não deve comprometer a compreensão do conteúdo principal.
Exemplos de uso: notas de rodapé, caixas de destaque ("saiba mais"), glossários laterais, listas de links relacionados, publicidade contextual, perfis de autor, widgets de redes sociais.
<main>
<article>
<h2>Protocolo HTTP</h2>
<p>O HTTP é o protocolo que governa a comunicação entre
clientes e servidores na Web. Cada interação é composta
por uma requisição e uma resposta...</p>
<!-- Nota tangencial: não é essencial, mas enriquece -->
<aside>
<h3>Curiosidade</h3>
<p>O HTTP foi criado por Tim Berners-Lee em 1989 como
parte do projeto que daria origem à World Wide Web.</p>
</aside>
<p>O protocolo é definido como <em>stateless</em>...</p>
</article>
<!-- Aside no nível da página: conteúdo lateral -->
<aside aria-label="Leituras recomendadas">
<h2>Leituras recomendadas</h2>
<ul>
<li><a href="https://developer.mozilla.org">MDN Web Docs</a></li>
<li><a href="https://html.spec.whatwg.org">HTML Living Standard</a></li>
</ul>
</aside>
</main>
3.2.7 — <footer>: rodapé¶
O elemento <footer> representa o rodapé de sua seção de escopo. Tipicamente contém informações sobre autoria, direitos autorais, links institucionais, dados de contato e metadados sobre o conteúdo. Como <header>, pode ser usado tanto no nível global da página quanto no nível de um <article> ou <section>.
<!-- Rodapé de um artigo -->
<article>
<h2>Introdução ao HTML</h2>
<p>Conteúdo do artigo...</p>
<footer>
<p>Autor: Prof. Silva |
Última atualização:
<time datetime="2026-03-01">março de 2026</time>
</p>
</footer>
</article>
<!-- Rodapé global da página -->
<footer>
<p>© 2026 IFAL — Instituto Federal de Alagoas</p>
<nav aria-label="Links institucionais">
<a href="/acessibilidade">Acessibilidade</a>
<a href="/privacidade">Política de Privacidade</a>
<a href="/contato">Contato</a>
</nav>
</footer>
Restrição: <footer> não pode ser descendente de <address> ou de outro <footer>, e não pode conter um elemento <header>.
3.2.8 — O algoritmo de outline e a hierarquia de títulos¶
Compreender os elementos de seção individualmente não é suficiente para escrever HTML semântico correto: é preciso entender como eles interagem com os títulos (<h1>–<h6>) para formar o outline do documento — a estrutura hierárquica que navegadores, leitores de tela e ferramentas de indexação constroem a partir da marcação.
O outline é, em essência, o sumário implícito do documento. Cada elemento de seção (<article>, <section>, <aside>, <nav>) cria um contexto de seção próprio, e os títulos dentro dele são interpretados relativamente a esse contexto. Isso tem uma consequência prática importante: um <h1> dentro de um <article> representa o título principal daquele artigo, não necessariamente o título principal da página inteira.
Considere o seguinte documento:
<body>
<h1>Blog de Tecnologia</h1> <!-- Título da página -->
<main>
<article>
<h2>O que é HTTP?</h2> <!-- Título do artigo -->
<section>
<h3>História do HTTP</h3> <!-- Subtítulo da seção -->
</section>
</article>
<article>
<h2>Introdução ao CSS</h2> <!-- Título do segundo artigo -->
</article>
</main>
</body>
O outline resultante desse documento seria:
1. Blog de Tecnologia
1.1 O que é HTTP?
1.1.1 História do HTTP
1.2 Introdução ao CSS
Este outline é o que um leitor de tela apresenta ao usuário quando ele solicita a lista de títulos da página — o mecanismo mais comum de navegação rápida em tecnologias assistivas. Se a hierarquia de títulos estiver incorreta (por exemplo, saltar de <h1> para <h3> sem um <h2> intermediário), o outline gerado será incoerente, prejudicando a experiência de quem depende desse mecanismo.
A regra prática mais importante: nunca salte níveis de título. A sequência <h1> → <h2> → <h3> deve ser respeitada como uma hierarquia lógica, não visual.
Imagem sugerida: captura lado a lado da aba Accessibility do Chrome DevTools mostrando o outline de uma página com hierarquia de títulos correta versus uma página com títulos fora de ordem — ilustrando visualmente como a estrutura é percebida por leitores de tela.
(imagem será adicionada posteriormente)
No DevTools: abra a aba Elements, selecione qualquer elemento de seção e, no painel lateral, clique em Accessibility. O painel exibe o role semântico do elemento, seu nome acessível e sua posição na árvore de acessibilidade — a mesma estrutura que um leitor de tela enxerga.
Referências: - WHATWG — Headings and sections - MDN — Document and website structure
3.2.9 — <section> sem título: consequências práticas e solução¶
A orientação de que <section> deve sempre conter um título tem uma justificativa técnica precisa que vai além da convenção editorial. Quando um elemento <section> não possui um título visível associado, ele se torna opaco para tecnologias assistivas: o leitor de tela não consegue nomear a seção, o que prejudica a navegação por regiões do documento.
A solução para casos em que o título não deve ser visível — por razões de design — é utilizar o atributo aria-labelledby (quando o título existe mas está fora do <section>) ou aria-label (quando não há título no DOM):
<!-- Caso 1: título visível — uso convencional -->
<section>
<h2>Depoimentos de alunos</h2>
<p>...</p>
</section>
<!-- Caso 2: sem título visível, mas com aria-label
— a seção é nomeada para leitores de tela -->
<section aria-label="Depoimentos de alunos">
<p>...</p>
</section>
<!-- Caso 3: título existe no DOM mas está fora da section
— aria-labelledby referencia o id do título -->
<h2 id="titulo-depoimentos">Depoimentos de alunos</h2>
<section aria-labelledby="titulo-depoimentos">
<p>...</p>
</section>
A omissão tanto do título quanto dos atributos ARIA transforma o <section> em um contêiner semanticamente anônimo — nesse caso, <div> seria igualmente adequado e mais honesto quanto à ausência de semântica.
Referência: MDN — <section>: The Generic Section element
3.2.10 — <figure> e <figcaption>: conteúdo autônomo referenciado¶
O elemento <figure> representa conteúdo autônomo e autocontido que é referenciado pelo fluxo principal do documento, mas que poderia, em princípio, ser movido para outro lugar (um apêndice, uma barra lateral, outro documento) sem prejudicar a compreensão do texto que o referencia.
Um equívoco comum é associar <figure> exclusivamente a imagens. Na especificação, <figure> pode conter qualquer tipo de conteúdo autorreferenciado: imagens, diagramas, gráficos, trechos de código, tabelas, citações e até mesmo vídeos. O critério é a autonomia e a referencialidade — o fluxo principal menciona ou depende desse conteúdo, mas o conteúdo em si poderia ser deslocado.
<!-- Figura com imagem -->
<figure>
<img
src="figures/outline-semantico.png"
alt="Diagrama mostrando o outline de um documento HTML semântico"
width="700"
height="400"
/>
<figcaption>
Figura 1 — Representação do outline gerado por um documento
com hierarquia semântica correta.
</figcaption>
</figure>
<!-- Figura com bloco de código -->
<figure>
<pre><code><article>
<h2>Título</h2>
<p>Conteúdo...</p>
</article></code></pre>
<figcaption>
Figura 2 — Estrutura mínima de um elemento <article>.
</figcaption>
</figure>
<!-- Figura com citação longa -->
<figure>
<blockquote cite="https://html.spec.whatwg.org/">
<p>The article element represents a complete, or self-contained,
composition in a document, page, application, or site.</p>
</blockquote>
<figcaption>
— <cite>HTML Living Standard</cite>, WHATWG
</figcaption>
</figure>
O elemento <figcaption>, quando presente, deve ser o primeiro ou o último filho direto do <figure>. Ele fornece a legenda ou descrição do conteúdo da figura e é associado automaticamente ao conteúdo pelo leitor de tela — tornando desnecessário repetir a informação da legenda no atributo alt da imagem quando ambos descrevem a mesma coisa.
Referências:
- MDN — <figure>: The Figure with Optional Caption element
- WHATWG — The figure element
3.3 — Elementos de texto com semântica específica¶
Vídeo curto explicativo (link será adicionado posteriormente)
Além dos elementos de seção, o HTML oferece um conjunto de elementos para atribuir significado semântico específico a fragmentos de texto. Compreender a distinção entre esses elementos — especialmente entre os pares que produzem aparência visual similar — é essencial para escrever HTML correto.
3.3.1 — Ênfase e importância: <em> e <strong>¶
Estes dois elementos são frequentemente confundidos por sua aparência visual padrão (itálico e negrito, respectivamente), mas possuem semânticas distintas e não intercambiáveis.
<em> — ênfase
O elemento <em> (emphasis) indica que o trecho possui ênfase linguística — uma entonação que, na fala, alteraria o sentido da frase. Seu efeito é semântico, não decorativo: leitores de tela podem alterar a entonação de síntese de voz para elementos <em>.
<!-- A ênfase muda o sentido da frase em cada caso -->
<p><em>Eu</em> nunca disse que ela roubou o dinheiro.</p>
<p>Eu nunca disse que <em>ela</em> roubou o dinheiro.</p>
<p>Eu nunca disse que ela <em>roubou</em> o dinheiro.</p>
<strong> — importância elevada
O elemento <strong> indica que o trecho possui importância, seriedade ou urgência elevada no contexto do documento. É utilizado para avisos, alertas, informações críticas ou termos que o leitor não deve ignorar.
<p>
<strong>Atenção:</strong> é obrigatório realizar o commit
antes de fechar o terminal.
</p>
<p>
O prazo de entrega é <strong>improrrogável</strong>.
</p>
3.3.2 — Destaque tipográfico sem semântica: <b> e <i>¶
Os elementos <b> e <i> existem no HTML5 com semânticas mais restritas do que seu nome sugere.
<b> — destaque tipográfico convencional
O elemento <b> (bold) é usado para atrair a atenção do leitor para um trecho de texto sem que isso implique importância elevada ou ênfase especial. Exemplos: palavras-chave em um resumo, nome de produto em uma resenha, primeiras palavras de um parágrafo em um artigo jornalístico.
<i> — voz alternativa ou convenção tipográfica
O elemento <i> (italic) é usado para texto em uma "voz" ou modo alternativo ao prosa principal: termos técnicos, palavras estrangeiras, títulos de obras citadas em linha, pensamentos de personagens em ficção.
<!-- <b>: palavra-chave em destaque, sem importância semântica -->
<p>O protocolo <b>HTTP</b> é a base da comunicação na Web.</p>
<!-- <i>: termo técnico em latim -->
<p>O princípio <i lang="la">ad hoc</i> é frequentemente
aplicado em redes sem fio.</p>
<!-- <i>: título de obra inline -->
<p>Para aprofundar o tema, consulte
<cite>HTML: The Living Standard</cite>.</p>
3.3.3 — Outros elementos semânticos de texto¶
| Elemento | Semântica | Exemplo de uso |
|---|---|---|
<mark> |
Trecho relevante para o contexto atual (como resultado de busca) | Destacar termos pesquisados |
<small> |
Texto de menor relevância: notas, asteriscos, direitos autorais | Letra miúda em contratos |
<del> |
Conteúdo removido ou obsoleto | Preço anterior riscado |
<ins> |
Conteúdo inserido ou acrescentado | Correções em documentos |
<code> |
Fragmento de código-fonte | console.log("olá") |
<pre> |
Texto pré-formatado (preserva espaços e quebras) | Blocos de código |
<kbd> |
Entrada de teclado do usuário | Pressione Ctrl+S |
<samp> |
Saída de programa ou sistema | Resultado de um comando |
<var> |
Variável matemática ou de programação | x = 2 |
<abbr> |
Abreviação ou sigla, com expansão via title |
<abbr title="HyperText Markup Language">HTML</abbr> |
<cite> |
Título de obra referenciada | Nome de livro, artigo |
<time> |
Data ou hora legível por máquina | <time datetime="2026-03-01"> |
<address> |
Informações de contato do autor ou da seção | E-mail, endereço postal |
<blockquote> |
Citação longa de fonte externa | Trecho de discurso ou artigo |
<q> |
Citação curta, inline | Frase citada dentro de parágrafo |
<dfn> |
Definição de um termo | Primeira ocorrência de um conceito |
O elemento <time> merece atenção especial por seu impacto prático. Ele permite que datas e horários sejam legíveis por máquinas (navegadores, mecanismos de busca, aplicativos de calendário) sem sacrificar a legibilidade humana:
<!-- datetime usa o formato ISO 8601 -->
<p>A aula ocorre toda
<time datetime="2026-03-20">sexta-feira, 20 de março de 2026</time>
às <time datetime="08:00">8h</time>.
</p>
<!-- Duração -->
<p>O evento tem duração de <time datetime="PT2H">2 horas</time>.</p>
Referências para esta seção: - MDN — Referência de elementos HTML (completa) - WHATWG — Text-level semantics - W3C — Using semantic elements
3.4 — Refatoração: de marcação não semântica para semântica¶
Vídeo curto explicativo (link será adicionado posteriormente)
A melhor forma de consolidar o entendimento do HTML semântico é observar o processo de refatoração — a transformação de um documento funcional, mas semanticamente incorreto, em um documento estruturalmente correto. Refatorar não significa alterar o visual da página; significa corrigir o significado da marcação.
3.4.1 — Exemplo completo de refatoração¶
A seguir, apresenta-se uma página típica construída inteiramente com <div> e classes CSS — uma abordagem comum entre desenvolvedores que não dominam os elementos semânticos — seguida de sua versão refatorada.
Versão original (não semântica):
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<title>Blog de Tecnologia</title>
</head>
<body>
<div class="cabecalho">
<div class="logo">
<img src="logo.png" alt="Logo" />
</div>
<div class="titulo-site">Blog de Tecnologia</div>
<div class="menu">
<div class="item-menu"><a href="/">Início</a></div>
<div class="item-menu"><a href="/artigos">Artigos</a></div>
<div class="item-menu"><a href="/contato">Contato</a></div>
</div>
</div>
<div class="conteudo">
<div class="postagem">
<div class="titulo-postagem">O que é HTML Semântico?</div>
<div class="meta">Por Prof. Silva — 01/03/2026</div>
<div class="texto">
<p>O HTML semântico é a prática de usar elementos
que descrevem o significado do conteúdo...</p>
</div>
<div class="rodape-post">
<span class="categoria">Categoria: HTML</span>
</div>
</div>
</div>
<div class="barra-lateral">
<div class="titulo-lateral">Artigos relacionados</div>
<div class="lista-links">
<div><a href="/html-basico">HTML Básico</a></div>
<div><a href="/css-intro">Introdução ao CSS</a></div>
</div>
</div>
<div class="rodape">
<div class="copyright">© 2026 Blog de Tecnologia</div>
</div>
</body>
</html>
Versão refatorada (semântica):
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Blog de Tecnologia</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<header>
<a href="/">
<img src="logo.png" alt="Blog de Tecnologia" />
</a>
<h1>Blog de Tecnologia</h1>
<nav aria-label="Navegação principal">
<ul>
<li><a href="/">Início</a></li>
<li><a href="/artigos">Artigos</a></li>
<li><a href="/contato">Contato</a></li>
</ul>
</nav>
</header>
<main>
<article>
<header>
<h2>O que é HTML Semântico?</h2>
<p>
Por <strong>Prof. Silva</strong> —
<time datetime="2026-03-01">1º de março de 2026</time>
</p>
</header>
<p>O HTML semântico é a prática de usar elementos
que descrevem o significado do conteúdo...</p>
<footer>
<p>Categoria: <a href="/categoria/html">HTML</a></p>
</footer>
</article>
<aside aria-label="Artigos relacionados">
<h2>Artigos relacionados</h2>
<ul>
<li><a href="/html-basico">HTML Básico</a></li>
<li><a href="/css-intro">Introdução ao CSS</a></li>
</ul>
</aside>
</main>
<footer>
<p>© 2026 Blog de Tecnologia</p>
</footer>
</body>
</html>
3.4.2 — Análise das decisões de refatoração¶
Cada substituição realizada no exemplo acima obedece a um critério semântico preciso:
<div class="cabecalho"> → <header>: o bloco introduz a página com logotipo, título e navegação — função exata do <header>.
<div class="titulo-site"> → <h1>: o nome do site é o título principal do documento. Não há razão semântica para que seja um <div> estilizado.
<div class="menu"> + <div class="item-menu"> → <nav> + <ul> + <li>: a navegação é uma lista de links — semântica de lista e semântica de navegação devem ser combinadas.
<div class="conteudo"> → <main>: o bloco contém o conteúdo principal e único da página.
<div class="postagem"> → <article>: a postagem é um conteúdo autônomo — faria sentido publicado isoladamente.
<div class="titulo-postagem"> → <h2> dentro de <header> do artigo: o título da postagem é um título de nível 2 (subordinado ao <h1> da página).
<div class="meta"> → <p> com <time>: a data é um dado estruturado que se beneficia do elemento <time> para legibilidade por máquina.
<div class="barra-lateral"> → <aside>: o conteúdo lateral é tangencialmente relacionado ao artigo principal — semântica exata do <aside>.
<div class="rodape"> → <footer>: o rodapé global da página.
3.5 — Validação semântica e diagnóstico com ferramentas¶
Vídeo curto explicativo (link será adicionado posteriormente)
Escrever HTML semântico correto é uma habilidade que se aprimora com feedback constante. Existem ferramentas especializadas — algumas online, outras integradas diretamente ao navegador — que permitem verificar se um documento está sintaticamente válido, semanticamente coerente e acessível. Incorporar essas ferramentas ao fluxo de desenvolvimento desde os primeiros projetos é uma prática profissional essencial.
3.5.1 — W3C Markup Validation Service¶
O W3C Markup Validation Service é a ferramenta oficial de validação sintática de documentos HTML, mantida pelo World Wide Web Consortium. Ela verifica se o documento está em conformidade com a especificação HTML e reporta erros como tags não fechadas, atributos inválidos, elementos aninhados incorretamente e ausência de elementos obrigatórios (como <!DOCTYPE> ou <title>).
A validação pode ser realizada de três formas: por URL (para páginas publicadas), por upload de arquivo ou por entrada direta de código. Para as atividades desta disciplina, a entrada direta de código é a mais prática.
Acesse: https://validator.w3.org/
Um documento com erros de validação não é necessariamente disfuncional — os navegadores modernos são tolerantes e realizam correções automáticas. Contudo, depender dessa tolerância é uma prática frágil: o comportamento de correção automática não é padronizado entre navegadores, e erros estruturais podem produzir resultados imprevisíveis em contextos específicos (leitores de tela, parsers automatizados, ambientes embarcados).
3.5.2 — WAVE — Web Accessibility Evaluation Tool¶
O WAVE é uma ferramenta gratuita de avaliação de acessibilidade desenvolvida pela WebAIM (Web Accessibility In Mind). Diferentemente do validador W3C — que verifica apenas a sintaxe —, o WAVE analisa o documento sob a perspectiva da acessibilidade: identifica ausência de textos alternativos, hierarquias de título incorretas, falta de labels em formulários, contraste insuficiente e outros problemas que afetam diretamente usuários de tecnologias assistivas.
O WAVE exibe os resultados sobrepostos à própria página, com ícones codificados por cor: erros (vermelho), alertas (amarelo) e itens estruturais positivos (verde e azul). Essa visualização in-page é especialmente eficaz para diagnosticar problemas em relação ao contexto em que ocorrem.
Acesse: https://wave.webaim.org/
Imagem sugerida: captura de tela do WAVE analisando uma página com problemas semânticos (ausência de
alt, hierarquia de títulos incorreta), mostrando os ícones de erro sobrepostos à página.(imagem será adicionada posteriormente)
3.5.3 — DevTools: Accessibility Tree e Lighthouse¶
O Chrome DevTools (e o Firefox DevTools, de forma similar) oferece dois recursos diretamente relevantes para o diagnóstico semântico de documentos HTML:
Accessibility Tree (Árvore de Acessibilidade)
A aba Elements do DevTools exibe a árvore DOM do documento. Ao selecionar qualquer elemento e abrir o painel lateral Accessibility (F12 → Elements → painel Accessibility à direita), é possível visualizar:
- O role semântico do elemento (ex.:
heading,navigation,article,generic) - O nome acessível — o texto ou rótulo que o leitor de tela anuncia ao focar o elemento
- O estado — se está expandido, selecionado, desabilitado, etc.
- A posição do elemento na Accessibility Tree completa
Esta visualização revela, de forma concreta, a diferença entre um <div> (role: generic, sem nome acessível) e um <nav aria-label="Navegação principal"> (role: navigation, nome: "Navegação principal"). A Accessibility Tree é exatamente o que um leitor de tela como o NVDA ou o VoiceOver enxerga ao processar a página.
Imagem sugerida: captura do painel Accessibility do Chrome DevTools mostrando o role e o nome acessível de um elemento
<nav aria-label="...">versus um<div class="nav">sem semântica — lado a lado.(imagem será adicionada posteriormente)
Como usar no DevTools:
1. Abra o DevTools (F12 ou Ctrl+Shift+I)
2. Na aba Elements, clique em qualquer elemento da árvore DOM
3. No painel lateral direito, localize a seção Accessibility
4. Observe o role, o name e os states do elemento selecionado
5. Marque a opção "Enable full-page accessibility tree" para visualizar a árvore completa de acessibilidade no painel Elements
Lighthouse
O Lighthouse é uma ferramenta de auditoria automatizada integrada ao Chrome DevTools (aba Lighthouse). Ela gera relatórios sobre desempenho, acessibilidade, boas práticas e SEO da página. A auditoria de acessibilidade é baseada nas diretrizes WCAG e verifica automaticamente dezenas de critérios, incluindo:
- Presença e qualidade dos atributos
altem imagens - Hierarquia de títulos (
<h1>–<h6>) - Uso correto de
<label>em formulários - Contraste de cores entre texto e fundo
- Presença de atributos ARIA adequados
- Navegabilidade por teclado
O resultado é uma pontuação de 0 a 100, acompanhada de uma lista de problemas com links diretos para a documentação relevante.
Como usar o Lighthouse: 1. Abra o DevTools (
F12) 2. Navegue até a aba Lighthouse 3. Em Categories, selecione Accessibility (e opcionalmente Best practices e SEO) 4. Clique em Analyze page load 5. Analise o relatório gerado, corrigindo os problemas identificados
3.5.4 — Fluxo de diagnóstico recomendado¶
Para as atividades desta disciplina, recomenda-se seguir o seguinte fluxo de diagnóstico em ordem, do mais básico ao mais específico:
- W3C Validator — garantir que o documento não tem erros sintáticos
- DevTools → Elements → Accessibility — verificar o role e o nome acessível dos elementos-chave (
<nav>,<main>,<article>,<header>,<footer>) - WAVE — auditar a página completa sob a perspectiva de acessibilidade
- Lighthouse — obter pontuação geral e lista priorizada de problemas a corrigir
Este fluxo não substitui o teste com leitores de tela reais (como NVDA, JAWS ou VoiceOver), mas fornece uma base sólida de diagnóstico automatizado antes de testes manuais.
Referências: - W3C Markup Validation Service - WAVE Web Accessibility Evaluation Tool - Chrome DevTools — Accessibility features - Lighthouse — Accessibility audits - WebAIM — Introduction to Web Accessibility - NVDA Screen Reader (gratuito)
Atividades — Capítulo 3¶
1. Um desenvolvedor precisa marcar um bloco de conteúdo que contém uma lista de links para outras páginas do site (menu principal). Qual elemento é semanticamente mais apropriado?
2. Qual é a distinção semântica correta entre <article> e <section>?
3. Por que o elemento <main> deve ocorrer apenas uma vez por página?
4. Qual é a diferença semântica entre <em> e <i>?
- GitHub Classroom: Refatorar a página não semântica fornecida, substituindo todos os
<div>por elementos semânticos apropriados e justificando cada decisão em comentários HTML. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 2 — Fundamentos do HTML :material-arrow-right: Ir ao Capítulo 4 — Tabelas, Listas e Mídia
Capítulo 4 — Tabelas, Listas e Mídia¶
Vídeo curto explicativo (link será adicionado posteriormente)
4.1 — Tabelas HTML¶
Vídeo curto explicativo (link será adicionado posteriormente)
Uma tabela HTML é utilizada para representar dados organizados em linhas e colunas.
As tabelas constituem um dos mecanismos mais antigos e ao mesmo tempo mais mal utilizados do HTML. Introduzidas desde as primeiras versões da linguagem, foram durante anos empregadas indevidamente como ferramenta de layout de página — prática que o advento do CSS tornou obsoleta e que a especificação do HTML5 condena explicitamente. Compreender o propósito correto das tabelas, sua estrutura semântica e suas implicações de acessibilidade é essencial para qualquer desenvolvedor que precise apresentar dados tabulares de forma adequada.
Os principais elementos são:
<table>— define a tabela<tr>— define uma linha<td>— define uma célula de dado<th>— define uma célula de cabeçalho
💡 Importante:
Neste momento do curso, estamos focando apenas na estrutura (HTML).
A aparência visual (cores, bordas, espaçamento) será estudada no próximo bimestre com CSS.
4.1.1 — Quando usar tabelas (e quando não usar)¶
Tabelas devem ser usadas exclusivamente para dados tabulares, ou seja, informações organizadas em linhas e colunas com relação lógica.
O critério de uso é objetivo: se o conteúdo pode ser descrito por uma grade onde cada célula representa a intersecção entre uma categoria de linha e uma categoria de coluna, uma tabela é o elemento correto. Se o conteúdo é apenas uma lista de itens que se beneficiaria de um layout em múltiplas colunas por razões puramente visuais, a solução correta é CSS — não uma tabela.
⚠️ Erro comum: usar tabelas para layout de página
Isso torna o código semanticamente incorreto e prejudica a acessibilidade.
Usos incorretos que devem ser evitados:
<!-- INCORRETO: tabela usada para criar layout de duas colunas -->
<table>
<tr>
<td>
<nav>Menu lateral</nav>
</td>
<td>
<main>Conteúdo principal</main>
</td>
</tr>
</table>
<!-- CORRETO: layout é responsabilidade do CSS -->
<div class="layout">
<nav>Menu lateral</nav>
<main>Conteúdo principal</main>
</div>
O uso de tabelas para layout, além de semanticamente incorreto, produz documentos inacessíveis: leitores de tela anunciam o início e o fim de cada tabela, o número de linhas e colunas, e navegam célula a célula — comportamento adequado para dados tabulares, mas completamente inadequado para estrutura de página.
Referência: WHATWG — The table element
4.1.2 — Estrutura básica: <table>, <tr>, <td> e <th>¶
A estrutura mínima de uma tabela HTML é composta por três elementos obrigatórios:
<table>— o contêiner raiz da tabela<tr>(table row) — uma linha da tabela<td>(table data) — uma célula de dado, dentro de uma linha<th>(table header) — uma célula de cabeçalho, semânticamente distinta de<td>
<table>
<tr>
<th>Disciplina</th>
<th>Dia</th>
<th>Horário</th>
</tr>
<tr>
<td>Programação Web 1</td>
<td>Sexta-feira</td>
<td>08h–10h</td>
</tr>
<tr>
<td>Banco de Dados</td>
<td>Quarta-feira</td>
<td>14h–16h</td>
</tr>
</table>
✔️ Boa prática:
Use<th>para cabeçalhos — isso melhora a acessibilidade e a compreensão da tabela.
A distinção entre <th> e <td> não é apenas visual (o <th> é renderizado em negrito e centralizado por padrão): ela é semântica. O elemento <th> comunica que aquela célula é um rótulo para as demais células da linha ou coluna, estabelecendo uma relação de cabeçalho que tecnologias assistivas utilizam para contextualizar os dados. Quando um leitor de tela navega por uma tabela, ele anuncia o cabeçalho da coluna ou linha ao ler cada célula de dado — mas apenas se <th> for utilizado corretamente.
4.1.3 — Estrutura semântica: <thead>, <tbody>, <tfoot> e <caption>¶
Para tabelas de qualquer complexidade real, o HTML oferece elementos de agrupamento semântico que organizam as linhas em regiões funcionais distintas:
<thead>(table head) — agrupa as linhas de cabeçalho da tabela<tbody>(table body) — agrupa as linhas de dados (o corpo da tabela)<tfoot>(table foot) — agrupa as linhas de rodapé, geralmente contendo totais ou resumos<caption>— fornece um título descritivo para a tabela inteira
<table>
<caption><strong>Horário semanal — Sistemas de Informação, 3º Período (2026.1)</strong></caption>
<thead>
<tr>
<th scope="col">Disciplina</th>
<th scope="col">Professor</th>
<th scope="col">Dia</th>
<th scope="col">Horário</th>
<th scope="col">Sala</th>
</tr>
</thead>
<tbody>
<tr>
<td>Programação Web 1</td>
<td>Prof. Silva</td>
<td>Sexta-feira</td>
<td>08h–10h</td>
<td>Lab. 3</td>
</tr>
<tr>
<td>Banco de Dados I</td>
<td>Prof. Santos</td>
<td>Quarta-feira</td>
<td>14h–16h</td>
<td>Lab. 2</td>
</tr>
<tr>
<td>Estruturas de Dados</td>
<td>Prof. Lima</td>
<td>Terça-feira</td>
<td>10h–12h</td>
<td>Sala 7</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5">Total: 3 disciplinas — 6 horas semanais</td>
</tr>
</tfoot>
</table>
O elemento <caption> merece atenção especial: ele é o mecanismo semântico correto para titular uma tabela, sendo associado automaticamente a ela por leitores de tela. Ao navegar para uma tabela, o leitor de tela anuncia o caption antes de qualquer outro conteúdo — permitindo que o usuário decida se deseja explorar os dados ou pular para a próxima região. A ausência do <caption> obriga o usuário a navegar pela tabela para descobrir seu conteúdo, o que é uma falha de acessibilidade.
Os elementos <thead>, <tbody> e <tfoot> oferecem ainda uma vantagem prática no CSS: permitem que o cabeçalho e o rodapé da tabela permaneçam fixos enquanto o corpo rola — comportamento útil em tabelas longas — sem JavaScript adicional.
Nota técnica: o elemento
<tfoot>pode ser declarado antes do<tbody>no código-fonte HTML sem que isso afete a renderização. O navegador sempre posicionará o<tfoot>visualmente após o<tbody>, independentemente da ordem no código.
4.1.4 — Mesclagem de células: colspan e rowspan¶
Tabelas com estrutura mais complexa frequentemente requerem que uma célula ocupe o espaço de múltiplas colunas ou linhas. O HTML oferece dois atributos para esse propósito:
colspan— indica que a célula deve se estender por n colunasrowspan— indica que a célula deve se estender por n linhas
<table>
<caption>Disponibilidade de laboratórios por turno</caption>
<thead>
<tr>
<th scope="col">Laboratório</th>
<th scope="col">Segunda</th>
<th scope="col">Terça</th>
<th scope="col">Quarta</th>
<th scope="col">Quinta</th>
<th scope="col">Sexta</th>
</tr>
</thead>
<tbody>
<tr>
<!-- rowspan: "Lab. 1" abrange duas linhas (Manhã e Tarde) -->
<th scope="row" rowspan="2">Lab. 1</th>
<td>Livre</td>
<td>Ocupado</td>
<td>Livre</td>
<td>Livre</td>
<td>Ocupado</td>
</tr>
<tr>
<!-- Linha de continuação do Lab. 1 — a primeira célula foi consumida pelo rowspan -->
<td>Ocupado</td>
<td>Livre</td>
<td>Ocupado</td>
<td>Livre</td>
<td>Livre</td>
</tr>
<tr>
<th scope="row">Lab. 2</th>
<!-- colspan: esta célula abrange 5 colunas -->
<td colspan="5">Em manutenção esta semana</td>
</tr>
</tbody>
</table>
Um erro frequente ao utilizar rowspan é não subtrair as células correspondentes nas linhas subsequentes. Quando uma célula com rowspan="2" é declarada em uma linha, a linha seguinte deve conter uma célula a menos — pois aquela posição já está "ocupada" pela célula que se estende. O não cumprimento dessa regra produz tabelas com colunas desalinhadas.
Imagem sugerida: diagrama visual de uma tabela com
colspanerowspandestacando as células mescladas e suas fronteiras — com anotações indicando os valores dos atributos em cada célula afetada.(imagem será adicionada posteriormente)
4.1.5 — Acessibilidade em tabelas: scope e headers¶
Tabelas simples com uma única linha de cabeçalho no topo são interpretadas corretamente pela maioria dos leitores de tela sem atributos adicionais. Contudo, tabelas mais complexas — com cabeçalhos em múltiplas direções, células mescladas ou estruturas irregulares — requerem atributos explícitos para que a relação entre cabeçalhos e células de dado seja inequívoca.
O atributo scope
O atributo scope é declarado em elementos <th> e indica a direção em que aquele cabeçalho se aplica:
| Valor | Significado |
|---|---|
scope="col" |
O cabeçalho se aplica a todas as células da coluna abaixo |
scope="row" |
O cabeçalho se aplica a todas as células da linha à direita |
scope="colgroup" |
O cabeçalho se aplica a um grupo de colunas (<colgroup>) |
scope="rowgroup" |
O cabeçalho se aplica a um grupo de linhas (<thead>, <tbody>, <tfoot>) |
<table>
<caption>Notas bimestrais — Turma A</caption>
<thead>
<tr>
<th scope="col">Aluno</th>
<th scope="col">1º Bimestre</th>
<th scope="col">2º Bimestre</th>
<th scope="col">Média</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Ana Souza</th>
<td>8.5</td>
<td>9.0</td>
<td>8.75</td>
</tr>
<tr>
<th scope="row">Bruno Lima</th>
<td>7.0</td>
<td>7.5</td>
<td>7.25</td>
</tr>
</tbody>
</table>
Neste exemplo, quando um leitor de tela navega para a célula 8.5, ele anuncia: "1º Bimestre, Ana Souza: 8.5" — contextualizando o dado com seus dois cabeçalhos (coluna e linha). Sem o atributo scope, este anúncio pode não ocorrer corretamente em todos os leitores de tela.
O atributo headers
Para tabelas de alta complexidade — com múltiplos níveis de cabeçalho ou estruturas irregulares —, o atributo headers permite associar explicitamente cada célula de dado a um ou mais cabeçalhos por meio de seus id:
<table>
<caption>Consumo de recursos por servidor</caption>
<thead>
<tr>
<th id="servidor">Servidor</th>
<th id="cpu" colspan="2">CPU (%)</th>
<th id="ram" colspan="2">RAM (GB)</th>
</tr>
<tr>
<td></td>
<th id="cpu-min" headers="cpu">Mínimo</th>
<th id="cpu-max" headers="cpu">Máximo</th>
<th id="ram-min" headers="ram">Mínimo</th>
<th id="ram-max" headers="ram">Máximo</th>
</tr>
</thead>
<tbody>
<tr>
<th id="srv1" headers="servidor">Servidor A</th>
<td headers="srv1 cpu cpu-min">12</td>
<td headers="srv1 cpu cpu-max">78</td>
<td headers="srv1 ram ram-min">4.2</td>
<td headers="srv1 ram ram-max">15.8</td>
</tr>
</tbody>
</table>
O uso de headers é reservado para tabelas de alta complexidade. Para a maioria das tabelas de uso cotidiano, scope é suficiente e mais simples de manter.
No DevTools: abra a aba Accessibility no Chrome DevTools e selecione uma célula
<td>de uma tabela. O painel exibirá os cabeçalhos associados àquela célula na árvore de acessibilidade — permitindo verificar se as relaçõesscope/headersestão sendo interpretadas corretamente pelo navegador.
Referências:
- MDN — <table>: The Table element
- WebAIM — Creating Accessible Tables
- W3C — Tables Concepts
Atividade — Seção 4.1¶
1. Por que o uso de tabelas HTML para criar layouts de página é considerado uma prática incorreta?
2. Qual é a função semântica do elemento <caption> em uma tabela?
3. Uma célula `
- GitHub Classroom: Construir a tabela de horários do curso com
<caption>,<thead>,<tbody>,<tfoot>, atributosscopenos cabeçalhos e pelo menos uma célula comcolspan. (link será adicionado)
4.2 — Listas avançadas¶
Vídeo curto explicativo (link será adicionado posteriormente)
O Capítulo 2 introduziu os três tipos fundamentais de listas em HTML: <ul>, <ol> e <dl>. Esta seção aprofunda os aspectos menos explorados de cada tipo — os atributos de controle de numeração de <ol>, as possibilidades semânticas da lista de definições e os padrões de aninhamento que permitem representar hierarquias complexas.
4.2.1 — Listas ordenadas em profundidade: atributos de controle¶
O elemento <ol> aceita três atributos que controlam o comportamento da numeração, todos com implicações semânticas e práticas relevantes:
type — tipo de marcador
O atributo type define o sistema de numeração utilizado. Embora a aparência dos marcadores possa ser controlada via CSS (propriedade list-style-type), o atributo HTML type tem peso semântico: ele comunica o significado do tipo de numeração, não apenas sua aparência.
| Valor | Sistema | Exemplo |
|---|---|---|
1 (padrão) |
Arábico | 1, 2, 3... |
A |
Letras maiúsculas | A, B, C... |
a |
Letras minúsculas | a, b, c... |
I |
Romano maiúsculo | I, II, III... |
i |
Romano minúsculo | i, ii, iii... |
<!-- Lista de etapas de um processo legal com numeração romana -->
<ol type="I">
<li>Petição inicial</li>
<li>Citação do réu</li>
<li>Contestação</li>
<li>Instrução processual</li>
<li>Sentença</li>
</ol>
<!-- Subitens com letras minúsculas -->
<ol type="a">
<li>Análise de requisitos</li>
<li>Projeto de arquitetura</li>
<li>Implementação</li>
</ol>
start — valor inicial da contagem
O atributo start define o número de partida da sequência. É especialmente útil quando uma lista ordenada é interrompida por outro conteúdo e precisa continuar a contagem a partir do ponto anterior:
<p>Os três primeiros requisitos são obrigatórios:</p>
<ol start="1">
<li>Autenticação de dois fatores</li>
<li>Criptografia de dados em repouso</li>
<li>Logs de auditoria</li>
</ol>
<p>Os requisitos a seguir são desejáveis mas opcionais:</p>
<ol start="4">
<li>Dashboard de monitoramento em tempo real</li>
<li>Exportação de relatórios em PDF</li>
</ol>
reversed — contagem regressiva
O atributo booleano reversed inverte a direção da contagem, produzindo uma sequência decrescente. Útil para rankings, contagens regressivas e listas onde os itens mais recentes têm numeração maior:
<!-- Top 5 tecnologias mais usadas (5 → 1) -->
<ol reversed>
<li>Vue.js</li>
<li>Angular</li>
<li>TypeScript</li>
<li>React</li>
<li>JavaScript</li>
</ol>
O atributo value em <li> permite sobrescrever o número de um item específico, fazendo com que os itens subsequentes continuem a partir desse valor:
<ol>
<li>Primeiro item</li> <!-- 1 -->
<li value="5">Quinto item</li> <!-- 5 -->
<li>Sexto item</li> <!-- 6 (continua a partir de 5) -->
</ol>
Referência: MDN — <ol>: The Ordered List element
4.2.2 — Listas de definição em profundidade: <dl>, <dt> e <dd>¶
A lista de definições é o tipo de lista menos compreendido e mais subutilizado do HTML. Sua semântica vai além de simples glossários: o elemento <dl> representa qualquer conjunto de pares nome–valor onde os nomes (<dt>) descrevem ou rotulam os valores (<dd>).
A estrutura fundamental:
<dl>
<dt>HTTP</dt>
<dd>
Protocolo de transferência de hipertexto; define as regras
de comunicação entre clientes e servidores na Web.
</dd>
<dt>DNS</dt>
<dd>
Sistema de nomes de domínio; traduz nomes de domínio
legíveis por humanos em endereços IP numéricos.
</dd>
</dl>
Variações semânticas importantes
Um único <dt> pode ter múltiplos <dd> associados (um termo com várias definições ou valores):
<dl>
<dt>Formatos de imagem suportados</dt>
<dd>JPEG — fotografias e gradientes complexos</dd>
<dd>PNG — imagens com transparência e texto</dd>
<dd>WebP — substituto moderno para JPEG e PNG</dd>
<dd>SVG — gráficos vetoriais escaláveis</dd>
</dl>
Múltiplos <dt> podem compartilhar um único <dd> (sinônimos ou termos equivalentes com a mesma definição):
<dl>
<dt>Frontend</dt>
<dt>Client-side</dt>
<dd>
Camada de desenvolvimento responsável pela interface
com a qual o usuário interage diretamente no navegador.
</dd>
</dl>
Usos além do glossário
A especificação permite o uso de <dl> para qualquer estrutura de metadados nome–valor, o que o torna adequado para uma variedade de contextos práticos:
<!-- Ficha técnica de um produto -->
<dl>
<dt>Autor</dt>
<dd>Tim Berners-Lee</dd>
<dt>Publicação</dt>
<dd><time datetime="1989-03-12">12 de março de 1989</time></dd>
<dt>Organização</dt>
<dd>CERN — Organização Europeia para a Pesquisa Nuclear</dd>
<dt>Título do documento</dt>
<dd><cite>Information Management: A Proposal</cite></dd>
</dl>
<!-- Perguntas frequentes (FAQ) -->
<dl>
<dt>O que é o HTML Living Standard?</dt>
<dd>
É a especificação em evolução contínua do HTML,
mantida pelo WHATWG desde 2004.
</dd>
<dt>Qual a diferença entre HTML e XHTML?</dt>
<dd>
O XHTML é uma reformulação do HTML 4 com sintaxe
XML estrita; o HTML5 retomou a abordagem mais
tolerante do HTML original.
</dd>
</dl>
Referência: MDN — <dl>: The Description List element
4.2.3 — Listas aninhadas e hierarquias complexas¶
O aninhamento de listas é o mecanismo HTML para representar hierarquias de conteúdo — estruturas onde itens possuem subitens, que por sua vez podem ter seus próprios subitens. O aninhamento é realizado inserindo um novo elemento de lista (<ul> ou <ol>) diretamente dentro de um elemento <li>, nunca diretamente dentro de <ul> ou <ol>.
<!-- CORRETO: lista aninhada dentro de <li> -->
<ul>
<li>Frontend
<ul>
<li>HTML</li>
<li>CSS
<ul>
<li>Flexbox</li>
<li>Grid</li>
</ul>
</li>
<li>JavaScript</li>
</ul>
</li>
<li>Backend
<ul>
<li>Node.js</li>
<li>Python</li>
</ul>
</li>
</ul>
<!-- INCORRETO: lista aninhada fora do <li> -->
<ul>
<li>Frontend</li>
<ul> <!-- ERRADO: <ul> não pode ser filho direto de outro <ul> -->
<li>HTML</li>
</ul>
</ul>
Listas de tipos diferentes podem ser aninhadas livremente:
<!-- Sumário de um documento técnico -->
<ol>
<li>Introdução
<ol type="a">
<li>Contexto e motivação</li>
<li>Objetivos</li>
<li>Estrutura do documento</li>
</ol>
</li>
<li>Fundamentação teórica
<ul>
<li>Protocolos de rede</li>
<li>Arquitetura cliente-servidor</li>
</ul>
</li>
<li>Metodologia</li>
<li>Resultados</li>
<li>Conclusão</li>
</ol>
Profundidade de aninhamento e legibilidade
A especificação não impõe limite técnico para a profundidade de aninhamento de listas. Contudo, aninhamentos com mais de três níveis raramente são adequados para a Web: eles indicam, na maioria dos casos, que o conteúdo deveria ser reorganizado em seções distintas com títulos (<section> + <h2>/<h3>) em vez de uma estrutura de lista profundamente aninhada.
Atividade — Seção 4.2¶
1. Em uma lista <ol reversed start="10"> com quatro itens, quais serão os números exibidos?
2. Qual estrutura <dl> é semanticamente válida para representar um termo com três definições distintas?
- GitHub Classroom: Construir um glossário de termos técnicos da Web utilizando
<dl>,<dt>e<dd>, com pelo menos 8 termos, incluindo dois termos com múltiplas definições e dois sinônimos compartilhando uma mesma definição. (link será adicionado)
4.3 — Multimídia¶
Vídeo curto explicativo (link será adicionado posteriormente)
A Web moderna é essencialmente multimídia. Imagens, vídeos, áudios e conteúdos incorporados de plataformas externas são componentes ubíquos de páginas e aplicações web. O HTML5 consolidou o suporte nativo a esses recursos, eliminando a dependência de plugins externos como o Adobe Flash e estabelecendo uma base declarativa para a incorporação de mídia diretamente no documento.
4.3.1 — Imagens revisitadas: srcset, sizes e <picture>¶
O elemento <img> foi introduzido no Capítulo 2 com seus atributos fundamentais (src, alt, width, height, loading). Para cenários mais sofisticados — especialmente o contexto de design responsivo, onde a mesma página é acessada por dispositivos com telas de tamanhos e densidades radicalmente diferentes —, o HTML oferece mecanismos adicionais que permitem ao navegador selecionar a imagem mais adequada para cada contexto.
O atributo srcset
O atributo srcset fornece ao navegador um conjunto de imagens candidatas, cada uma com um descritor que indica sua largura em pixels ou sua densidade de pixels em relação à tela padrão. O navegador seleciona automaticamente a imagem mais adequada com base na tela do dispositivo, na resolução e na velocidade da conexão.
<!-- Descritor de largura (w): o navegador escolhe com base no tamanho
do espaço disponível para a imagem -->
<img
src="foto-800.jpg"
srcset="
foto-400.jpg 400w,
foto-800.jpg 800w,
foto-1200.jpg 1200w
"
alt="Vista aérea do campus do IFAL"
width="800"
height="450"
/>
<!-- Descritor de densidade (x): o navegador escolhe com base
na densidade de pixels da tela -->
<img
src="logo.png"
srcset="
logo.png 1x,
logo@2x.png 2x,
logo@3x.png 3x
"
alt="Logotipo IFAL"
width="120"
height="60"
/>
O atributo sizes
Quando o atributo srcset utiliza descritores de largura (w), o atributo sizes é necessário para informar ao navegador qual será o tamanho de exibição da imagem em diferentes condições de viewport. Sem sizes, o navegador assume que a imagem ocupará 100% da largura do viewport — o que raramente é verdade.
<img
src="foto-800.jpg"
srcset="
foto-400.jpg 400w,
foto-800.jpg 800w,
foto-1200.jpg 1200w
"
sizes="
(max-width: 600px) 100vw,
(max-width: 1024px) 50vw,
800px
"
alt="Diagrama da arquitetura de microsserviços"
width="800"
height="500"
/>
Neste exemplo, o navegador é informado de que: em viewports de até 600px, a imagem ocupa 100% da largura; em viewports de até 1024px, ocupa 50%; em viewports maiores, ocupa exatamente 800px. Com essa informação, o navegador pode calcular qual das imagens do srcset é suficiente — evitando o download de uma imagem de 1200px quando uma de 400px seria adequada.
O elemento <picture>
O elemento <picture> vai além de simplesmente selecionar a melhor versão de uma imagem: ele permite trocar completamente a imagem com base em condições de mídia, como orientação da tela, largura do viewport ou suporte a formatos modernos. É composto por zero ou mais elementos <source> e exatamente um elemento <img> de fallback.
<!-- Caso de uso 1: suporte a formatos modernos com fallback -->
<picture>
<!-- Navegadores que suportam AVIF recebem AVIF (melhor compressão) -->
<source type="image/avif" srcset="foto.avif" />
<!-- Navegadores que suportam WebP recebem WebP -->
<source type="image/webp" srcset="foto.webp" />
<!-- Fallback universal: JPEG -->
<img src="foto.jpg" alt="Vista do laboratório de informática" width="800" height="450" />
</picture>
<!-- Caso de uso 2: imagens diferentes para diferentes viewports
(art direction — mudança de enquadramento, não apenas de tamanho) -->
<picture>
<!-- Em viewports largos: foto paisagem com contexto amplo -->
<source
media="(min-width: 768px)"
srcset="campus-wide.jpg"
/>
<!-- Em viewports estreitos: recorte em close da entrada principal -->
<img
src="campus-mobile.jpg"
alt="Entrada principal do campus IFAL Arapiraca"
width="400"
height="400"
/>
</picture>
A ordem dos elementos <source> dentro de <picture> é importante: o navegador utiliza o primeiro <source> cujas condições são atendidas. Fontes mais específicas ou mais otimizadas devem ser listadas primeiro; o <img> de fallback deve ser sempre o último elemento.
Referências:
- MDN — Responsive images
- MDN — <picture>: The Picture element
4.3.2 — Vídeo nativo: <video>, múltiplas fontes e <track>¶
O elemento <video> foi introduzido no HTML5 como mecanismo nativo para incorporar vídeo sem dependência de plugins. Ele oferece controle declarativo sobre reprodução, dimensões, comportamento de pré-carregamento e legibilidade do conteúdo.
Estrutura básica e atributos fundamentais
<video
src="aula-html-semantico.mp4"
controls
width="800"
height="450"
poster="thumbnail-aula.jpg"
preload="metadata"
>
<p>
Seu navegador não suporta reprodução de vídeo HTML5.
<a href="aula-html-semantico.mp4">Baixe o vídeo aqui</a>.
</p>
</video>
| Atributo | Tipo | Descrição |
|---|---|---|
src |
URL | Caminho do arquivo de vídeo |
controls |
booleano | Exibe os controles nativos do navegador (play, pause, volume, etc.) |
width / height |
número | Dimensões em pixels; previnem layout shift |
poster |
URL | Imagem de miniatura exibida antes da reprodução |
preload |
none / metadata / auto |
Controla o pré-carregamento do vídeo |
autoplay |
booleano | Inicia a reprodução automaticamente (requer muted) |
muted |
booleano | Inicia sem áudio (necessário para autoplay funcionar) |
loop |
booleano | Reinicia automaticamente ao terminar |
playsinline |
booleano | Reproduz em linha em dispositivos iOS (sem tela cheia forçada) |
Múltiplas fontes para compatibilidade
Diferentes navegadores suportam diferentes formatos de vídeo. Para garantir compatibilidade universal, é recomendado fornecer o vídeo em múltiplos formatos utilizando elementos <source> filhos — o navegador utiliza o primeiro formato que consegue decodificar:
<video controls width="800" height="450" poster="thumbnail.jpg">
<!-- MP4/H.264: suporte universal -->
<source src="aula.mp4" type="video/mp4" />
<!-- WebM/VP9: melhor compressão, suporte na maioria dos navegadores modernos -->
<source src="aula.webm" type="video/webm" />
<!-- Fallback para navegadores sem suporte a <video> -->
<p>
Seu navegador não suporta vídeo HTML5.
<a href="aula.mp4">Baixe o arquivo</a>.
</p>
</video>
Legendas e transcrições: o elemento <track>
O elemento <track> associa arquivos de texto temporizado ao vídeo — legendas, transcrições, descrições de áudio e capítulos. É o mecanismo fundamental para tornar vídeos acessíveis a pessoas surdas ou com deficiência auditiva, e é exigido pelas diretrizes WCAG 2.1 (critério de sucesso 1.2.2 — Legendas pré-gravadas, nível A).
<video controls width="800" height="450">
<source src="aula-html.mp4" type="video/mp4" />
<!-- Legendas em português (padrão) -->
<track
kind="subtitles"
src="legendas/aula-html-pt.vtt"
srclang="pt"
label="Português"
default
/>
<!-- Legendas em inglês -->
<track
kind="subtitles"
src="legendas/aula-html-en.vtt"
srclang="en"
label="English"
/>
<!-- Descrição de áudio para usuários com deficiência visual -->
<track
kind="descriptions"
src="legendas/aula-html-descricao.vtt"
srclang="pt"
label="Descrição de áudio"
/>
</video>
Os arquivos referenciados pelo <track> utilizam o formato WebVTT (Web Video Text Tracks), um formato de texto simples que associa intervalos de tempo a fragmentos de texto:
WEBVTT
00:00:00.000 --> 00:00:04.500
Bem-vindos à aula sobre HTML Semântico.
00:00:04.500 --> 00:00:09.000
Nesta aula, vamos aprender a utilizar os elementos
corretos para cada tipo de conteúdo.
Os valores possíveis para o atributo kind do <track> são: subtitles (legendas para conteúdo falado), captions (legendas completas incluindo efeitos sonoros), descriptions (descrição de áudio do conteúdo visual), chapters (marcadores de capítulo para navegação) e metadata (dados para uso por scripts).
Referências:
- MDN — <video>: The Video Embed element
- MDN — Adicionando legendas e subtítulos ao vídeo HTML5
- W3C — WebVTT: The Web Video Text Tracks Format
4.3.3 — Áudio nativo: <audio>¶
O elemento <audio> segue o mesmo modelo do <video>: suporta múltiplas fontes via <source>, aceita os atributos controls, autoplay, muted, loop e preload, e oferece um conteúdo de fallback para navegadores sem suporte.
<audio controls preload="metadata">
<source src="podcast-ep01.mp3" type="audio/mpeg" />
<source src="podcast-ep01.ogg" type="audio/ogg" />
<p>
Seu navegador não suporta áudio HTML5.
<a href="podcast-ep01.mp3">Baixe o arquivo de áudio</a>.
</p>
</audio>
As principais diferenças em relação ao <video> são a ausência dos atributos width, height e poster — que são específicos de conteúdo visual — e o suporte a um conjunto diferente de formatos de arquivo:
| Formato | MIME type | Suporte |
|---|---|---|
| MP3 | audio/mpeg |
Universal |
| OGG Vorbis | audio/ogg |
Firefox, Chrome, Opera |
| WAV | audio/wav |
Suporte amplo, mas arquivos grandes |
| AAC | audio/aac |
Safari, Chrome, Edge |
| WebM (Opus) | audio/webm |
Chrome, Firefox, Edge |
Para conteúdo de áudio que acompanha informações importantes — como instruções em áudio, podcasts educacionais ou narrações de conteúdo —, as diretrizes WCAG recomendam fornecer uma transcrição textual (critério 1.2.1). O elemento <track> com kind="captions" pode ser utilizado em <audio> da mesma forma que em <video>.
Referência: MDN — <audio>: The Embed Audio element
4.3.4 — Embeds externos: <iframe>¶
O elemento <iframe> (inline frame) incorpora um documento HTML externo dentro do documento atual, renderizando-o em uma área retangular delimitada. É o mecanismo padrão para incorporar conteúdo de plataformas externas — vídeos do YouTube, podcasts do Spotify, mapas do Google Maps, visualizações do Figma, repositórios do GitHub e uma variedade de outros serviços.
Estrutura básica
<iframe
src="https://www.youtube.com/embed/hBRDMaxKB8Q"
title="O que é e como funciona a internet"
width="800"
height="450"
allowfullscreen
loading="lazy"
>
</iframe>
O atributo title é obrigatório do ponto de vista da acessibilidade: ele fornece um rótulo descritivo para o <iframe> que é anunciado por leitores de tela ao focar o elemento. Sem o title, o usuário de leitor de tela não tem como saber o que o <iframe> contém antes de navegar para dentro dele.
Atributos relevantes
| Atributo | Descrição |
|---|---|
src |
URL do documento a ser incorporado |
title |
Descrição acessível do conteúdo (obrigatório para acessibilidade) |
width / height |
Dimensões do frame em pixels |
allowfullscreen |
Permite que o conteúdo entre em tela cheia |
loading="lazy" |
Adia o carregamento do frame até que ele esteja próximo do viewport |
sandbox |
Aplica restrições de segurança ao conteúdo incorporado |
allow |
Define permissões específicas (câmera, microfone, autoplay, etc.) |
referrerpolicy |
Controla as informações de referência enviadas ao servidor externo |
Incorporando conteúdo de plataformas comuns
As principais plataformas de mídia geram automaticamente o código de incorporação (<iframe>) com os atributos corretos. O fluxo padrão é: acessar a plataforma → localizar a opção "Compartilhar" ou "Incorporar" → copiar o código gerado → ajustar width, height e adicionar title e loading="lazy".
<!-- YouTube -->
<iframe
width="800"
height="450"
src="https://www.youtube-nocookie.com/embed/VIDEO_ID?rel=0"
title="Título descritivo do vídeo"
allow="accelerometer; autoplay; clipboard-write; encrypted-media;
gyroscope; picture-in-picture"
allowfullscreen
loading="lazy"
></iframe>
<!-- Spotify — episódio de podcast -->
<iframe
style="border-radius:12px"
src="https://open.spotify.com/embed/episode/EPISODE_ID"
width="100%"
height="152"
title="Nome do episódio do podcast"
allow="autoplay; clipboard-write; encrypted-media; fullscreen;
picture-in-picture"
loading="lazy"
></iframe>
<!-- Google Maps -->
<iframe
src="https://www.google.com/maps/embed?pb=EMBED_CODE"
width="600"
height="450"
title="Localização do IFAL Campus Arapiraca"
style="border:0;"
allowfullscreen
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
></iframe>
Observação sobre youtube-nocookie.com: o domínio youtube-nocookie.com é uma alternativa ao domínio padrão do YouTube que não define cookies de rastreamento enquanto o usuário não interage com o player. Para materiais educacionais e ambientes institucionais, é a opção recomendada.
Segurança: o atributo sandbox
O atributo sandbox aplica um conjunto de restrições ao conteúdo do <iframe>, impedindo que scripts maliciosos no conteúdo incorporado acessem o documento pai, executem formulários ou abram janelas pop-up. Quando sandbox é declarado sem valores, todas as restrições são aplicadas:
<!-- sandbox sem valores: máxima restrição -->
<iframe src="conteudo-externo.html" sandbox title="..."></iframe>
<!-- sandbox com permissões específicas habilitadas -->
<iframe
src="widget-interativo.html"
sandbox="allow-scripts allow-same-origin"
title="Widget interativo"
></iframe>
Para conteúdo de plataformas conhecidas como YouTube e Google Maps, o uso de sandbox pode interferir na funcionalidade do player e geralmente não é aplicado.
Referências:
- MDN — <iframe>: The Inline Frame element
- MDN — iframe sandbox
4.3.5 — <figure> e <figcaption> no contexto de mídia¶
O elemento <figure> — introduzido no Capítulo 3 no contexto semântico geral — tem aplicação especialmente relevante no contexto de mídia. Ele agrupa qualquer conteúdo multimídia (imagem, vídeo, áudio, <iframe>) com sua legenda correspondente, estabelecendo uma relação semântica entre o conteúdo e sua descrição.
<!-- Vídeo com legenda -->
<figure>
<video controls width="800" height="450" poster="thumb.jpg">
<source src="demo-html5.mp4" type="video/mp4" />
<track kind="subtitles" src="demo-pt.vtt" srclang="pt" label="Português" default />
</video>
<figcaption>
Vídeo 1 — Demonstração das APIs multimídia do HTML5:
elemento <code><video></code> com legendas WebVTT.
</figcaption>
</figure>
<!-- Embed externo com legenda -->
<figure>
<iframe
width="100%"
height="315"
src="https://www.youtube-nocookie.com/embed/hBRDMaxKB8Q"
title="O que é e como funciona a internet"
allowfullscreen
loading="lazy"
></iframe>
<figcaption>
Figura 3 — Vídeo introdutório: "O que é e como funciona a internet".
Fonte: Canal Código Fonte TV, YouTube.
</figcaption>
</figure>
<!-- Áudio com transcrição -->
<figure>
<audio controls>
<source src="entrevista-berners-lee.mp3" type="audio/mpeg" />
</audio>
<figcaption>
Áudio 1 — Trecho da entrevista de Tim Berners-Lee sobre
o futuro da Web aberta (2024).
<a href="transcricao.html">Ler transcrição completa</a>.
</figcaption>
</figure>
A combinação de <figure> com <figcaption> é especialmente importante para vídeos e áudios em materiais educacionais: ela fornece contexto imediato sobre o conteúdo da mídia, cita a fonte quando necessário, e oferece um ponto de ancoragem para links à transcrição ou aos recursos adicionais.
🔎 Direitos autorais¶
⚠️ Importante:
Nem toda imagem, vídeo ou áudio disponível na internet pode ser utilizado livremente.
Utilize bancos de mídia gratuitos:
- Unsplash
- Pexels
- Pixabay
Referências gerais desta seção: - MDN — Multimídia e incorporação - W3C — WCAG 2.1 — Guideline 1.2: Time-based Media - WebAIM — Accessible Rich Media
Atividade — Seção 4.3¶
1. Qual é a diferença funcional entre o atributo srcset no elemento <img> e o elemento <picture>?
2. Por que o atributo title é obrigatório do ponto de vista da acessibilidade em um elemento <iframe>?
3. Qual elemento HTML é responsável por associar arquivos de legenda no formato WebVTT a um elemento <video>?
- GitHub Classroom: Construir uma página multimídia que contenha: um elemento
<video>com múltiplas fontes, um<track>de legendas e um<poster>; um elemento<audio>com fallback; um<iframe>incorporando um vídeo do YouTube comtitleeloading="lazy"; todos envolvidos por<figure>com<figcaption>adequados. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 3 — HTML Semântico :material-arrow-right: Ir ao Capítulo 5 — Formulários HTML
Capítulo 5 — Formulários HTML¶
Vídeo curto explicativo (link será adicionado posteriormente)
5.1 — O papel dos formulários na Web¶
Vídeo curto explicativo (link será adicionado posteriormente)
Formulários são o principal mecanismo de comunicação bidirecional entre o usuário e um servidor na Web. Enquanto a maioria dos elementos HTML serve para apresentar informações ao usuário — textos, imagens, vídeos —, os formulários invertem essa direção: eles coletam dados inseridos pelo usuário e os transmitem para processamento.
Essa distinção é fundamental para compreender o papel dos formulários no desenvolvimento web: o HTML apenas estrutura e envia os dados. Ele não os processa. O processamento — autenticar um login, salvar um cadastro em banco de dados, enviar um e-mail, processar um pagamento — é sempre responsabilidade do backend, um programa executado no servidor escrito em linguagens como Python, PHP, Node.js ou Java.
5.1.1 — O fluxo completo de envio de um formulário¶
Quando um usuário preenche e envia um formulário, ocorre a seguinte sequência de eventos:
[1] Usuário preenche os campos e clica em "Enviar"
↓
[2] Navegador coleta os dados dos campos
↓
[3] Navegador monta uma requisição HTTP com os dados
↓
[4] Requisição é enviada ao servidor indicado no atributo action do <form>
↓
[5] Servidor recebe os dados, executa a lógica de negócio e gera uma resposta
↓
[6] Navegador recebe a resposta e exibe o resultado ao usuário
Este fluxo é idêntico ao ciclo de requisição-resposta HTTP estudado no Capítulo 1 — a diferença é que, em vez de simplesmente solicitar um recurso, o navegador está enviando dados junto com a requisição.
Ponto crítico: toda validação realizada no navegador via HTML (atributos
required,pattern, etc.) ou via JavaScript pode ser contornada por um usuário técnico. Por essa razão, a validação no frontend é uma conveniência para o usuário — nunca uma medida de segurança. A validação definitiva sempre deve ocorrer no servidor (backend). Este princípio será aprofundado nos capítulos de back-end e segurança.
5.2 — Estrutura básica: <form>, action e method¶
Vídeo curto explicativo (link será adicionado posteriormente)
O elemento <form> é o contêiner que delimita um formulário HTML. Ele possui dois atributos fundamentais que determinam para onde e como os dados serão enviados:
<form action="/cadastro" method="post">
<!-- campos do formulário -->
</form>
5.2.1 — O atributo action¶
O atributo action especifica a URL de destino para a qual os dados do formulário serão enviados quando o usuário submeter o formulário. Pode ser uma URL absoluta ou relativa:
<!-- URL relativa: envia para /login no mesmo servidor -->
<form action="/login" method="post">...</form>
<!-- URL absoluta: envia para um servidor externo -->
<form action="https://api.exemplo.com/contato" method="post">...</form>
<!-- Sem action: envia para a própria página atual -->
<form method="get">...</form>
Quando action é omitido, o formulário é enviado para a URL da página atual — comportamento útil em páginas que processam o próprio formulário no servidor.
5.2.2 — O atributo method: GET vs POST¶
O atributo method define o método HTTP utilizado para enviar os dados. Os dois valores possíveis são get e post, e a escolha entre eles tem implicações técnicas, semânticas e de segurança significativas.
method="get" — dados na URL
Com o método GET, os dados do formulário são codificados na query string da URL, visíveis na barra de endereços do navegador:
<form action="https://httpbin.org/get" method="get">
<label for="busca">Buscar:</label>
<input type="text" id="busca" name="q" />
<button type="submit">Buscar</button>
</form>
Após o envio com o valor "HTML semântico", a URL resultante seria:
https://httpbin.org/get?q=HTML+sem%C3%A2ntico
Os dados aparecem após o ? na forma de pares nome=valor, separados por & quando há múltiplos campos.
method="post" — dados no corpo da requisição
Com o método POST, os dados são enviados no corpo (body) da requisição HTTP, invisíveis na URL:
<form action="https://httpbin.org/post" method="post">
<label for="email">E-mail:</label>
<input type="email" id="email" name="email" />
<label for="senha">Senha:</label>
<input type="password" id="senha" name="senha" />
<button type="submit">Entrar</button>
</form>
Após o envio, a URL permanece https://httpbin.org/post — sem nenhum dado exposto.
Comparativo GET vs POST
| Critério | GET | POST |
|---|---|---|
| Dados na URL | ✅ Sim | ❌ Não |
| Histórico do navegador | Dados ficam no histórico | Dados não ficam no histórico |
| Bookmarkável | ✅ Sim | ❌ Não |
| Limite de dados | ~2.000 caracteres (varia por navegador/servidor) | Sem limite prático |
| Cache | Pode ser cacheado | Não é cacheado por padrão |
| Idempotência | Idempotente (repetir não muda estado) | Não idempotente |
| Uso adequado | Buscas, filtros, navegação | Login, cadastro, envio de arquivos, pagamentos |
⚠️ Alerta de segurança: nunca use
method="get"para formulários que transmitem dados sensíveis — senhas, tokens, dados pessoais, informações financeiras. Com GET, esses dados aparecem na URL, ficam armazenados no histórico do navegador e nos logs do servidor, e podem ser expostos no cabeçalho HTTPRefererao navegar para outra página.
5.2.3 — Testando o envio com httpbin.org¶
O serviço httpbin.org é uma ferramenta gratuita que recebe requisições HTTP e retorna seus dados em formato JSON — ideal para inspecionar exatamente o que um formulário está enviando durante o desenvolvimento:
<!-- Teste com GET: observe os dados na URL e no JSON retornado -->
<form action="https://httpbin.org/get" method="get">
<input type="text" name="usuario" placeholder="Seu nome" />
<input type="email" name="email" placeholder="Seu e-mail" />
<button type="submit">Testar GET</button>
</form>
<!-- Teste com POST: observe os dados no corpo JSON retornado -->
<form action="https://httpbin.org/post" method="post">
<input type="text" name="usuario" placeholder="Seu nome" />
<input type="email" name="email" placeholder="Seu e-mail" />
<button type="submit">Testar POST</button>
</form>
Ao submeter esses formulários, o httpbin retorna uma resposta JSON que exibe exatamente quais dados foram recebidos — incluindo cabeçalhos, método, IP de origem e os parâmetros enviados. É uma forma concreta de visualizar a diferença entre GET e POST sem necessidade de um backend próprio.
Acesse: https://httpbin.org
5.3 — Campos de entrada: <input> e seus tipos fundamentais¶
Vídeo curto explicativo (link será adicionado posteriormente)
O elemento <input> é o componente mais versátil dos formulários HTML. Seu comportamento é inteiramente determinado pelo atributo type — que define não apenas a aparência do campo, mas também o teclado virtual exibido em dispositivos móveis, as restrições de entrada aceitas e o comportamento de validação nativa do navegador.
5.3.1 — Os atributos name e id: distinção fundamental¶
Antes de explorar os tipos de <input>, é essencial compreender a diferença entre dois atributos que frequentemente causam confusão em iniciantes: name e id.
name — identidade para envio de dados
O atributo name define o identificador do campo nos dados enviados ao servidor. Quando o formulário é submetido, o navegador monta pares nome=valor para cada campo — e o "nome" é exatamente o valor do atributo name.
⚠️ Erro muito comum: campos sem o atributo
namenão são enviados ao servidor. Um campo pode estar visível, preenchido e validado — mas se não tivername, seus dados simplesmente não chegam ao backend. Este é um dos erros mais frequentes em formulários criados por iniciantes.
<!-- Este campo SERÁ enviado: tem name -->
<input type="text" name="usuario" id="campo-usuario" />
<!-- Este campo NÃO será enviado: não tem name -->
<input type="text" id="campo-teste" />
id — identidade no documento HTML
O atributo id identifica o elemento de forma única no documento HTML. Ele é utilizado para: associar um <label> ao campo (via for), referenciar o campo via CSS (seletores #id) e manipular o campo via JavaScript (document.getElementById). O id não tem nenhuma influência sobre o envio de dados.
Resumo prático:
| Atributo | Para que serve | Obrigatório para envio? |
|---|---|---|
name |
Identificar o dado no servidor | ✅ Sim |
id |
Identificar o elemento no documento | ❌ Não (mas recomendado para acessibilidade) |
Na prática, ambos são geralmente declarados e, por convenção, recebem o mesmo valor — mas são conceitualmente independentes:
<input type="text" name="email" id="email" />
5.3.2 — Tipos fundamentais de <input>¶
type="text" — texto livre
<label for="nome">Nome completo:</label>
<input
type="text"
id="nome"
name="nome"
placeholder="Ex.: Maria Silva"
autocomplete="name"
/>
type="email" — endereço de e-mail
O navegador valida automaticamente o formato básico de e-mail (presença de @ e domínio):
<label for="email">E-mail:</label>
<input
type="email"
id="email"
name="email"
placeholder="usuario@dominio.com"
autocomplete="email"
/>
type="password" — senha
O valor digitado é ocultado visualmente. O dado é enviado em texto simples pelo HTML — a criptografia da transmissão é responsabilidade do protocolo HTTPS, não do formulário:
<label for="senha">Senha:</label>
<input
type="password"
id="senha"
name="senha"
minlength="8"
autocomplete="current-password"
/>
type="number" — valor numérico
<label for="idade">Idade:</label>
<input
type="number"
id="idade"
name="idade"
min="18"
max="120"
step="1"
/>
type="checkbox" — múltiplas escolhas independentes
Checkboxes representam opções independentes entre si — o usuário pode marcar zero, uma ou várias. Cada checkbox envia seu valor apenas quando marcado; quando desmarcado, o campo não é enviado.
<!-- Cada checkbox tem name diferente: são dados independentes -->
<fieldset>
<legend>Tecnologias que você conhece:</legend>
<label>
<input type="checkbox" name="html" value="sim" /> HTML
</label>
<label>
<input type="checkbox" name="css" value="sim" /> CSS
</label>
<label>
<input type="checkbox" name="javascript" value="sim" /> JavaScript
</label>
</fieldset>
Para enviar múltiplos valores sob o mesmo nome (como um array), todos os checkboxes de um grupo recebem o mesmo name com colchetes:
<!-- Todos com o mesmo name: enviados como array no backend -->
<input type="checkbox" name="tecnologias[]" value="html" /> HTML
<input type="checkbox" name="tecnologias[]" value="css" /> CSS
<input type="checkbox" name="tecnologias[]" value="js" /> JavaScript
type="radio" — escolha exclusiva
Radio buttons representam opções mutuamente exclusivas — o usuário deve escolher exatamente uma. O agrupamento é definido pelo atributo name: todos os radio buttons com o mesmo name pertencem ao mesmo grupo, e apenas um pode estar selecionado por vez.
<fieldset>
<legend>Período de preferência:</legend>
<!-- Mesmo name = mesmo grupo = escolha exclusiva -->
<label>
<input type="radio" name="periodo" value="manha" /> Manhã
</label>
<label>
<input type="radio" name="periodo" value="tarde" /> Tarde
</label>
<label>
<input type="radio" name="periodo" value="noite" /> Noite
</label>
</fieldset>
Diferença prática entre checkbox e radio:
checkbox |
radio |
|
|---|---|---|
| Quantas opções o usuário pode escolher | Zero ou mais | Exatamente uma |
| Agrupamento | Por convenção (mesmo name[]) |
Por name (obrigatório) |
| Valor enviado quando não marcado | Nenhum | Nenhum |
| Caso de uso | "Quais linguagens você conhece?" | "Qual seu período preferido?" |
type="submit" — botão de envio
<input type="submit" value="Enviar formulário" />
Na prática, o elemento <button type="submit"> é mais flexível e preferível, pois aceita conteúdo HTML interno (como ícones):
<button type="submit">Enviar formulário</button>
⚠️ Alerta: um elemento
<button>sem o atributotypeexplícito é tratado comotype="submit"por padrão — e submeterá o formulário ao ser clicado, mesmo que o desenvolvedor não tenha essa intenção. Sempre declare otypeexplicitamente em botões dentro de formulários:<button type="submit">Enviar</button> <!-- envia o formulário --> <button type="button">Cancelar</button> <!-- não envia nada --> <button type="reset">Limpar</button> <!-- limpa os campos -->
5.4 — Tipos de input menos conhecidos¶
Vídeo curto explicativo (link será adicionado posteriormente)
Além dos tipos fundamentais, o HTML5 introduziu uma série de tipos de <input> especializados que oferecem interfaces nativas do sistema operacional — seletores de data, sliders, paletas de cor — sem necessidade de JavaScript. A adoção desses tipos melhora significativamente a experiência do usuário, especialmente em dispositivos móveis, onde o navegador exibe teclados virtuais otimizados para cada tipo de dado.
⚠️ Compatibilidade: o suporte a tipos de input avançados varia entre navegadores e sistemas operacionais. Em navegadores que não reconhecem um
typeespecífico, o campo é renderizado comotype="text"— o que garante degradação graceful (o campo ainda funciona, mas sem a interface especializada). Antes de usar esses tipos em produção, verifique o suporte atual em https://caniuse.com.
5.4.1 — Data e hora¶
<!-- Data (ano-mês-dia) -->
<label for="nascimento">Data de nascimento:</label>
<input
type="date"
id="nascimento"
name="nascimento"
min="1900-01-01"
max="2010-12-31"
/>
<!-- Hora (horas:minutos) -->
<label for="horario">Horário preferido:</label>
<input type="time" id="horario" name="horario" min="08:00" max="22:00" />
<!-- Data e hora local (sem fuso horário) -->
<label for="agendamento">Agendamento:</label>
<input type="datetime-local" id="agendamento" name="agendamento" />
<!-- Mês e ano -->
<label for="competencia">Competência:</label>
<input type="month" id="competencia" name="competencia" />
<!-- Semana do ano -->
<label for="semana">Semana de início:</label>
<input type="week" id="semana" name="semana" />
O valor enviado ao servidor segue o formato ISO 8601 (ex.: 2026-03-20 para datas, 14:30 para horas) — independentemente do formato visual exibido ao usuário, que varia conforme as configurações de locale do sistema operacional.
Nota de compatibilidade:
type="date"tem suporte amplo em navegadores modernos, mas a aparência do seletor varia significativamente entre plataformas. Em Safari iOS, o seletor usa rodas giratórias; no Chrome Android, usa um calendário visual. Para projetos que exigem aparência consistente entre plataformas, bibliotecas JavaScript de date picker são frequentemente preferidas.
5.4.2 — Intervalo numérico: type="range"¶
Renderiza um controle deslizante (slider) para seleção de valores numéricos em um intervalo:
<label for="volume">Volume: <span id="valor-volume">50</span>%</label>
<input
type="range"
id="volume"
name="volume"
min="0"
max="100"
step="5"
value="50"
/>
O type="range" não exibe o valor atual por padrão — é necessário JavaScript para atualizar um elemento de texto em tempo real. O valor enviado é numérico.
5.4.3 — Cor: type="color"¶
Abre o seletor de cores nativo do sistema operacional:
<label for="cor-tema">Cor do tema:</label>
<input
type="color"
id="cor-tema"
name="cor_tema"
value="#E8632A"
/>
O valor enviado ao servidor é sempre um código hexadecimal de 6 dígitos em minúsculas (ex.: #e8632a).
Nota de compatibilidade: o seletor de cor nativo varia significativamente entre sistemas operacionais — macOS, Windows e Android exibem interfaces completamente diferentes. O atributo
valuedeve ser sempre um código hexadecimal de 6 dígitos; valores comorgb()ou nomes de cor não são aceitos.
5.4.4 — Upload de arquivo: type="file"¶
Permite que o usuário selecione um ou mais arquivos do dispositivo para envio:
<!-- Arquivo único -->
<label for="curriculo">Currículo (PDF):</label>
<input
type="file"
id="curriculo"
name="curriculo"
accept=".pdf"
/>
<!-- Múltiplos arquivos -->
<label for="fotos">Fotos do projeto:</label>
<input
type="file"
id="fotos"
name="fotos[]"
accept="image/jpeg, image/png, image/webp"
multiple
/>
⚠️ Atenção técnica: formulários com
type="file"obrigatoriamente requeremmethod="post"e o atributoenctype="multipart/form-data"no elemento<form>. Sem essas configurações, o arquivo não é enviado corretamente — apenas o nome do arquivo é transmitido.<form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="arquivo" /> <button type="submit">Enviar arquivo</button> </form>
5.4.5 — Outros tipos úteis¶
<!-- Busca: semântica de campo de pesquisa (pode exibir botão de limpar) -->
<input type="search" name="q" placeholder="Buscar..." />
<!-- Telefone: exibe teclado numérico em dispositivos móveis -->
<input type="tel" name="telefone" placeholder="(82) 99999-9999" />
<!-- URL: valida formato de endereço web -->
<input type="url" name="website" placeholder="https://www.exemplo.com" />
<!-- Campo oculto: envia dados sem exibição ao usuário -->
<!-- Uso comum: tokens CSRF, IDs de sessão, dados de contexto -->
<input type="hidden" name="csrf_token" value="abc123xyz" />
5.5 — Elementos de formulário complementares¶
Vídeo curto explicativo (link será adicionado posteriormente)
5.5.1 — <textarea>: texto multilinha¶
O elemento <textarea> cria um campo de texto de múltiplas linhas, adequado para mensagens, descrições e comentários. Diferentemente de <input>, ele possui tag de abertura e fechamento, e seu valor padrão é definido pelo conteúdo entre as tags:
<label for="mensagem">Mensagem:</label>
<textarea
id="mensagem"
name="mensagem"
rows="5"
cols="50"
maxlength="500"
placeholder="Descreva sua dúvida com detalhes..."
></textarea>
Os atributos rows e cols definem o tamanho inicial em linhas e caracteres, respectivamente — mas o usuário pode redimensionar o campo em navegadores que o permitem. O controle preciso do tamanho deve ser feito via CSS (propriedade resize).
5.5.2 — <select>: lista de opções¶
O elemento <select> cria uma lista suspensa (dropdown) de opções. Cada opção é definida por um elemento <option>:
<label for="curso">Curso:</label>
<select id="curso" name="curso">
<!-- O primeiro option como placeholder -->
<option value="">Selecione um curso...</option>
<option value="si">Sistemas de Informação</option>
<option value="cc">Ciência da Computação</option>
<option value="ec">Engenharia da Computação</option>
</select>
Boa prática: sempre inclua um
<option value="">vazio como primeiro item, funcionando como placeholder. Isso evita que a primeira opção real seja selecionada por padrão sem que o usuário tenha feito uma escolha consciente.
O atributo value do <option> define o dado enviado ao servidor — que pode ser diferente do texto exibido ao usuário. Se value for omitido, o texto exibido é enviado como valor.
Agrupando opções com <optgroup>
Para listas longas, <optgroup> organiza as opções em grupos visuais rotulados:
<label for="linguagem">Linguagem de programação:</label>
<select id="linguagem" name="linguagem">
<option value="">Selecione...</option>
<optgroup label="Frontend">
<option value="html">HTML</option>
<option value="css">CSS</option>
<option value="js">JavaScript</option>
</optgroup>
<optgroup label="Backend">
<option value="python">Python</option>
<option value="nodejs">Node.js</option>
<option value="php">PHP</option>
</optgroup>
</select>
Seleção múltipla
O atributo multiple transforma o <select> em uma lista de seleção múltipla. O usuário deve usar Ctrl (ou Cmd) para selecionar mais de uma opção:
<label for="interesses">Áreas de interesse (selecione uma ou mais):</label>
<select id="interesses" name="interesses[]" multiple size="4">
<option value="web">Desenvolvimento Web</option>
<option value="dados">Ciência de Dados</option>
<option value="infra">Infraestrutura e DevOps</option>
<option value="seguranca">Segurança da Informação</option>
</select>
5.5.3 — <datalist>: sugestões automáticas¶
O elemento <datalist> oferece uma lista de sugestões para um campo de texto, sem restringir o usuário a apenas essas opções — ao contrário do <select>, que limita as escolhas disponíveis:
<label for="cidade">Cidade:</label>
<input
type="text"
id="cidade"
name="cidade"
list="sugestoes-cidade"
placeholder="Digite sua cidade..."
/>
<datalist id="sugestoes-cidade">
<option value="Maceió" />
<option value="Arapiraca" />
<option value="Palmeira dos Índios" />
<option value="União dos Palmares" />
<option value="Penedo" />
</datalist>
A associação entre o <input> e o <datalist> é feita pelo atributo list do input, cujo valor deve corresponder ao id do <datalist>.
5.6 — Formulários acessíveis¶
Vídeo curto explicativo (link será adicionado posteriormente)
Formulários são componentes de interface que exigem atenção especial à acessibilidade: o usuário precisa compreender o que cada campo solicita, receber feedback quando comete erros e ser capaz de navegar pelo formulário inteiramente via teclado. As fundações da acessibilidade em formulários são alcançadas com elementos e atributos HTML nativos — sem necessidade de ARIA na maioria dos casos.
5.6.1 — <label>: associando rótulos a campos¶
O elemento <label> é o mecanismo fundamental de acessibilidade em formulários. Ele associa um rótulo textual a um campo, tornando claro para o usuário — e para tecnologias assistivas — o que aquele campo solicita.
Existem duas formas de associar um <label> a um campo:
Associação explícita (recomendada): via atributo for no <label> referenciando o id do campo:
<label for="nome">Nome completo:</label>
<input type="text" id="nome" name="nome" />
Associação implícita: envolvendo o campo dentro do <label>:
<label>
Nome completo:
<input type="text" name="nome" />
</label>
Ambas as formas são válidas. A associação explícita é preferida quando o layout visual separa o rótulo do campo.
Por que <label> é indispensável:
- Leitores de tela anunciam o rótulo ao focar o campo — sem ele, o usuário ouve apenas "campo de texto" sem saber o que preencher
- Clicar no
<label>move o foco para o campo associado — ampliando a área clicável, o que beneficia usuários com dificuldades motoras - O WCAG 2.1 exige que todos os campos de formulário tenham um rótulo programaticamente associado (critério 1.3.1, nível A)
⚠️ Erro comum: usar
placeholdercomo substituto de<label>. Oplaceholderdesaparece quando o usuário começa a digitar, o que dificulta a revisão do formulário preenchido.placeholderdeve ser usado apenas como exemplo do formato esperado, nunca como rótulo principal do campo.
5.6.2 — <fieldset> e <legend>: agrupando campos relacionados¶
O elemento <fieldset> agrupa semanticamente campos relacionados de um formulário, e <legend> fornece um título para esse grupo. São especialmente importantes para grupos de radio buttons e checkboxes, onde o <legend> fornece o contexto da pergunta:
<form action="/matricula" method="post">
<fieldset>
<legend>Dados pessoais</legend>
<label for="nome">Nome completo:</label>
<input type="text" id="nome" name="nome" required />
<label for="cpf">CPF:</label>
<input type="text" id="cpf" name="cpf" pattern="\d{3}\.\d{3}\.\d{3}-\d{2}" />
</fieldset>
<fieldset>
<legend>Período de preferência:</legend>
<label>
<input type="radio" name="periodo" value="manha" /> Manhã
</label>
<label>
<input type="radio" name="periodo" value="tarde" /> Tarde
</label>
<label>
<input type="radio" name="periodo" value="noite" /> Noite
</label>
</fieldset>
<button type="submit">Enviar matrícula</button>
</form>
Quando um leitor de tela foca um radio button, ele anuncia: "Manhã, botão de opção, 1 de 3 — Período de preferência". O <legend> fornece o contexto "Período de preferência", sem o qual o usuário ouvia apenas "Manhã" sem saber a qual pergunta a opção pertence.
5.6.3 — ARIA em formulários: uso avançado¶
Os atributos ARIA são utilizados em formulários para situações que os elementos HTML nativos não cobrem diretamente — como mensagens de erro dinâmicas, descrições adicionais e estados de validação. O princípio fundamental é: use elementos HTML nativos primeiro; recorra ao ARIA apenas quando não há elemento semântico adequado.
Os atributos ARIA mais comuns em formulários:
<!-- aria-required: indica campo obrigatório (equivalente semântico ao required) -->
<input type="text" name="nome" aria-required="true" />
<!-- aria-describedby: associa uma descrição adicional ao campo -->
<label for="senha">Senha:</label>
<input
type="password"
id="senha"
name="senha"
aria-describedby="dica-senha"
/>
<p id="dica-senha">A senha deve ter no mínimo 8 caracteres, incluindo letras e números.</p>
<!-- aria-invalid: indica que o campo contém um valor inválido
(normalmente definido via JavaScript após validação) -->
<input
type="email"
name="email"
aria-invalid="true"
aria-describedby="erro-email"
/>
<p id="erro-email" role="alert">Por favor, informe um e-mail válido.</p>
Referência: MDN — Formulários HTML: Validação de dados de formulário
5.7 — Validação nativa do navegador¶
Vídeo curto explicativo (link será adicionado posteriormente)
O HTML5 introduziu um conjunto de atributos que habilitam a validação nativa do navegador — verificação dos dados diretamente no cliente, antes do envio ao servidor. Essa funcionalidade melhora a experiência do usuário ao fornecer feedback imediato, sem necessidade de JavaScript ou de uma viagem ao servidor.
⚠️ Princípio fundamental: a validação nativa do navegador é uma conveniência para o usuário — não é uma medida de segurança. Qualquer usuário com conhecimento técnico pode desabilitar a validação do navegador, modificar o HTML via DevTools ou enviar requisições HTTP diretamente sem usar o formulário. A validação definitiva deve sempre ser implementada no backend. Validações mais complexas no frontend (máscaras, verificação em tempo real, formatação automática) serão abordadas nos capítulos de JavaScript.
5.7.1 — O atributo required¶
Impede o envio do formulário quando o campo está vazio:
<label for="nome">Nome completo: *</label>
<input type="text" id="nome" name="nome" required />
5.7.2 — Restrições de comprimento: minlength e maxlength¶
<label for="bio">Biografia:</label>
<textarea
id="bio"
name="bio"
minlength="50"
maxlength="500"
placeholder="Escreva entre 50 e 500 caracteres..."
></textarea>
5.7.3 — Restrições numéricas: min, max e step¶
<label for="nota">Nota (0 a 10):</label>
<input
type="number"
id="nota"
name="nota"
min="0"
max="10"
step="0.5"
/>
5.7.4 — Validação por padrão: pattern¶
O atributo pattern aceita uma expressão regular que o valor do campo deve satisfazer. É a forma mais poderosa de validação nativa, permitindo definir formatos específicos:
<!-- CEP no formato 00000-000 -->
<label for="cep">CEP:</label>
<input
type="text"
id="cep"
name="cep"
pattern="\d{5}-\d{3}"
placeholder="00000-000"
title="CEP no formato 00000-000"
/>
<!-- Placa de veículo: formato antigo (ABC-1234) ou Mercosul (ABC1D23) -->
<label for="placa">Placa do veículo:</label>
<input
type="text"
id="placa"
name="placa"
pattern="[A-Z]{3}-?\d{4}|[A-Z]{3}\d[A-Z]\d{2}"
title="Placa no formato ABC-1234 ou ABC1D23"
/>
<!-- Senha: mínimo 8 caracteres, ao menos uma letra e um número -->
<label for="nova-senha">Nova senha:</label>
<input
type="password"
id="nova-senha"
name="nova_senha"
pattern="(?=.*[A-Za-z])(?=.*\d).{8,}"
title="Mínimo de 8 caracteres, incluindo ao menos uma letra e um número"
/>
O atributo title fornece a mensagem de erro exibida pelo navegador quando o padrão não é satisfeito — tornando o feedback compreensível para o usuário.
5.7.5 — Validação implícita por type¶
Muitos tipos de <input> realizam validação implícita sem atributos adicionais:
| Tipo | Validação implícita |
|---|---|
email |
Verifica presença de @ e formato básico |
url |
Verifica formato de URL (protocolo + domínio) |
number |
Aceita apenas valores numéricos |
date |
Aceita apenas datas válidas no formato do sistema |
tel |
Sem validação de formato (formatos variam por país) |
5.7.6 — Desabilitando a validação nativa¶
Em alguns cenários — como quando se implementa validação customizada via JavaScript — pode ser necessário desabilitar a validação nativa com o atributo booleano novalidate no elemento <form>:
<form action="/cadastro" method="post" novalidate>
<!-- Validação gerenciada inteiramente por JavaScript -->
</form>
5.8 — Formulário completo: exemplo integrado¶
A seguir, um formulário de contato que integra todos os conceitos abordados neste capítulo — estrutura semântica, acessibilidade, tipos de campos, validação nativa e boas práticas:
<form action="/contato" method="post" novalidate>
<fieldset>
<legend>Dados de identificação</legend>
<div class="campo">
<label for="nome">Nome completo: <span aria-hidden="true">*</span></label>
<input
type="text"
id="nome"
name="nome"
required
minlength="3"
maxlength="100"
autocomplete="name"
aria-required="true"
/>
</div>
<div class="campo">
<label for="email">E-mail: <span aria-hidden="true">*</span></label>
<input
type="email"
id="email"
name="email"
required
autocomplete="email"
aria-required="true"
aria-describedby="dica-email"
/>
<small id="dica-email">Utilizaremos este e-mail apenas para responder sua mensagem.</small>
</div>
<div class="campo">
<label for="telefone">Telefone:</label>
<input
type="tel"
id="telefone"
name="telefone"
pattern="\(\d{2}\)\s\d{4,5}-\d{4}"
placeholder="(82) 99999-9999"
title="Telefone no formato (DDD) NNNNN-NNNN"
autocomplete="tel"
/>
</div>
</fieldset>
<fieldset>
<legend>Assunto da mensagem</legend>
<div class="campo">
<label for="tipo">Tipo de solicitação: <span aria-hidden="true">*</span></label>
<select id="tipo" name="tipo" required aria-required="true">
<option value="">Selecione...</option>
<option value="duvida">Dúvida sobre o curso</option>
<option value="sugestao">Sugestão</option>
<option value="problema">Relato de problema</option>
<option value="outro">Outro assunto</option>
</select>
</div>
<div class="campo">
<label for="mensagem">Mensagem: <span aria-hidden="true">*</span></label>
<textarea
id="mensagem"
name="mensagem"
required
minlength="20"
maxlength="1000"
rows="6"
aria-required="true"
placeholder="Descreva sua solicitação com detalhes..."
></textarea>
</div>
</fieldset>
<fieldset>
<legend>Como prefere ser contatado?</legend>
<label>
<input type="radio" name="contato" value="email" checked />
Por e-mail
</label>
<label>
<input type="radio" name="contato" value="telefone" />
Por telefone
</label>
</fieldset>
<div class="campo">
<label>
<input type="checkbox" name="aceite" value="sim" required />
Li e aceito os <a href="/termos">termos de uso</a>
</label>
</div>
<!-- Campo oculto: dado de contexto enviado automaticamente -->
<input type="hidden" name="origem" value="pagina-contato" />
<div class="acoes">
<button type="reset">Limpar formulário</button>
<button type="submit">Enviar mensagem</button>
</div>
</form>
5.9 — Boas práticas em formulários HTML¶
Um formulário tecnicamente correto não é necessariamente um formulário bem projetado. As boas práticas a seguir reúnem os princípios de usabilidade, acessibilidade e integridade de dados mais relevantes para o desenvolvimento de formulários na Web:
Sempre declare name em campos que devem ser enviados. Campos sem name são silenciosamente ignorados pelo navegador — um dos erros mais difíceis de diagnosticar em formulários.
Sempre use <label> associado a cada campo. placeholder não é um substituto de <label> — ele desaparece quando o usuário começa a digitar e não é anunciado de forma consistente por leitores de tela.
Declare type explicitamente em todos os <button>. O padrão type="submit" de um <button> sem type causa envios acidentais de formulário com frequência.
Agrupe campos relacionados com <fieldset> e <legend>. Isso é especialmente importante para grupos de radio buttons e checkboxes, onde o <legend> fornece o contexto da pergunta.
Use o método HTTP correto. GET para buscas e filtros (os parâmetros na URL são desejáveis); POST para ações que modificam estado no servidor (cadastros, logins, envios de mensagem). Nunca use GET para dados sensíveis.
Informe o usuário sobre campos obrigatórios. A convenção * é amplamente reconhecida, mas deve ser acompanhada de uma nota como "* campos obrigatórios" no início ou no final do formulário.
Forneça mensagens de erro claras. O atributo title em conjunto com pattern melhora as mensagens de erro nativas do navegador. Mensagens genéricas como "Valor inválido" são inadequadas — informe o formato esperado.
Nunca confie apenas na validação do frontend. Toda validação HTML e JavaScript pode ser contornada. A validação definitiva ocorre no servidor.
Prefira autocomplete adequado. O atributo autocomplete com valores semânticos corretos (name, email, tel, current-password, etc.) melhora a experiência do usuário e é reconhecido por gerenciadores de senha.
Referências: - MDN — Guia de formulários HTML - WebAIM — Creating Accessible Forms - W3C — Web Accessibility Tutorials: Forms - WHATWG — The form element
Atividades — Capítulo 5¶
1. Um formulário tem um campo <input type="text" id="usuario"> sem o atributo name. O que ocorre quando o formulário é enviado?
2. Qual é a diferença fundamental entre type="checkbox" e type="radio"?
3. Por que a validação nativa do navegador (atributos required, pattern, etc.) não substitui a validação no servidor?
4. Um <button> sem o atributo type está dentro de um <form>. O que acontece quando ele é clicado?
- GitHub Classroom: Construir um formulário de matrícula com:
<fieldset>e<legend>para agrupamento, ao menos um campo de cada tipo (text,email,select,radio,checkbox,textarea), todos comname,ide<label>associado, validação nativa comrequiredepattern, e envio parahttps://httpbin.org/postpara inspeção da requisição. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 4 — Tabelas, Listas e Mídia :material-arrow-right: Ir ao Capítulo 6 — Introdução à Acessibilidade Web
Capítulo 6 — Introdução à Acessibilidade Web¶
Vídeo curto explicativo (link será adicionado posteriormente)
6.1 — O que é acessibilidade web e por que importa¶
Vídeo curto explicativo (link será adicionado posteriormente)
A acessibilidade web é a prática de projetar e desenvolver páginas e aplicações que possam ser percebidas, compreendidas, navegadas e utilizadas por todas as pessoas — independentemente de suas capacidades físicas, sensoriais, cognitivas ou tecnológicas. Isso inclui pessoas com deficiência visual, auditiva, motora ou cognitiva, mas também pessoas em situações temporárias (um braço imobilizado, um ambiente com muito ruído) ou contextuais (conexão lenta, tela pequena, luz solar intensa).
Uma forma precisa de compreender acessibilidade é pela definição do W3C (World Wide Web Consortium): a Web acessível é aquela em que pessoas com deficiência podem perceber, compreender, navegar, interagir e contribuir de forma equivalente às demais.
O termo equivalente é deliberado. Acessibilidade não significa oferecer uma experiência inferior ou simplificada para determinados grupos — significa garantir que a experiência seja funcionalmente equivalente para todos, ainda que os meios de acesso sejam diferentes.
6.1.1 — Quem se beneficia¶
É tentador imaginar acessibilidade como um conjunto de medidas destinadas a uma minoria específica. Essa percepção é equivocada por dois motivos.
Primeiro, o universo de pessoas beneficiadas é muito maior do que intuitivamente se imagina. Segundo dados da Organização Mundial da Saúde (OMS), aproximadamente 1,3 bilhão de pessoas — cerca de 16% da população mundial — vivem com alguma forma de deficiência significativa. No Brasil, o Censo IBGE 2022 identificou que mais de 18,6 milhões de pessoas declararam ter deficiência, o que representa aproximadamente 8,9% da população.
Segundo, muitos recursos desenvolvidos para acessibilidade beneficiam diretamente toda a população. Legendas em vídeos foram criadas para pessoas surdas, mas são amplamente utilizadas em ambientes barulhentos ou por pessoas aprendendo um idioma. O contraste elevado entre texto e fundo foi criado para pessoas com baixa visão, mas melhora a legibilidade para qualquer pessoa em condições de iluminação adversa. A navegação por teclado foi criada para pessoas com limitações motoras, mas é amplamente utilizada por desenvolvedores e usuários avançados. Este fenômeno é conhecido como curb-cut effect — uma analogia às rampas nas calçadas, criadas para cadeirantes mas utilizadas por ciclistas, pais com carrinhos de bebê e idosos.
6.1.2 — Contexto legal: a obrigação jurídica¶
No Brasil, a acessibilidade digital não é apenas uma boa prática — é uma obrigação legal para um conjunto crescente de organizações.
A Lei Brasileira de Inclusão da Pessoa com Deficiência (Lei nº 13.146/2015), conhecida como Estatuto da Pessoa com Deficiência, estabelece em seu artigo 63 que:
"É obrigatória a acessibilidade nos sítios da internet mantidos por empresas com sede ou representação comercial no País ou por órgãos de governo, para uso da pessoa com deficiência."
O Decreto nº 5.296/2004 e as normas da ABNT NBR 15599 e NBR 9050 regulamentam aspectos técnicos da acessibilidade em diferentes contextos, incluindo o digital.
Para órgãos públicos federais, o e-MAG (Modelo de Acessibilidade em Governo Eletrônico) define diretrizes específicas baseadas nas WCAG para portais governamentais brasileiros.
No cenário internacional, a Seção 508 da legislação norte-americana e a Diretiva de Acessibilidade Web da União Europeia (2016/2102) estabelecem obrigações similares em seus respectivos contextos.
Referências: - Lei nº 13.146/2015 — Estatuto da Pessoa com Deficiência - e-MAG — Modelo de Acessibilidade em Governo Eletrônico - OMS — Disability and health
6.2 — Os quatro princípios WCAG: o modelo POUR¶
Vídeo curto explicativo (link será adicionado posteriormente)
As WCAG (Web Content Accessibility Guidelines) são as diretrizes internacionais de acessibilidade para conteúdo web, publicadas e mantidas pelo W3C. A versão atual em uso amplo é a WCAG 2.1, publicada em 2018, com a WCAG 2.2 publicada em 2023 introduzindo refinamentos adicionais.
As WCAG organizam seus critérios em torno de quatro princípios fundamentais, conhecidos pelo acrônimo POUR:
6.2.1 — Perceptível (Perceivable)¶
O conteúdo deve ser apresentado de forma que todos os usuários possam percebê-lo — independentemente do sentido utilizado. Informações que existem apenas em uma modalidade sensorial (apenas visual ou apenas auditiva) excluem automaticamente usuários que não têm acesso a essa modalidade.
Exemplos práticos:
- Imagens informativas devem ter texto alternativo (
alt) descritivo — pessoas cegas usam leitores de tela que convertem oaltem áudio ou Braille - Vídeos devem ter legendas — pessoas surdas dependem do texto para acessar o conteúdo auditivo
- O conteúdo não deve depender exclusivamente de cor para transmitir informação — pessoas com daltonismo podem não distinguir vermelho de verde
- O contraste entre texto e fundo deve ser suficiente — a WCAG 2.1 nível AA exige razão de contraste mínima de 4,5:1 para texto normal e 3:1 para texto grande
<!-- Perceptível: imagem informativa com alt descritivo -->
<img
src="grafico-vendas-2026.png"
alt="Gráfico de barras mostrando crescimento de 40% nas vendas
do 1º semestre de 2026 em relação ao mesmo período de 2025"
/>
<!-- Perceptível: não dependendo apenas de cor para transmitir erro -->
<!-- INCORRETO: apenas vermelho indica o erro -->
<input type="text" style="border: 2px solid red;" />
<!-- CORRETO: cor + ícone + texto comunicam o erro -->
<input type="text" aria-invalid="true" aria-describedby="erro-campo" />
<p id="erro-campo">⚠ Campo obrigatório — por favor, preencha este campo.</p>
6.2.2 — Operável (Operable)¶
A interface deve ser operável por todos os usuários, independentemente do dispositivo de entrada utilizado. Pessoas com limitações motoras podem não usar mouse — elas navegam com teclado, switches, rastreadores oculares ou voz.
Exemplos práticos:
- Toda funcionalidade deve ser acessível via teclado — navegação por
Tab, ativação porEnter/Espaço, fechamento de modais porEsc - O foco do teclado deve ser sempre visível — nunca remover o outline do foco via CSS sem fornecer uma alternativa visual
- Links e botões devem ter área clicável adequada — a WCAG 2.2 recomenda área mínima de 24×24 pixels
- Conteúdos com movimento ou animação automática devem ter mecanismo de pausa
<!-- Operável: botão com foco visível e navegável por teclado -->
<button type="button" class="btn-primario">
Salvar alterações
</button>
<!-- CSS: nunca faça isso -->
/* INCORRETO: remove completamente o indicador de foco */
button:focus { outline: none; }
/* CORRETO: substitui pelo indicador customizado com contraste adequado */
button:focus-visible {
outline: 3px solid #0057B8;
outline-offset: 2px;
}
6.2.3 — Compreensível (Understandable)¶
O conteúdo e a operação da interface devem ser compreensíveis — tanto o texto quanto o comportamento da página devem ser previsíveis e inteligíveis.
Exemplos práticos:
- O idioma da página deve ser declarado no atributo
langdo<html>— leitores de tela utilizam essa informação para selecionar o sintetizador de voz correto - Mudanças de idioma dentro do documento devem ser marcadas com
langno elemento correspondente - Mensagens de erro em formulários devem identificar o campo problemático e sugerir como corrigi-lo
- A navegação deve ser consistente entre páginas — elementos repetidos (menu, rodapé) devem aparecer sempre na mesma posição relativa
<!-- Compreensível: idioma declarado -->
<html lang="pt-BR">
<!-- Compreensível: trecho em outro idioma -->
<p>O princípio <span lang="la">ad hoc</span> é amplamente aplicado.</p>
<!-- Compreensível: mensagem de erro descritiva -->
<label for="cep">CEP:</label>
<input
type="text"
id="cep"
name="cep"
aria-describedby="cep-erro"
aria-invalid="true"
/>
<p id="cep-erro" role="alert">
CEP inválido. Informe o CEP no formato 00000-000.
</p>
6.2.4 — Robusto (Robust)¶
O conteúdo deve ser robusto o suficiente para ser interpretado de forma confiável por uma ampla variedade de agentes de usuário — navegadores atuais, navegadores antigos, leitores de tela, motores de indexação e tecnologias assistivas em geral.
Na prática, robustez significa essencialmente duas coisas: usar HTML semântico correto (conforme estudado nos capítulos anteriores) e garantir que o código seja válido e bem formado. Tecnologias assistivas dependem da estrutura semântica do documento para construir sua representação da página — um documento com erros estruturais produz comportamentos imprevisíveis.
Exemplos práticos:
- Usar elementos HTML semânticos nativos em vez de recriar comportamentos com
<div>e ARIA - Manter atributos ARIA consistentes com o estado real do componente
- Validar o documento regularmente com o W3C Validator
<!-- Robusto: botão nativo — comportamento previsível em todos os contextos -->
<button type="button">Abrir menu</button>
<!-- Frágil: div tentando se comportar como botão —
requer ARIA, JavaScript e tratamento de teclado manual -->
<div role="button" tabindex="0">Abrir menu</div>
A regra fundamental de robustez pode ser resumida assim: a primeira regra de ARIA é não usar ARIA. Se existe um elemento HTML nativo que expressa a semântica necessária, use-o. ARIA é um complemento para situações que o HTML nativo não cobre — não um substituto para HTML semântico.
Referências: - W3C — WCAG 2.1 - W3C — WCAG 2.2 - MDN — Accessibility
6.3 — ARIA básico¶
Vídeo curto explicativo (link será adicionado posteriormente)
ARIA (Accessible Rich Internet Applications) é uma especificação do W3C que define um conjunto de atributos HTML capazes de complementar a semântica nativa dos elementos — comunicando funções, estados e propriedades de componentes de interface a tecnologias assistivas quando o HTML sozinho não é suficiente.
É importante compreender o escopo correto do ARIA: ele não adiciona funcionalidade, não muda a aparência visual e não afeta o comportamento do elemento para usuários sem tecnologia assistiva. ARIA comunica significado para a camada de acessibilidade do navegador — a Accessibility Tree — que por sua vez informa os leitores de tela e outros agentes.
6.3.1 — Quando usar (e quando não usar) ARIA¶
A especificação WAI-ARIA do W3C estabelece cinco regras de uso, das quais a mais importante é a primeira:
Primeira regra do ARIA: se você pode usar um elemento HTML nativo ou atributo com a semântica e o comportamento que você precisa, use-o em vez de redefinir um elemento e adicionar ARIA.
Isso significa que <button> é sempre preferível a <div role="button">, que <nav> é sempre preferível a <div role="navigation">, e que <h2> é sempre preferível a <div role="heading" aria-level="2">.
ARIA é necessário em situações genuínas: componentes de interface personalizados sem equivalente HTML nativo (como abas, accordions, sliders customizados), estados dinâmicos que mudam via JavaScript, e regiões de conteúdo que precisam de rótulos adicionais.
6.3.2 — Os três pilares do ARIA: roles, states e properties¶
Roles (funções)
O atributo role define a função semântica de um elemento — o que ele é na interface:
<!-- Quando um elemento não-semântico precisa comunicar sua função -->
<div role="alert">
Sua sessão expirará em 5 minutos.
</div>
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="painel-1">Aba 1</button>
<button role="tab" aria-selected="false" aria-controls="painel-2">Aba 2</button>
</div>
<div role="tabpanel" id="painel-1">Conteúdo da Aba 1</div>
States (estados)
Atributos ARIA de estado comunicam a condição atual de um elemento — informações que mudam dinamicamente conforme o usuário interage:
<!-- aria-expanded: indica se um elemento colapsável está aberto ou fechado -->
<button aria-expanded="false" aria-controls="menu-dropdown">
Menu
</button>
<ul id="menu-dropdown" hidden>
<li><a href="/inicio">Início</a></li>
<li><a href="/sobre">Sobre</a></li>
</ul>
<!-- aria-selected: indica item selecionado em uma lista -->
<li role="option" aria-selected="true">JavaScript</li>
<!-- aria-checked: estado de checkbox customizado -->
<div role="checkbox" aria-checked="false" tabindex="0">
Aceitar termos
</div>
Properties (propriedades)
Atributos ARIA de propriedade fornecem informações descritivas sobre um elemento — geralmente estáticas ou raramente alteradas:
<!-- aria-label: rótulo para elementos sem texto visível -->
<button aria-label="Fechar modal" type="button">✕</button>
<!-- aria-labelledby: referencia outro elemento como rótulo -->
<h2 id="titulo-secao">Configurações de conta</h2>
<section aria-labelledby="titulo-secao">
...
</section>
<!-- aria-describedby: associa descrição adicional a um elemento -->
<input
type="password"
aria-describedby="requisitos-senha"
/>
<p id="requisitos-senha">
Mínimo de 8 caracteres, incluindo letras maiúsculas, minúsculas e números.
</p>
<!-- aria-hidden: oculta elemento da árvore de acessibilidade -->
<!-- Útil para ícones decorativos que não devem ser anunciados -->
<button type="button">
<svg aria-hidden="true" focusable="false">...</svg>
Salvar documento
</button>
<!-- aria-live: anuncia atualizações dinâmicas de conteúdo -->
<div aria-live="polite" aria-atomic="true">
<!-- Conteúdo atualizado via JavaScript será anunciado pelo leitor de tela -->
3 resultados encontrados
</div>
6.3.3 — Atributos ARIA mais utilizados na prática¶
| Atributo | Tipo | Uso principal |
|---|---|---|
role |
role | Define a função semântica do elemento |
aria-label |
property | Rótulo para elementos sem texto visível |
aria-labelledby |
property | Referencia outro elemento como rótulo |
aria-describedby |
property | Associa descrição adicional |
aria-hidden |
state | Oculta da árvore de acessibilidade |
aria-expanded |
state | Estado de expansão (aberto/fechado) |
aria-selected |
state | Item selecionado em lista ou abas |
aria-checked |
state | Estado de marcação |
aria-disabled |
state | Elemento desabilitado |
aria-required |
property | Campo obrigatório em formulários |
aria-invalid |
state | Campo com valor inválido |
aria-live |
property | Anuncia atualizações dinâmicas |
aria-current |
state | Item atual (página, passo, localização) |
Referências: - W3C — WAI-ARIA 1.2 - MDN — ARIA - W3C — ARIA Authoring Practices Guide
6.4 — Boas práticas imediatas¶
Vídeo curto explicativo (link será adicionado posteriormente)
Acessibilidade pode parecer um tema vasto e complexo, mas a maior parte dos problemas mais frequentes é resolvida por um conjunto relativamente pequeno de boas práticas que qualquer desenvolvedor pode aplicar imediatamente — sem conhecimento avançado de ARIA ou tecnologias assistivas.
6.4.1 — Hierarquia de títulos¶
A hierarquia de títulos (<h1>–<h6>) é o principal mecanismo de navegação de usuários de leitores de tela. Segundo pesquisas da WebAIM, 67,7% dos usuários de leitores de tela navegam páginas web primeiramente pela lista de títulos.
A regra é direta: os títulos devem refletir a estrutura lógica do documento, sem saltar níveis. Uma página deve ter exatamente um <h1>, e os demais títulos devem seguir uma hierarquia coerente.
<!-- INCORRETO: salto de h1 para h3 -->
<h1>Programação Web 1</h1>
<h3>Capítulo 2</h3> <!-- deveria ser h2 -->
<h4>Introdução</h4>
<!-- CORRETO: hierarquia sem saltos -->
<h1>Programação Web 1</h1>
<h2>Capítulo 2 — Fundamentos do HTML</h2>
<h3>2.1 — Introdução ao HTML</h3>
<h3>2.2 — Tags Essenciais</h3>
<h2>Capítulo 3 — HTML Semântico</h2>
6.4.2 — Textos alternativos para imagens¶
O atributo alt é obrigatório em todos os elementos <img>. A qualidade do texto alternativo, porém, varia enormemente na prática. Algumas orientações:
- Imagens informativas: descreva o conteúdo e a função da imagem — não apenas o que ela mostra, mas o que ela comunica no contexto
- Imagens decorativas: use
alt=""(vazio) — o leitor de tela ignorará a imagem - Imagens de texto: transcreva o texto exato contido na imagem
- Gráficos e infográficos: descreva a conclusão ou tendência principal, não os dados brutos
<!-- Informativa: descreve conteúdo e contexto -->
<img
src="grafico-crescimento.png"
alt="Gráfico de linha mostrando crescimento de 127% no número
de matrículas em cursos de TI entre 2020 e 2025"
/>
<!-- Decorativa: alt vazio -->
<img src="divider-ornamental.svg" alt="" />
<!-- Imagem de texto: transcrição exata -->
<img
src="citacao-berners-lee.jpg"
alt="Citação de Tim Berners-Lee: 'The Web is more a social
creation than a technical one.'"
/>
6.4.3 — Contraste de cores¶
O contraste insuficiente entre texto e fundo é um dos problemas de acessibilidade mais comuns e mais fáceis de verificar. As WCAG 2.1 definem os seguintes requisitos mínimos de contraste:
| Nível | Texto normal (< 18pt) | Texto grande (≥ 18pt ou ≥ 14pt negrito) |
|---|---|---|
| AA (mínimo) | 4,5:1 | 3:1 |
| AAA (aprimorado) | 7:1 | 4,5:1 |
Ferramentas para verificar contraste:
- WebAIM Contrast Checker: https://webaim.org/resources/contrastchecker/
- Colour Contrast Analyser (aplicativo desktop gratuito)
- DevTools: aba Elements → inspecionar propriedade color → o Chrome exibe a razão de contraste automaticamente
Imagem sugerida: captura do Chrome DevTools mostrando o painel de cor com a razão de contraste exibida ao inspecionar um elemento de texto — destacando a diferença entre uma razão abaixo de 4,5:1 (reprovada) e uma acima (aprovada).
(imagem será adicionada posteriormente)
6.4.4 — Navegação por teclado¶
Toda a funcionalidade de uma página deve ser acessível sem o uso do mouse. O fluxo de navegação por teclado segue esta sequência:
Tab— avança para o próximo elemento focávelShift+Tab— retorna ao elemento focável anteriorEnter— ativa links e botõesEspaço— ativa botões e checkboxesSetas— navega entre itens de radio buttons e menus
Para garantir navegação por teclado adequada:
<!-- Elementos interativos nativos são focáveis automaticamente -->
<a href="/pagina">Link</a>
<button>Botão</button>
<input type="text" />
<select>...</select>
<!-- tabindex="0": torna qualquer elemento focável na ordem natural -->
<div tabindex="0" role="button">Elemento customizado focável</div>
<!-- tabindex="-1": torna elemento programaticamente focável
(via JavaScript), mas não na navegação por Tab -->
<div tabindex="-1" id="conteudo-modal">...</div>
<!-- NUNCA use tabindex positivo (tabindex="1", "2", etc.)
— cria uma ordem de foco artificial e confusa -->
Nunca remova o indicador de foco sem fornecer uma alternativa:
/* INCORRETO: remove completamente o foco visível */
* { outline: none; }
/* CORRETO: customiza o foco com contraste adequado */
:focus-visible {
outline: 3px solid #0057B8;
outline-offset: 3px;
border-radius: 2px;
}
6.4.5 — Links descritivos¶
O texto de um link deve fazer sentido fora de contexto — pois usuários de leitores de tela frequentemente navegam pela lista de links da página sem ler o texto ao redor.
<!-- INCORRETO: texto não descritivo -->
<a href="/relatorio-2026.pdf">Clique aqui</a>
<a href="/sobre">Saiba mais</a>
<!-- CORRETO: texto descritivo -->
<a href="/relatorio-2026.pdf">Relatório anual 2026 (PDF, 2,4 MB)</a>
<a href="/sobre">Saiba mais sobre o IFAL Arapiraca</a>
<!-- Quando o texto visível for inevitavelmente curto,
use aria-label para complementar -->
<a href="/relatorio-2026.pdf" aria-label="Baixar relatório anual 2026 em PDF">
Baixar
</a>
6.4.6 — Formulários acessíveis: revisão¶
Os princípios de acessibilidade em formulários foram abordados em profundidade no Capítulo 5. Como revisão, os pontos essenciais são:
- Todo campo deve ter um
<label>associado programaticamente — nunca depender apenas deplaceholder - Grupos de campos relacionados devem ser envolvidos por
<fieldset>com<legend> - Mensagens de erro devem ser associadas ao campo via
aria-describedbye anunciadas dinamicamente viarole="alert"ouaria-live - O
<button>dentro de formulários deve sempre tertypedeclarado explicitamente
6.5 — Ferramentas e próximos passos¶
Vídeo curto explicativo (link será adicionado posteriormente)
6.5.1 — Ferramentas de diagnóstico¶
As ferramentas de diagnóstico de acessibilidade foram apresentadas no Capítulo 3 (seção 3.5). Como referência consolidada para este capítulo:
| Ferramenta | Tipo | O que verifica | Acesso |
|---|---|---|---|
| W3C Validator | Online | Validade sintática do HTML | validator.w3.org |
| WAVE | Online / extensão | Acessibilidade geral (erros, alertas, estrutura) | wave.webaim.org |
| Lighthouse | DevTools | Pontuação de acessibilidade + lista priorizada | F12 → Lighthouse |
| Accessibility Tree | DevTools | Árvore de acessibilidade, roles, nomes | F12 → Elements → Accessibility |
| Contrast Checker | Online | Razão de contraste entre texto e fundo | webaim.org/resources/contrastchecker |
| NVDA | Leitor de tela | Teste real com tecnologia assistiva | nvaccess.org |
No DevTools: para inspecionar a árvore de acessibilidade completa de uma página, abra o DevTools (
F12), vá até a aba Elements, selecione qualquer elemento e expanda o painel Accessibility no lado direito. Ative a opção "Enable full-page accessibility tree" para visualizar toda a hierarquia de elementos acessíveis da página em formato de árvore, paralela à árvore DOM.
6.5.2 — O fluxo de diagnóstico recomendado¶
Para os projetos desta disciplina, o fluxo de diagnóstico recomendado é o apresentado no Capítulo 3, seção 3.5.4 — resumido aqui como referência:
- W3C Validator — garantir ausência de erros sintáticos
- DevTools → Accessibility Tree — verificar roles e nomes acessíveis dos elementos principais
- WAVE — auditoria completa de acessibilidade da página
- Lighthouse — pontuação geral e lista priorizada de problemas
6.5.3 — Acessibilidade no contexto profissional de SI¶
Para estudantes de Sistemas de Informação, acessibilidade não é apenas uma questão de front-end. Ela atravessa múltiplas dimensões do desenvolvimento de software:
No backend: APIs que retornam mensagens de erro claras e estruturadas permitem que o frontend construa feedback acessível para o usuário. A estrutura dos dados influencia como o frontend pode apresentá-los de forma acessível.
No banco de dados: textos alternativos, transcrições e metadados de acessibilidade frequentemente precisam ser armazenados e gerenciados como dados de primeira classe.
Em sistemas de informação: relatórios, dashboards e interfaces administrativas — os produtos mais comuns de projetos de SI — são frequentemente acessados por uma ampla variedade de usuários, incluindo pessoas com deficiência visual que usam leitores de tela para navegar por tabelas de dados.
Na gestão de projetos: requisitos de acessibilidade devem ser incluídos desde a fase de levantamento de requisitos, não tratados como ajustes finais. Incorporar acessibilidade tardiamente é significativamente mais custoso do que projetá-la desde o início.
6.5.4 — Para aprofundamento¶
Este capítulo oferece uma visão panorâmica da acessibilidade web. Para aprofundamento progressivo, recomenda-se a seguinte sequência de recursos:
Nível introdutório: - WebAIM — Introduction to Web Accessibility — leitura inicial recomendada - Google — Web Accessibility (curso gratuito no Udacity)
Nível intermediário: - W3C — WAI Tutorials — tutoriais práticos por componente (menus, formulários, tabelas, imagens) - MDN — Accessibility
Referência técnica: - WCAG 2.1 — Especificação completa - WCAG 2.2 — Especificação completa - ARIA Authoring Practices Guide — padrões de implementação de componentes interativos acessíveis
Atividades — Capítulo 6¶
1. O que significa dizer que a acessibilidade web visa uma experiência "equivalente" para todos os usuários?
2. Qual dos quatro princípios WCAG (POUR) está sendo violado quando uma mensagem de erro é comunicada apenas pela cor vermelha de um campo, sem texto ou ícone adicional?
3. Qual é a "primeira regra do ARIA"?
4. Por que nunca se deve remover o outline de foco via CSS sem fornecer uma alternativa visual?
- GitHub Classroom: Auditar a página construída nas atividades anteriores utilizando WAVE e Lighthouse, documentar os problemas encontrados em um arquivo
acessibilidade.mdcom: lista de erros identificados, descrição do impacto de cada erro para o usuário afetado, e as correções aplicadas no HTML. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 5 — Formulários HTML :material-arrow-right: Ir ao Capítulo 7 — Fundamentos do CSS
Capítulo 7 — Fundamentos do CSS¶
Vídeo curto explicativo (link será adicionado posteriormente)
7.1 — O papel do CSS na Web¶
Vídeo curto explicativo (link será adicionado posteriormente)
O CSS (Cascading Style Sheets — Folhas de Estilo em Cascata) é a linguagem responsável pela apresentação visual de documentos HTML na Web. Enquanto o HTML define a estrutura e o significado do conteúdo — o que cada elemento é —, o CSS define como esse conteúdo aparece: cores, tipografia, espaçamento, dimensões, posicionamento, animações e a disposição geral dos elementos na tela.
A separação entre HTML e CSS não é apenas uma convenção de organização: é um princípio arquitetural fundamental da Web. Antes do CSS, atributos de apresentação eram declarados diretamente no HTML — elementos como <font>, <center> e <bgcolor> misturavam estrutura e aparência de forma indissociável, tornando os documentos difíceis de manter e impossíveis de reutilizar visualmente. O CSS resolveu esse problema ao centralizar todas as decisões visuais em uma camada separada, permitindo que o mesmo HTML seja apresentado de formas radicalmente diferentes para diferentes contextos — tela, impressão, leitor de tela, dispositivo móvel.
7.1.1 — Separação de responsabilidades¶
A arquitetura de três camadas da Web moderna distribui responsabilidades de forma precisa:
- HTML — estrutura e significado: o que o conteúdo é
- CSS — apresentação visual: como o conteúdo aparece
- JavaScript — comportamento interativo: como o conteúdo age
Esta separação tem consequências práticas diretas. Um documento HTML semanticamente correto pode ser estilizado de formas completamente diferentes sem qualquer alteração na marcação — basta substituir ou modificar a folha de estilos. O projeto CSS Zen Garden (csszengarden.com) demonstra isso de forma notável: o mesmo documento HTML é apresentado com dezenas de designs radicalmente diferentes, cada um criado apenas com CSS diferente.
Para desenvolvedores de sistemas de informação, essa separação tem implicações além do front-end: sistemas que geram HTML programaticamente (relatórios, dashboards, e-mails transacionais) se beneficiam diretamente de uma marcação limpa separada da apresentação.
7.1.2 — O modelo de renderização do navegador¶
Compreender como o navegador processa HTML e CSS é essencial para diagnosticar comportamentos inesperados e otimizar o desempenho de páginas.
Quando o navegador recebe um documento HTML, ele executa o seguinte pipeline de renderização:
1. HTML parsing → DOM Tree
O navegador lê o HTML e constrói a árvore DOM
2. CSS parsing → CSSOM Tree
O navegador lê o CSS e constrói a árvore CSSOM
(CSS Object Model)
3. Render Tree
DOM + CSSOM são combinados em uma árvore de renderização
(apenas elementos visíveis)
4. Layout (Reflow)
O navegador calcula posição e dimensões de cada elemento
5. Paint
O navegador pinta os pixels na tela
6. Compositing
Camadas são combinadas na imagem final exibida ao usuário
Este pipeline tem implicações práticas importantes. CSS que bloqueia a renderização (arquivos externos no <head>) atrasa a exibição da página. Propriedades CSS que afetam o layout (como width, margin, display) são mais custosas do que propriedades que afetam apenas a pintura (como color, background-color). Propriedades que afetam apenas a composição (como transform e opacity) são as mais performáticas para animações.
No DevTools: a aba Performance do Chrome DevTools permite gravar e inspecionar o pipeline de renderização quadro a quadro, identificando gargalos de layout e pintura. Para inspeção estática, a aba Elements exibe os estilos computados de qualquer elemento selecionado no painel Computed, mostrando o valor final de cada propriedade CSS após cascata, herança e especificidade.
7.2 — Inclusão de CSS no documento¶
Vídeo curto explicativo (link será adicionado posteriormente)
Existem três formas de associar CSS a um documento HTML, cada uma com características e casos de uso distintos.
7.2.1 — CSS externo (recomendado)¶
A forma preferencial e mais comum: um arquivo .css separado, vinculado ao HTML via elemento <link> no <head>:
<head>
<meta charset="UTF-8" />
<title>Meu Site</title>
<link rel="stylesheet" href="css/style.css" />
</head>
Vantagens:
- O mesmo arquivo CSS pode ser compartilhado por múltiplas páginas HTML
- O navegador armazena o arquivo em cache após o primeiro carregamento — páginas subsequentes carregam mais rápido
- Clara separação entre estrutura (HTML) e apresentação (CSS)
- Facilita manutenção: alterar o visual do site inteiro requer modificar apenas o arquivo CSS
Ordem de carregamento: múltiplos arquivos CSS são aplicados na ordem em que são declarados. Declarações no segundo arquivo sobrescrevem as do primeiro quando há conflito (respeitando especificidade):
<!-- reset.css é aplicado primeiro; style.css pode sobrescrever -->
<link rel="stylesheet" href="css/reset.css" />
<link rel="stylesheet" href="css/style.css" />
7.2.2 — CSS interno¶
Declarado dentro de um elemento <style> no <head> do documento:
<head>
<style>
body {
font-family: 'Trebuchet MS', sans-serif;
background-color: #f5f5f5;
}
h1 {
color: #12243A;
}
</style>
</head>
Quando usar: prototipação rápida, e-mails HTML (que não suportam CSS externo de forma confiável), páginas únicas que não compartilham estilos com outras páginas.
Desvantagem: os estilos não são cacheados separadamente e não são reutilizáveis entre páginas.
7.2.3 — CSS inline¶
Declarado diretamente no atributo style de um elemento HTML:
<p style="color: #E8632A; font-weight: bold; margin-top: 1rem;">
Texto com estilo inline.
</p>
Quando usar: ajustes pontuais e específicos que não se repetem, geração programática de estilos via JavaScript, situações onde o CSS externo não está disponível.
Desvantagens: viola a separação de responsabilidades, não é reutilizável, tem a maior especificidade possível (sobrescreve praticamente qualquer regra CSS externa), e torna o HTML difícil de ler e manter.
Regra prática: CSS externo para projetos reais, CSS interno para prototipação, CSS inline somente como último recurso ou quando gerado programaticamente.
7.3 — Sintaxe e estrutura do CSS¶
Vídeo curto explicativo (link será adicionado posteriormente)
7.3.1 — Regras, declarações e valores¶
A unidade fundamental do CSS é a regra (rule), composta por um seletor e um bloco de declarações:
/* Anatomia de uma regra CSS */
seletor {
propriedade: valor;
propriedade: valor;
}
/* Exemplo concreto */
h1 {
color: #12243A;
font-size: 2rem;
margin-bottom: 1rem;
}
Cada linha dentro do bloco é uma declaração, composta por uma propriedade e um valor, separados por dois-pontos e terminados com ponto e vírgula. O ponto e vírgula na última declaração do bloco é tecnicamente opcional, mas é uma boa prática incluí-lo para evitar erros ao adicionar novas declarações.
Múltiplos seletores para a mesma regra são separados por vírgula:
/* Aplicar o mesmo estilo a h1, h2 e h3 */
h1,
h2,
h3 {
font-family: Georgia, serif;
color: #12243A;
}
7.3.2 — Comentários¶
Comentários em CSS são delimitados por /* e */ e podem ocupar uma ou múltiplas linhas:
/* Comentário de uma linha */
/*
Comentário de
múltiplas linhas
*/
/* ─── Seção: Tipografia ──────────────────────────── */
body {
font-family: 'Trebuchet MS', sans-serif; /* Fonte principal */
font-size: 16px;
}
Comentários são removidos pelo navegador antes do processamento e não afetam o comportamento do CSS.
7.4 — Unidades de medida¶
Vídeo curto explicativo (link será adicionado posteriormente)
A escolha da unidade de medida em CSS tem impacto direto na responsividade, na acessibilidade e na manutenibilidade do código. Compreender as diferenças entre unidades absolutas, relativas e de viewport é fundamental para construir layouts que se adaptam corretamente a diferentes contextos.
7.4.1 — Unidades absolutas¶
Unidades absolutas possuem tamanho fixo, independente do contexto. Na Web, px (pixel) é a única unidade absoluta de uso prático:
/* px: pixel CSS — unidade absoluta mais comum na web */
.elemento {
width: 300px;
height: 200px;
border: 2px solid #ccc;
font-size: 16px;
}
Quando usar px: bordas, sombras, valores que não devem escalar com o texto do usuário (como espessuras de linha), e como valor base para cálculos. Evitar para font-size de elementos de texto — impede que a página respeite as preferências de tamanho de fonte do usuário no navegador.
As demais unidades absolutas (cm, mm, in, pt, pc) são raramente usadas na Web — seu uso prático se restringe a folhas de estilo para impressão.
7.4.2 — Unidades relativas¶
Unidades relativas calculam seu valor em relação a outro valor de referência — tornando o layout mais flexível e adaptável.
em — relativo ao font-size do elemento pai
.container {
font-size: 16px;
}
.container p {
font-size: 1em; /* 16px (igual ao pai) */
margin-bottom: 1.5em; /* 24px (1.5 × 16px) */
padding: 0.75em; /* 12px (0.75 × 16px) */
}
.container h2 {
font-size: 2em; /* 32px (2 × 16px) */
}
Atenção ao aninhamento: em se multiplica em elementos aninhados, o que pode produzir resultados inesperados:
/* Se o body tem font-size: 16px */
.pai { font-size: 1.5em; } /* 24px */
.filho { font-size: 1.5em; } /* 36px (1.5 × 24px, não 1.5 × 16px) */
rem — relativo ao font-size do elemento raiz (<html>)
rem resolve o problema de multiplicação do em ao sempre referenciar o elemento <html>, independentemente do aninhamento:
html {
font-size: 16px; /* Base: 1rem = 16px em todo o documento */
}
h1 { font-size: 2rem; } /* 32px — sempre, independente do contexto */
h2 { font-size: 1.5rem; } /* 24px */
p { font-size: 1rem; } /* 16px */
.pequeno { font-size: 0.875rem; } /* 14px */
Boa prática: use
remparafont-sizee espaçamentos globais (margens, paddings entre seções). Useempara valores que devem escalar proporcionalmente ao tamanho de fonte local (padding interno de botões, por exemplo). Evitepxparafont-size— ele impede que o usuário ajuste o tamanho do texto nas preferências do navegador, uma falha de acessibilidade.
% — relativo ao elemento pai
.container {
width: 800px;
}
.coluna {
width: 50%; /* 400px — metade do container */
padding: 2%; /* 16px — 2% de 800px */
}
Para width e height, % é calculado em relação à dimensão correspondente do elemento pai. Para padding e margin, % é sempre calculado em relação à largura do elemento pai (mesmo para padding vertical).
7.4.3 — Unidades de viewport¶
Unidades de viewport são relativas às dimensões da janela visível do navegador (viewport):
| Unidade | Referência | Descrição |
|---|---|---|
vw |
Largura do viewport | 1vw = 1% da largura da janela |
vh |
Altura do viewport | 1vh = 1% da altura da janela |
vmin |
Menor dimensão do viewport | 1vmin = 1% do menor entre vw e vh |
vmax |
Maior dimensão do viewport | 1vmax = 1% do maior entre vw e vh |
svh |
Small viewport height | Altura sem barras de navegação do browser mobile |
dvh |
Dynamic viewport height | Ajusta dinamicamente conforme barras aparecem/desaparecem |
/* Seção que ocupa toda a altura da janela */
.hero {
height: 100vh;
width: 100%;
}
/* Tipografia que escala com a largura da janela */
.titulo-fluido {
font-size: 5vw; /* Em viewport de 1200px → 60px; em 600px → 30px */
}
/* Overlay que cobre toda a tela */
.modal-overlay {
position: fixed;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
}
Nota sobre
vhem mobile: em navegadores móveis,100vhinclui a área coberta pelas barras de navegação do browser, produzindo scroll indesejado. As unidadessvhedvh, introduzidas mais recentemente, resolvem esse problema. Para projetos que precisam suportar navegadores mais antigos, soluções via JavaScript ainda são necessárias.
7.4.4 — A função clamp() — tipografia e layout fluidos¶
A função clamp(mínimo, preferido, máximo) permite definir valores que escalam fluentemente entre um mínimo e um máximo, sem necessidade de media queries:
/* font-size entre 16px e 24px, escalando com a largura do viewport */
p {
font-size: clamp(1rem, 2.5vw, 1.5rem);
}
/* Largura entre 300px e 800px */
.card {
width: clamp(300px, 50%, 800px);
}
/* Espaçamento fluido */
.secao {
padding: clamp(1.5rem, 5vw, 4rem);
}
clamp() é especialmente poderoso para tipografia fluida — o texto escala suavemente com o viewport, sem os saltos abruptos das media queries. Será aprofundado no Capítulo 10 (Design Responsivo).
7.5 — Seletores CSS¶
Vídeo curto explicativo (link será adicionado posteriormente)
Os seletores são o mecanismo pelo qual o CSS identifica quais elementos HTML devem receber determinados estilos. Compreender a hierarquia e o poder expressivo dos seletores é fundamental para escrever CSS eficiente, manutenível e previsível.
7.5.1 — Seletores básicos¶
Seletor de tipo (elemento)
Seleciona todos os elementos de um determinado tipo HTML:
/* Todos os parágrafos */
p {
line-height: 1.6;
color: #333;
}
/* Todos os links */
a {
color: #0057B8;
text-decoration: underline;
}
Seletor de classe
Seleciona elementos com uma determinada classe. É o seletor mais utilizado no desenvolvimento web moderno, por oferecer reutilização sem o alto peso de especificidade do seletor de ID:
/* Todos os elementos com class="destaque" */
.destaque {
background-color: #FFF3CD;
border-left: 4px solid #E8632A;
padding: 1rem;
}
/* Um elemento pode ter múltiplas classes */
/* <p class="texto grande destaque"> */
.texto { font-family: Georgia, serif; }
.grande { font-size: 1.25rem; }
Seletor de ID
Seleciona o elemento com um determinado ID único. Tem especificidade muito mais alta que classes, o que dificulta a sobreposição e manutenção:
/* O elemento com id="cabecalho-principal" */
#cabecalho-principal {
background-color: #12243A;
padding: 1.5rem 2rem;
}
Boa prática: reserve seletores de ID para JavaScript (
document.getElementById) e âncoras de navegação (href="#secao"). Para estilização, prefira classes — elas são reutilizáveis e têm especificidade mais baixa, facilitando sobrescrições futuras.
Seletor universal
Seleciona todos os elementos. Usado principalmente em resets e para aplicar box-sizing:
/* Reset universal de box-sizing */
*,
*::before,
*::after {
box-sizing: border-box;
}
7.5.2 — Seletores compostos (combinadores)¶
Combinadores expressam relações estruturais entre elementos na árvore DOM.
Descendente (espaço) — seleciona todos os descendentes, independente da profundidade:
/* Todos os <a> dentro de <nav>, em qualquer nível */
nav a {
color: white;
text-decoration: none;
}
Filho direto (>) — seleciona apenas filhos diretos, não descendentes mais profundos:
/* Apenas <li> filhos diretos de <ul class="menu"> */
.menu > li {
display: inline-block;
margin-right: 1rem;
}
Irmão adjacente (+) — seleciona o elemento imediatamente após outro:
/* O <p> que vem imediatamente após um <h2> */
h2 + p {
font-size: 1.125rem;
color: #555;
}
Irmãos gerais (~) — seleciona todos os irmãos após um elemento:
/* Todos os <p> que são irmãos após um <h2> */
h2 ~ p {
margin-left: 1rem;
}
Agrupamento (,) — aplica a mesma regra a múltiplos seletores:
h1, h2, h3, h4 {
font-family: Georgia, serif;
color: #12243A;
line-height: 1.2;
}
7.5.3 — Seletores de atributo¶
Selecionam elementos com base na presença ou valor de atributos HTML:
/* Elementos com o atributo target */
a[target] { font-weight: bold; }
/* Elementos com target="_blank" exato */
a[target="_blank"]::after {
content: " ↗";
font-size: 0.8em;
}
/* href que começa com "https" */
a[href^="https"] { color: green; }
/* href que termina com ".pdf" */
a[href$=".pdf"] { color: red; }
/* class que contém a palavra "btn" */
[class*="btn"] { cursor: pointer; }
/* input com type="email" */
input[type="email"] {
border: 2px solid #0057B8;
}
7.5.4 — Pseudo-classes¶
Pseudo-classes selecionam elementos com base em seu estado ou posição na estrutura do documento:
Estados de interação:
/* Link não visitado */
a:link { color: #0057B8; }
/* Link visitado */
a:visited { color: #6B21A8; }
/* Mouse sobre o elemento */
a:hover { text-decoration: underline; }
/* Elemento ativo (sendo clicado) */
a:active { color: #E8632A; }
/* Elemento com foco (teclado ou clique) */
input:focus {
outline: 3px solid #0057B8;
outline-offset: 2px;
}
/* Foco apenas via teclado (não via clique) */
button:focus-visible {
outline: 3px solid #0057B8;
}
/* Checkbox marcado */
input[type="checkbox"]:checked + label {
font-weight: bold;
}
/* Campo de formulário desabilitado */
input:disabled {
background-color: #e9ecef;
cursor: not-allowed;
}
/* Campo obrigatório */
input:required {
border-left: 3px solid #E8632A;
}
/* Campo com valor válido */
input:valid { border-color: green; }
/* Campo com valor inválido (após interação) */
input:invalid { border-color: red; }
Posição estrutural:
/* Primeiro filho */
li:first-child { font-weight: bold; }
/* Último filho */
li:last-child { border-bottom: none; }
/* Enésimo filho */
tr:nth-child(even) { background-color: #f8f9fa; } /* linhas pares */
tr:nth-child(odd) { background-color: white; } /* linhas ímpares */
tr:nth-child(3) { background-color: #fff3cd; } /* terceira linha */
/* Fórmula: nth-child(An+B) */
li:nth-child(3n) { color: red; } /* a cada 3 elementos */
li:nth-child(3n+1) { color: blue; } /* 1º, 4º, 7º... */
/* Primeiro de um tipo específico */
p:first-of-type { font-size: 1.125rem; }
/* Elemento único de seu tipo */
p:only-child { text-align: center; }
/* Elementos que NÃO correspondem ao seletor */
li:not(.ativo) { opacity: 0.6; }
input:not([type="submit"]) { border: 1px solid #ccc; }
/* Pseudo-classe :is() — simplifica agrupamentos complexos */
:is(h1, h2, h3) + p { margin-top: 0; }
7.5.5 — Pseudo-elementos¶
Pseudo-elementos permitem estilizar partes específicas de um elemento ou inserir conteúdo antes ou depois dele:
/* Primeira letra de um parágrafo */
p::first-letter {
font-size: 3em;
font-weight: bold;
float: left;
line-height: 1;
margin-right: 0.1em;
}
/* Primeira linha de um parágrafo */
p::first-line {
font-variant: small-caps;
}
/* Conteúdo antes do elemento */
.citacao::before {
content: "\201C"; /* aspas abertas " */
font-size: 3em;
color: #E8632A;
}
/* Conteúdo depois do elemento */
.obrigatorio::after {
content: " *";
color: red;
aria-hidden: true;
}
/* Texto selecionado pelo usuário */
::selection {
background-color: #E8632A;
color: white;
}
/* Placeholder de inputs */
input::placeholder {
color: #9CA3AF;
font-style: italic;
}
7.6 — Especificidade e Cascata¶
Vídeo curto explicativo (link será adicionado posteriormente)
A especificidade e a cascata são os dois mecanismos que o CSS utiliza para resolver conflitos quando múltiplas regras se aplicam ao mesmo elemento. Compreendê-los é essencial para evitar o uso indiscriminado de !important e para escrever CSS previsível e manutenível.
7.6.1 — Cálculo de especificidade¶
A especificidade é calculada como um valor de três componentes, representado como (A, B, C):
| Componente | O que conta | Valor |
|---|---|---|
| A | Seletores de ID (#id) |
100 pontos |
| B | Classes (.classe), pseudo-classes (:hover), atributos ([type]) |
10 pontos |
| C | Tipos de elemento (p, div, h1), pseudo-elementos (::before) |
1 ponto |
O seletor universal (*) e os combinadores (>, +, ~, ) não contribuem com especificidade.
/* Especificidade: (0, 0, 1) = 1 ponto */
p { color: red; }
/* Especificidade: (0, 1, 0) = 10 pontos */
.texto { color: blue; }
/* Especificidade: (0, 1, 1) = 11 pontos */
p.texto { color: green; }
/* Especificidade: (1, 0, 0) = 100 pontos */
#titulo { color: orange; }
/* Especificidade: (1, 1, 1) = 111 pontos */
#titulo p.texto { color: purple; }
Quando dois seletores têm a mesma especificidade, a ordem de declaração decide: a última regra declarada prevalece.
Imagem sugerida: diagrama visual do cálculo de especificidade com exemplos de seletores e seus valores — similar a um placar (A, B, C) com cada componente colorido de forma distinta.
(imagem será adicionada posteriormente)
7.6.2 — CSS inline e !important¶
CSS inline (declarado no atributo style) tem especificidade equivalente a (1, 0, 0, 0) — superior a qualquer seletor CSS externo ou interno.
!important sobrescreve qualquer regra, independente da especificidade:
/* Esta regra prevalece sobre qualquer outra, incluindo CSS inline */
.elemento {
color: red !important;
}
⚠️ Alerta:
!importantdeve ser tratado como último recurso, não como ferramenta padrão. Seu uso indiscriminado cria um "braço de ferro" de especificidade que torna o CSS impossível de manter. Em projetos reais, a necessidade frequente de!importantindica que a arquitetura de seletores precisa ser revisada.
7.6.3 — A cascata: quatro fatores de resolução¶
Quando múltiplas regras conflitantes se aplicam ao mesmo elemento, o CSS as resolve por meio da cascata, que considera quatro fatores em ordem de prioridade:
-
Origem e importância — estilos do navegador (user agent) < estilos do autor (desenvolvedor) < estilos do usuário.
!importantinverte essa ordem para o fator correspondente. -
Especificidade — conforme calculado acima.
-
Ordem de aparição — entre regras de mesma especificidade, a última declarada prevalece.
-
Herança — valores herdados do elemento pai (explicado na próxima seção).
/* Estilos do navegador (implícitos):
h1 { font-size: 2em; font-weight: bold; } */
/* Estilos do desenvolvedor — sobrescrevem o navegador */
h1 {
font-size: 2.5rem; /* sobrescreve o navegador */
color: #12243A; /* adiciona nova propriedade */
}
/* Regra mais específica — sobrescreve a anterior */
.conteudo-principal h1 {
font-size: 3rem;
}
7.6.4 — Herança¶
A herança é o mecanismo pelo qual certas propriedades CSS se propagam automaticamente de um elemento pai para seus descendentes. Isso permite definir propriedades tipográficas no body e tê-las aplicadas a todos os elementos da página sem repetição:
body {
font-family: 'Trebuchet MS', sans-serif;
font-size: 16px;
color: #333;
line-height: 1.6;
}
/* Todos os elementos filhos herdam essas propriedades
a menos que as sobrescrevam explicitamente */
Propriedades tipicamente herdáveis: color, font-family, font-size, font-weight, font-style, line-height, text-align, letter-spacing, visibility, cursor.
Propriedades tipicamente não herdáveis: width, height, margin, padding, border, background, display, position, top, left.
Controlando herança explicitamente:
.elemento {
/* Força a herança do valor do pai */
color: inherit;
/* Usa o valor inicial (padrão da especificação) */
margin: initial;
/* Reverte para o estilo do navegador */
display: revert;
/* Equivale a inherit se a propriedade é herdável,
initial se não for */
border: unset;
}
7.7 — Box Model¶
Vídeo curto explicativo (link será adicionado posteriormente)
O Box Model (modelo de caixa) é o fundamento do layout em CSS. Todo elemento HTML é representado como uma caixa retangular composta por quatro camadas concêntricas, da mais interna para a mais externa:
┌─────────────────────────────────────┐
│ MARGIN │ ← Área externa (transparente)
│ ┌───────────────────────────────┐ │
│ │ BORDER │ │ ← Borda visível
│ │ ┌─────────────────────────┐ │ │
│ │ │ PADDING │ │ │ ← Espaçamento interno
│ │ │ ┌───────────────────┐ │ │ │
│ │ │ │ CONTENT │ │ │ │ ← Conteúdo (texto, imagem)
│ │ │ └───────────────────┘ │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
7.7.1 — As quatro camadas¶
Content (conteúdo)
A área onde o conteúdo do elemento é renderizado — texto, imagem, etc. Suas dimensões são controladas pelas propriedades width e height.
Padding (preenchimento interno)
Espaço entre o conteúdo e a borda. É parte do elemento visualmente — herda o background-color do elemento:
.card {
padding: 1.5rem; /* todos os lados iguais */
padding: 1rem 2rem; /* vertical | horizontal */
padding: 1rem 2rem 1.5rem; /* top | horizontal | bottom */
padding: 1rem 2rem 1.5rem 1rem; /* top | right | bottom | left */
/* Propriedades individuais */
padding-top: 1rem;
padding-right: 2rem;
padding-bottom: 1rem;
padding-left: 2rem;
}
Border (borda)
A linha que envolve o padding e o conteúdo:
.elemento {
border: 2px solid #ccc; /* shorthand: largura estilo cor */
border: 1px dashed #E8632A;
border: 3px double #12243A;
/* Lados individuais */
border-top: 4px solid #E8632A;
border-bottom: 1px solid #eee;
/* Propriedades individuais */
border-width: 2px;
border-style: solid; /* solid, dashed, dotted, double, none */
border-color: #ccc;
/* Arredondamento */
border-radius: 8px; /* todos os cantos */
border-radius: 8px 0 8px 0; /* TL TR BR BL */
border-radius: 50%; /* círculo (em elemento quadrado) */
}
Margin (margem externa)
Espaço externo ao elemento — separa o elemento de seus vizinhos. A margem é sempre transparente (não herda background):
.paragrafo {
margin: 1rem 0; /* 1rem top/bottom, 0 left/right */
margin-bottom: 1.5rem;
margin: 0 auto; /* centraliza horizontalmente (em elementos block) */
}
Outline
Tecnicamente não faz parte do Box Model — não ocupa espaço no layout. É renderizado sobre o elemento, por fora da borda. Usado principalmente para indicadores de foco acessíveis:
button:focus-visible {
outline: 3px solid #0057B8;
outline-offset: 2px; /* espaço entre outline e a borda do elemento */
}
7.7.2 — box-sizing: o comportamento que você realmente quer¶
Por padrão, o CSS calcula width e height como as dimensões do content apenas. Isso significa que padding e border são adicionados por fora, aumentando o tamanho total do elemento:
/* box-sizing: content-box (padrão) */
.elemento {
width: 300px;
padding: 20px;
border: 5px solid #ccc;
}
/* Largura total real: 300 + 20 + 20 + 5 + 5 = 350px */
Este comportamento é contraintuitivo — quando você define width: 300px, espera que o elemento ocupe 300px, não 350px. A propriedade box-sizing: border-box corrige isso, fazendo com que width e height incluam padding e border:
/* box-sizing: border-box */
.elemento {
box-sizing: border-box;
width: 300px;
padding: 20px;
border: 5px solid #ccc;
}
/* Largura total real: 300px — padding e border são subtraídos do content */
A convenção universalmente adotada no desenvolvimento web moderno é aplicar border-box globalmente:
/* Reset padrão do box-sizing — inclua em todo projeto */
*,
*::before,
*::after {
box-sizing: border-box;
}
No DevTools: selecione qualquer elemento na aba Elements e observe o diagrama do Box Model no painel Computed. Ele exibe as dimensões reais de content, padding, border e margin do elemento selecionado — uma ferramenta essencial para diagnosticar problemas de espaçamento e layout.
7.7.3 — Colapso de margens¶
O colapso de margens (margin collapsing) é um comportamento do CSS que funde as margens verticais adjacentes de dois elementos em uma única margem — igual à maior das duas, não à soma:
.paragrafo-1 { margin-bottom: 2rem; }
.paragrafo-2 { margin-top: 1rem; }
/* Resultado: espaço entre os parágrafos = 2rem (não 3rem) */
O colapso ocorre em três situações:
- Irmãos adjacentes: a margem inferior do primeiro e a margem superior do segundo colapsam
- Pai e primeiro/último filho: se não houver border, padding ou conteúdo separando-os
- Elemento vazio: margens superior e inferior do próprio elemento colapsam entre si
O colapso não ocorre em elementos flex, grid, inline-block, ou quando há overflow diferente de visible.
/* Prevenindo colapso com padding ou border no pai */
.container {
padding-top: 1px; /* evita colapso com o primeiro filho */
/* ou */
overflow: hidden; /* também previne colapso */
}
7.7.4 — Dimensionamento: width, height, min-* e max-*¶
.elemento {
/* Valores fixos */
width: 400px;
height: 200px;
/* Valores relativos */
width: 50%; /* 50% do elemento pai */
height: 100vh; /* 100% da altura do viewport */
/* Limites: evitam que o elemento fique muito pequeno ou grande */
min-width: 200px;
max-width: 800px;
min-height: 100px;
max-height: 500px;
/* Padrão responsivo: elemento fluido com largura máxima */
width: 100%;
max-width: 1200px;
margin: 0 auto; /* centraliza */
}
width: auto vs width: 100%:
auto(padrão para elementos block): o elemento ocupa o espaço disponível, respeitando padding e margem100%: o elemento ocupa 100% da largura do pai, e o padding é adicionado por fora (comcontent-box) — pode causar overflow
7.8 — Tipografia e Estilização de Texto¶
Vídeo curto explicativo (link será adicionado posteriormente)
A tipografia é um dos componentes mais impactantes do design visual. Escolhas tipográficas inadequadas comprometem a legibilidade, a hierarquia e a percepção de qualidade de uma interface. O CSS oferece um conjunto rico de propriedades para controle tipográfico preciso.
7.8.1 — Propriedades de fonte¶
font-family — família tipográfica
Define a fonte utilizada. Aceita uma lista de fontes em ordem de preferência (font stack) — o navegador usa a primeira disponível no sistema do usuário:
body {
/* Stack com fallbacks: fonte específica → alternativa → genérica */
font-family: 'Trebuchet MS', Helvetica, Arial, sans-serif;
}
h1, h2, h3 {
font-family: Georgia, 'Times New Roman', Times, serif;
}
code, pre {
font-family: 'Fira Code', 'Cascadia Code', Consolas, monospace;
}
As categorias genéricas (serif, sans-serif, monospace, cursive, fantasy) devem sempre ser o último item do stack, garantindo que o navegador use alguma fonte adequada mesmo quando todas as anteriores falharem.
font-size — tamanho da fonte
html { font-size: 16px; } /* Base: 1rem = 16px */
body { font-size: 1rem; } /* 16px */
h1 { font-size: 2.5rem; } /* 40px */
h2 { font-size: 2rem; } /* 32px */
h3 { font-size: 1.5rem; } /* 24px */
small { font-size: 0.875rem; } /* 14px */
font-weight — peso (espessura)
p { font-weight: 400; } /* normal */
strong { font-weight: 700; } /* bold */
.leve { font-weight: 300; } /* light */
.titulo { font-weight: 900; } /* black/heavy */
/* Palavras-chave equivalentes */
.normal { font-weight: normal; } /* = 400 */
.negrito { font-weight: bold; } /* = 700 */
font-style — estilo
em { font-style: italic; }
.obliquo { font-style: oblique; }
.normal { font-style: normal; }
line-height — altura da linha
Uma das propriedades mais importantes para legibilidade. Aceita valores sem unidade (multiplicador do font-size), que são preferíveis aos valores em px ou rem por herdarem corretamente:
body {
line-height: 1.6; /* 160% do font-size — adequado para texto corrido */
}
h1 {
line-height: 1.2; /* Títulos grandes ficam melhor com line-height menor */
}
.codigo {
line-height: 1.5;
}
7.8.2 — Estilização de texto¶
/* Alinhamento */
.centro { text-align: center; }
.direita { text-align: right; }
.justificado { text-align: justify; }
/* Decoração */
a { text-decoration: underline; }
a:hover { text-decoration: none; }
del { text-decoration: line-through; }
.sublinhado-personalizado {
text-decoration: underline dotted #E8632A;
}
/* Transformação */
.maiusculo { text-transform: uppercase; }
.minusculo { text-transform: lowercase; }
.capitalizado { text-transform: capitalize; }
/* Espaçamento de letras e palavras */
.espacado { letter-spacing: 0.1em; }
.titulo-caixa-alta {
text-transform: uppercase;
letter-spacing: 0.15em;
}
.palavra-espaco { word-spacing: 0.25em; }
/* Indentação */
p { text-indent: 1.5em; }
/* Controle de quebra de linha */
.sem-quebra { white-space: nowrap; }
.pre-formato { white-space: pre; }
.truncado {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis; /* "..." no final */
}
7.8.3 — Web Fonts: Google Fonts e @font-face¶
Fontes do sistema variam entre dispositivos e sistemas operacionais — o que garante consistência é carregar fontes externas.
Google Fonts (forma mais simples):
<!-- No <head>: link de pré-conexão + importação da fonte -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Merriweather:ital,wght@0,400;0,700;1,400&display=swap"
rel="stylesheet"
/>
body {
font-family: 'Inter', sans-serif;
}
h1, h2, h3 {
font-family: 'Merriweather', serif;
}
@font-face (fontes auto-hospedadas):
Quando você possui os arquivos de fonte localmente — por licenciamento, desempenho ou privacidade:
@font-face {
font-family: 'MinhaFonte';
src:
url('../fonts/minha-fonte.woff2') format('woff2'),
url('../fonts/minha-fonte.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap; /* exibe fallback até a fonte carregar */
}
@font-face {
font-family: 'MinhaFonte';
src: url('../fonts/minha-fonte-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
body {
font-family: 'MinhaFonte', sans-serif;
}
font-display: swapinstrui o navegador a exibir o texto com uma fonte de fallback enquanto a fonte personalizada carrega, em vez de deixar o texto invisível. É uma boa prática de desempenho e experiência do usuário.
7.9 — Cores e Backgrounds¶
Vídeo curto explicativo (link será adicionado posteriormente)
7.9.1 — Sistemas de cores¶
O CSS suporta múltiplos sistemas de representação de cores, cada um com características distintas:
Hexadecimal (HEX)
.elemento {
color: #12243A; /* 6 dígitos: #RRGGBB */
color: #E8632A;
color: #fff; /* 3 dígitos (shorthand): #RGB */
color: #0057B8CC; /* 8 dígitos: #RRGGBBAA (com alpha) */
}
RGB e RGBA
.elemento {
color: rgb(18, 36, 58); /* Valores 0–255 */
color: rgb(232, 99, 42);
background-color: rgba(18, 36, 58, 0.8); /* Com opacidade 0–1 */
/* Sintaxe moderna (CSS Color Level 4) */
color: rgb(18 36 58); /* sem vírgulas */
color: rgb(18 36 58 / 0.8); /* com alpha */
}
HSL e HSLA (Hue, Saturation, Lightness)
HSL é o sistema mais intuitivo para criar paletas de cores — modificar a luminosidade L mantendo H e S cria variações consistentes:
.elemento {
/* hsl(matiz 0-360°, saturação 0-100%, luminosidade 0-100%) */
color: hsl(210, 53%, 15%); /* azul escuro */
color: hsl(20, 78%, 54%); /* laranja */
color: hsl(210, 53%, 15%, 0.8); /* com opacidade */
/* Criar variações de uma cor mantendo identidade visual */
--cor-base: 210 53%; /* matiz + saturação fixos */
--cor-900: hsl(var(--cor-base) 10%);
--cor-700: hsl(var(--cor-base) 25%);
--cor-500: hsl(var(--cor-base) 45%);
--cor-300: hsl(var(--cor-base) 65%);
--cor-100: hsl(var(--cor-base) 90%);
}
7.9.2 — Propriedades de background¶
.elemento {
/* Cor de fundo */
background-color: #f8f9fa;
/* Imagem de fundo */
background-image: url('../images/pattern.png');
background-image: url('../images/hero.jpg');
/* Repetição */
background-repeat: no-repeat; /* não repete */
background-repeat: repeat-x; /* repete horizontalmente */
background-repeat: repeat; /* repete em ambos os eixos */
/* Posicionamento */
background-position: center;
background-position: top right;
background-position: 50% 30%;
/* Tamanho */
background-size: cover; /* cobre toda a área, pode cortar */
background-size: contain; /* cabe inteira, pode deixar espaços */
background-size: 200px 150px;
background-size: 100% auto;
/* Shorthand */
background: url('../images/hero.jpg') center/cover no-repeat;
}
background-size: cover vs contain:
cover: a imagem é escalada para cobrir completamente o elemento, mantendo proporção — partes podem ser cortadascontain: a imagem é escalada para caber inteiramente no elemento, mantendo proporção — podem aparecer áreas vazias
7.9.3 — Gradientes¶
/* Gradiente linear */
.hero {
background-image: linear-gradient(135deg, #12243A, #1C3A52);
background-image: linear-gradient(to right, #E8632A, #12243A);
background-image: linear-gradient(
to bottom,
rgba(18, 36, 58, 0) 0%,
rgba(18, 36, 58, 0.9) 100%
);
}
/* Gradiente radial */
.circulo {
background-image: radial-gradient(circle, #E8632A, #12243A);
background-image: radial-gradient(
ellipse at top left,
#1C3A52 0%,
#12243A 100%
);
}
/* Gradiente cônico */
.pizza {
background-image: conic-gradient(
#E8632A 0% 25%,
#12243A 25% 50%,
#1C7293 50% 75%,
#F7F5F2 75% 100%
);
}
/* Múltiplos backgrounds */
.elemento {
background:
linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)),
url('../images/hero.jpg') center/cover no-repeat;
}
7.9.4 — Opacidade e transparência¶
/* opacity: afeta o elemento inteiro, incluindo conteúdo */
.overlay {
opacity: 0.8; /* 0 = transparente, 1 = opaco */
}
/* rgba/hsla: afeta apenas a propriedade específica */
.fundo-semi-transparente {
background-color: rgba(18, 36, 58, 0.8); /* apenas o fundo é transparente */
color: white; /* texto permanece opaco */
}
7.10 — Display e Fluxo de Layout¶
Vídeo curto explicativo (link será adicionado posteriormente)
A propriedade display é a mais fundamental do layout CSS. Ela determina o modelo de formatação de um elemento — como ele se comporta no fluxo do documento e como seus filhos são organizados.
7.10.1 — Fluxo normal do documento¶
O fluxo normal é o comportamento padrão de layout quando nenhuma propriedade de posicionamento ou display especial é aplicada. Ele é governado por dois tipos de caixas:
Elementos de bloco (block): ocupam toda a largura disponível do pai, empilham-se verticalmente. Exemplos: <div>, <p>, <h1>–<h6>, <section>, <article>, <header>, <footer>, <ul>, <li>.
Elementos inline: ocupam apenas o espaço de seu conteúdo, fluem horizontalmente no texto, não quebram linha. Exemplos: <span>, <a>, <strong>, <em>, <img>.
7.10.2 — Valores de display¶
/* block: elemento de bloco — ocupa toda a largura, quebra linha */
.elemento { display: block; }
/* inline: flui com o texto, ignora width/height e margens verticais */
.elemento { display: inline; }
/* inline-block: flui com o texto MAS aceita width, height e margens */
.badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
background: #E8632A;
color: white;
}
/* none: remove o elemento do layout E da acessibilidade */
.oculto { display: none; }
/* flex: ativa Flexbox no container (Capítulo 8) */
.container { display: flex; }
/* grid: ativa Grid Layout no container (Capítulo 9) */
.grade { display: grid; }
display: none vs visibility: hidden:
/* none: elemento não existe no layout — não ocupa espaço */
.removido { display: none; }
/* hidden: elemento é invisível, mas MANTÉM seu espaço no layout */
.invisivel { visibility: hidden; }
7.10.3 — Overflow¶
A propriedade overflow controla o que acontece quando o conteúdo de um elemento ultrapassa suas dimensões definidas:
.elemento {
overflow: visible; /* padrão: conteúdo transborda para fora */
overflow: hidden; /* conteúdo cortado na borda do elemento */
overflow: scroll; /* sempre exibe barras de rolagem */
overflow: auto; /* barras de rolagem apenas quando necessário */
/* Controle por eixo */
overflow-x: hidden;
overflow-y: auto;
}
/* Caso de uso comum: container com conteúdo previsível */
.tabela-responsiva {
overflow-x: auto; /* scroll horizontal em telas pequenas */
max-width: 100%;
}
/* Texto com reticências */
.titulo-truncado {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
7.11 — Posicionamento¶
Vídeo curto explicativo (link será adicionado posteriormente)
A propriedade position controla como um elemento é posicionado em relação ao fluxo normal do documento ou a um contexto de posicionamento específico.
7.11.1 — position: static (padrão)¶
O valor padrão. O elemento segue o fluxo normal do documento. As propriedades top, right, bottom e left não têm efeito:
.normal {
position: static; /* comportamento padrão */
}
7.11.2 — position: relative¶
O elemento permanece no fluxo normal, mas pode ser deslocado em relação à sua posição original usando top, right, bottom e left. O espaço original do elemento é preservado no layout:
.deslocado {
position: relative;
top: 10px; /* desloca 10px para baixo */
left: 20px; /* desloca 20px para a direita */
}
Uso mais importante: criar um contexto de posicionamento para elementos filhos com position: absolute:
.container {
position: relative; /* contexto de posicionamento */
}
7.11.3 — position: absolute¶
O elemento é removido do fluxo normal — não ocupa espaço no layout. É posicionado em relação ao ancestral mais próximo com position diferente de static. Se nenhum ancestral for posicionado, é relativo ao <html>:
.pai {
position: relative; /* contexto de posicionamento */
width: 300px;
height: 200px;
}
.filho-absoluto {
position: absolute;
top: 0;
right: 0; /* canto superior direito do pai */
width: 80px;
height: 30px;
}
/* Centralizar com absolute */
.centralizado {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* ajusta pelo próprio tamanho */
}
7.11.4 — position: fixed¶
O elemento é removido do fluxo normal e posicionado em relação ao viewport — permanece na mesma posição mesmo com rolagem da página:
/* Barra de navegação fixa no topo */
.navbar-fixa {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 1000;
background-color: #12243A;
}
/* Botão "voltar ao topo" */
.btn-topo {
position: fixed;
bottom: 2rem;
right: 2rem;
}
7.11.5 — position: sticky¶
Comportamento híbrido: o elemento segue o fluxo normal até atingir um threshold definido, quando "gruda" na posição especificada:
/* Cabeçalho de tabela que gruda no topo ao rolar */
thead th {
position: sticky;
top: 0;
background-color: #12243A;
color: white;
z-index: 1;
}
/* Navegação lateral que gruda ao rolar */
.nav-lateral {
position: sticky;
top: 2rem; /* gruda a 2rem do topo */
height: fit-content;
}
Requisito: o elemento pai deve ter altura suficiente para que o sticky funcione, e overflow do pai não pode ser hidden ou auto.
7.11.6 — z-index e contexto de empilhamento¶
A propriedade z-index controla a ordem de empilhamento de elementos posicionados (com position diferente de static). Valores maiores ficam na frente:
.modal-overlay {
position: fixed;
z-index: 1000;
}
.modal {
position: fixed;
z-index: 1001; /* acima do overlay */
}
.tooltip {
position: absolute;
z-index: 100;
}
Contexto de empilhamento:
z-indexfunciona dentro de contextos de empilhamento. Um elemento comopacity < 1,transform,filterouwill-changecria um novo contexto de empilhamento —z-indexde seus filhos é relativo a esse contexto, não ao documento global. Este é um dos comportamentos mais frequentemente mal compreendidos do CSS.
7.12 — Variáveis CSS (Custom Properties)¶
Vídeo curto explicativo (link será adicionado posteriormente)
As variáveis CSS (oficialmente chamadas de Custom Properties) permitem definir valores reutilizáveis que podem ser referenciados em qualquer ponto da folha de estilos. Elas são fundamentais para criar sistemas de design consistentes e facilitar a manutenção de projetos.
7.12.1 — Definição e uso¶
Variáveis CSS são definidas com o prefixo -- e referenciadas com a função var():
/* Definição: geralmente no :root para escopo global */
:root {
/* Paleta de cores */
--cor-primaria: #12243A;
--cor-secundaria: #1C3A52;
--cor-destaque: #E8632A;
--cor-texto: #333333;
--cor-fundo: #F7F5F2;
/* Tipografia */
--fonte-base: 'Trebuchet MS', sans-serif;
--fonte-titulo: Georgia, serif;
--tamanho-base: 1rem;
/* Espaçamento */
--espaco-xs: 0.25rem;
--espaco-sm: 0.5rem;
--espaco-md: 1rem;
--espaco-lg: 2rem;
--espaco-xl: 4rem;
/* Bordas */
--raio-borda: 8px;
--raio-borda-sm: 4px;
--raio-circulo: 9999px;
/* Sombras */
--sombra-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
--sombra-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--sombra-lg: 0 10px 25px rgba(0, 0, 0, 0.15);
/* Transições */
--transicao-padrao: 200ms ease-in-out;
}
/* Uso em qualquer regra */
.btn-primario {
background-color: var(--cor-destaque);
color: white;
font-family: var(--fonte-base);
padding: var(--espaco-sm) var(--espaco-md);
border-radius: var(--raio-borda);
transition: background-color var(--transicao-padrao);
}
.card {
background-color: var(--cor-fundo);
border-radius: var(--raio-borda);
box-shadow: var(--sombra-md);
padding: var(--espaco-lg);
}
7.12.2 — Escopo e herança¶
Variáveis CSS respeitam o escopo do seletor onde são definidas e são herdadas pelos descendentes:
/* Global */
:root {
--cor-destaque: #E8632A;
}
/* Sobrescrevendo localmente */
.tema-escuro {
--cor-destaque: #FF8C55; /* versão mais clara para fundo escuro */
--cor-fundo: #1a1a1a;
--cor-texto: #f5f5f5;
}
/* Todos os elementos dentro de .tema-escuro usam as variáveis locais */
.tema-escuro .btn {
background-color: var(--cor-destaque); /* usa #FF8C55 */
}
7.12.3 — Valor de fallback¶
A função var() aceita um segundo argumento como valor de fallback:
.elemento {
/* Se --cor-primaria não estiver definida, usa #12243A */
color: var(--cor-primaria, #12243A);
/* Fallback pode referenciar outra variável */
background: var(--cor-fundo, var(--cor-secundaria, white));
}
7.12.4 — Variáveis CSS vs pré-processadores¶
Variáveis CSS diferem das variáveis de pré-processadores como Sass/LESS em aspectos importantes:
| Característica | Variáveis CSS | Variáveis Sass |
|---|---|---|
| Processamento | Runtime (navegador) | Compilação |
| Escopo | Cascata e herança CSS | Léxico |
| Modificável via JS | ✅ Sim | ❌ Não |
| Suporte a media queries | ✅ Sim | ❌ Não |
| Compatibilidade | Navegadores modernos | Requer build step |
A capacidade de modificar variáveis CSS via JavaScript as torna especialmente poderosas para temas dinâmicos e animações:
// Modificando uma variável CSS via JavaScript
document.documentElement.style.setProperty('--cor-destaque', '#1C7293');
7.13 — Organização e Boas Práticas¶
Vídeo curto explicativo (link será adicionado posteriormente)
CSS sem organização cresce rapidamente em um arquivo difícil de manter, com conflitos de especificidade e regras redundantes. Boas práticas de organização são especialmente relevantes para projetos de sistemas de informação, onde o código é mantido por equipes ao longo do tempo.
7.13.1 — Estrutura de arquivos¶
Para projetos simples, um único arquivo style.css organizado em seções é suficiente:
css/
└── style.css
Para projetos maiores, a separação por responsabilidade é recomendada:
css/
├── reset.css /* Reset/normalização de estilos do navegador */
├── variables.css /* Variáveis CSS (Custom Properties) */
├── typography.css /* Tipografia global */
├── layout.css /* Estrutura geral da página */
├── components.css /* Componentes reutilizáveis */
└── utilities.css /* Classes utilitárias */
Dentro do arquivo principal, organizar em seções comentadas:
/* ─── 1. Reset ─────────────────────────────────── */
*,
*::before,
*::after { box-sizing: border-box; }
/* ─── 2. Variáveis ──────────────────────────────── */
:root { ... }
/* ─── 3. Base ───────────────────────────────────── */
body { ... }
h1, h2, h3 { ... }
/* ─── 4. Layout ─────────────────────────────────── */
.container { ... }
.grid { ... }
/* ─── 5. Componentes ────────────────────────────── */
.card { ... }
.btn { ... }
.nav { ... }
/* ─── 6. Utilitários ────────────────────────────── */
.oculto { display: none; }
.centralizado { text-align: center; }
7.13.2 — Nomeação de classes¶
A nomeação consistente de classes é essencial para legibilidade e manutenção. A convenção mais adotada é o BEM (Block, Element, Modifier):
/* Bloco: componente autônomo */
.card { }
/* Elemento: parte do bloco, separado por __ */
.card__titulo { }
.card__imagem { }
.card__rodape { }
/* Modificador: variação do bloco ou elemento, separado por -- */
.card--destaque { }
.card--pequeno { }
.card__titulo--grande { }
<article class="card card--destaque">
<img class="card__imagem" src="..." alt="..." />
<div class="card__corpo">
<h2 class="card__titulo card__titulo--grande">Título</h2>
<p class="card__texto">Conteúdo...</p>
</div>
<footer class="card__rodape">Rodapé</footer>
</article>
7.13.3 — Evitando conflitos de especificidade¶
Algumas diretrizes para manter a especificidade sob controle:
- Prefira classes a seletores de tipo e ID para estilização
- Evite encadear mais de 3 seletores —
nav ul li aé difícil de sobrescrever - Não use
!importantexceto em utilitários (display: none !important) - Use especificidade crescente: estilos base com baixa especificidade, sobrescrições com especificidade um pouco maior
- Defina estilos de componentes com uma única classe sempre que possível
/* DIFÍCIL DE MANTER: alta especificidade, difícil de sobrescrever */
header nav ul li a.ativo { color: red; }
/* MELHOR: classe simples, fácil de sobrescrever */
.nav-link--ativo { color: var(--cor-destaque); }
7.13.4 — Reset e normalização¶
Navegadores aplicam estilos padrão diferentes aos elementos HTML. Um CSS reset remove esses estilos, enquanto um normalize os padroniza entre navegadores:
/* Reset minimalista moderno (baseado em Andy Bell / Josh Comeau) */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
}
body {
min-height: 100vh;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit; /* herda fonte do body, não usa a padrão do sistema */
}
p,
h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
Referências: - MDN — CSS Reference - W3C — CSS Specifications - CSS Tricks — A Complete Guide to CSS - Kevin Powell — CSS no YouTube - CSS Zen Garden - Can I Use — Compatibilidade de recursos CSS
Atividades — Capítulo 7¶
1. Qual é a especificidade do seletor #nav .menu > li:hover?
2. Um elemento tem width: 300px, padding: 20px e border: 5px solid. Com box-sizing: content-box, qual é a largura total ocupada na página?
3. Qual a diferença entre position: absolute e position: fixed?
4. Por que é recomendado usar rem em vez de px para font-size?
- GitHub Classroom: Estilizar a página HTML do projeto do 1º Bimestre aplicando: reset com
box-sizing: border-box, sistema de variáveis CSS no:root, tipografia com Web Font do Google Fonts, paleta de cores consistente, e posicionamento correto deheaderfixo efooter. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 6 — Introdução à Acessibilidade Web :material-arrow-right: Ir ao Capítulo 8 — Layout com Flexbox
Capítulo 8 — Layout com Flexbox¶
Vídeo curto explicativo (link será adicionado posteriormente)
8.1 — O que é Flexbox e quando usar¶
Vídeo curto explicativo (link será adicionado posteriormente)
O Flexbox (Flexible Box Layout Module) é um modelo de layout CSS projetado para distribuir espaço e alinhar elementos em uma dimensão — uma linha ou uma coluna. Introduzido como recomendação pelo W3C em 2012 e amplamente suportado desde 2015, o Flexbox resolveu um problema que afligia desenvolvedores web há quase duas décadas: a ausência de um mecanismo declarativo e previsível para alinhar e distribuir elementos em um container.
8.1.1 — O problema que o Flexbox resolve¶
Antes do Flexbox, criar layouts que pareciam simples — centralizar verticalmente um elemento, fazer colunas de altura igual, distribuir itens uniformemente em uma barra de navegação — exigia combinações frágeis e contraintuitivas de propriedades CSS que não foram projetadas para layout:
/* Era do pré-Flexbox: hacks para centralização vertical */
/* Hack 1: tabela (semanticamente incorreto) */
.container { display: table; }
.filho { display: table-cell; vertical-align: middle; }
/* Hack 2: posicionamento absoluto (inflexível) */
.filho {
position: absolute;
top: 50%;
left: 50%;
margin-top: -50px; /* metade da altura — valor hardcoded */
margin-left: -100px; /* metade da largura — valor hardcoded */
}
/* Hack 3: floats (requer clearfix, quebra o fluxo) */
.coluna { float: left; width: 33.33%; }
.container::after { content: ""; display: table; clear: both; }
Esses padrões funcionavam em casos específicos, mas quebravam ao mudar o tamanho do container, adicionar conteúdo dinâmico ou adaptar para diferentes viewports. O Flexbox substituiu todos esses hacks por um modelo coerente e expressivo:
/* Flexbox: centralização vertical e horizontal em duas linhas */
.container {
display: flex;
justify-content: center;
align-items: center;
}
8.1.2 — Conceito de container e itens¶
O Flexbox opera em dois níveis hierárquicos:
- Flex container: o elemento ao qual
display: flexé aplicado. Ele define o contexto flex e controla como seus filhos diretos são distribuídos. - Flex items: os filhos diretos do flex container. Eles são os elementos que recebem e respondem às regras de layout flex.
<nav class="navbar"> <!-- flex container -->
<a href="/">Logo</a> <!-- flex item -->
<a href="/sobre">Sobre</a> <!-- flex item -->
<a href="/contato">Contato</a> <!-- flex item -->
</nav>
.navbar {
display: flex; /* transforma .navbar em flex container */
/* Os filhos diretos (<a>) tornam-se automaticamente flex items */
}
Ponto crítico: apenas os filhos diretos do container se tornam flex items. Descendentes mais profundos não são afetados diretamente pelo contexto flex do container pai — a menos que eles próprios também sejam declarados como flex containers.
8.1.3 — Quando usar Flexbox vs Grid¶
Flexbox e Grid são complementares, não concorrentes. A escolha entre eles segue um princípio simples:
| Flexbox | Grid | |
|---|---|---|
| Dimensionalidade | Uma dimensão (linha ou coluna) | Duas dimensões (linhas e colunas) |
| Controle | A partir do conteúdo (content-first) | A partir do layout (layout-first) |
| Melhor para | Componentes: navbars, cards, forms, botões | Estruturas de página: grids de conteúdo, layouts completos |
A heurística prática: se você está distribuindo itens em uma única direção — uma linha de botões, uma lista de cards, uma barra de navegação —, Flexbox é a escolha natural. Se você precisa alinhar elementos em linhas e colunas simultaneamente — uma grade de artigos, um layout de página com header/sidebar/main/footer —, Grid é mais adequado. Na prática, a maioria dos projetos usa ambos: Grid para a estrutura macro da página, Flexbox para os componentes internos.
Referência: MDN — Flexbox
8.2 — Os dois eixos do Flexbox¶
Vídeo curto explicativo (link será adicionado posteriormente)
O modelo mental mais importante para dominar o Flexbox é a compreensão dos dois eixos que governam todo o sistema de alinhamento.
8.2.1 — Eixo principal (main axis)¶
O eixo principal é a direção ao longo da qual os flex items são distribuídos. Por padrão, ele corre horizontalmente da esquerda para a direita. As propriedades de alinhamento que atuam sobre o eixo principal são justify-content (no container) e justify-self (nos itens, com suporte limitado no Flexbox).
Eixo principal padrão (flex-direction: row):
←────────────────────────────────────────→
[ Item 1 ] [ Item 2 ] [ Item 3 ]
8.2.2 — Eixo cruzado (cross axis)¶
O eixo cruzado é sempre perpendicular ao eixo principal. Por padrão, ele corre verticalmente de cima para baixo. As propriedades que atuam sobre o eixo cruzado são align-items e align-content (no container) e align-self (nos itens).
Eixo cruzado padrão (flex-direction: row):
↑
│ [ Item 1 ] [ Item 2 ] [ Item 3 ]
│
↓
8.2.3 — Como flex-direction muda os eixos¶
A propriedade flex-direction define a direção do eixo principal — e consequentemente do eixo cruzado. Esta é a propriedade mais fundamental do Flexbox, pois redefine o significado de todas as outras propriedades de alinhamento:
/* row (padrão): eixo principal → horizontal (esquerda para direita) */
.container { flex-direction: row; }
/* row-reverse: eixo principal → horizontal (direita para esquerda) */
.container { flex-direction: row-reverse; }
/* column: eixo principal → vertical (cima para baixo) */
.container { flex-direction: column; }
/* column-reverse: eixo principal → vertical (baixo para cima) */
.container { flex-direction: column-reverse; }
flex-direction: row flex-direction: column
──────────────────── ─────────────────────
→ [1] [2] [3] ↓ [1]
eixo principal: → [2]
eixo cruzado: ↓ [3]
eixo principal: ↓
eixo cruzado: →
Imagem sugerida: diagrama visual dos quatro valores de
flex-directionmostrando a orientação dos eixos principal e cruzado em cada caso, com os itens numerados dispostos de acordo.(imagem será adicionada posteriormente)
A compreensão de que justify-content atua sempre sobre o eixo principal e align-items atua sempre sobre o eixo cruzado — independentemente de qual seja qual — é o que permite usar Flexbox com previsibilidade. Muita confusão com Flexbox vem de pensar em termos de "horizontal/vertical" em vez de "eixo principal/cruzado".
8.3 — Propriedades do container flex¶
Vídeo curto explicativo (link será adicionado posteriormente)
8.3.1 — display: flex e display: inline-flex¶
/* flex: o container se comporta como bloco (ocupa toda a largura) */
.container {
display: flex;
}
/* inline-flex: o container se comporta como inline-block */
.badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
A distinção é sobre o comportamento externo do container — como ele se relaciona com o fluxo do documento ao redor. Internamente, os dois se comportam de forma idêntica para os flex items.
8.3.2 — flex-direction¶
Já apresentado na seção 8.2.3. Recapitulando os quatro valores:
.container {
flex-direction: row; /* padrão */
flex-direction: row-reverse;
flex-direction: column;
flex-direction: column-reverse;
}
Caso de uso prático de column: componentes de card com conteúdo empilhado verticalmente, layouts mobile-first que empilham elementos, sidebars de navegação vertical.
8.3.3 — flex-wrap¶
Por padrão, flex items não quebram linha — eles encolhem para caber no container mesmo que isso os torne menores do que seu tamanho ideal. flex-wrap controla esse comportamento:
.container {
flex-wrap: nowrap; /* padrão: todos na mesma linha, encolhem se necessário */
flex-wrap: wrap; /* quebra para a próxima linha quando necessário */
flex-wrap: wrap-reverse; /* quebra para linha acima */
}
flex-wrap: nowrap (padrão):
[ Item 1 ][ Item 2 ][ Item 3 ][ Item 4 ][ Item 5 ]
↑ itens encolhem para caber
flex-wrap: wrap:
[ Item 1 ][ Item 2 ][ Item 3 ]
[ Item 4 ][ Item 5 ]
↑ itens quebram para a próxima linha
flex-wrap: wrap é fundamental para layouts responsivos com Flexbox — permite que itens se reorganizem naturalmente em telas menores sem media queries.
8.3.4 — flex-flow — shorthand¶
flex-flow combina flex-direction e flex-wrap em uma única declaração:
.container {
flex-flow: row wrap; /* direção + quebra */
flex-flow: column nowrap;
flex-flow: row-reverse wrap;
}
8.3.5 — justify-content — alinhamento no eixo principal¶
justify-content define como os flex items são distribuídos ao longo do eixo principal quando há espaço sobrando:
.container {
justify-content: flex-start; /* padrão: itens no início */
justify-content: flex-end; /* itens no final */
justify-content: center; /* itens centralizados */
justify-content: space-between; /* espaço igual ENTRE os itens */
justify-content: space-around; /* espaço igual AO REDOR de cada item */
justify-content: space-evenly; /* espaço igual entre todos, incluindo bordas */
}
justify-content: flex-start
[1][2][3]_ _ _ _ _ _
justify-content: flex-end
_ _ _ _ _ _[1][2][3]
justify-content: center
_ _ _[1][2][3]_ _ _
justify-content: space-between
[1]_ _ _ _[2]_ _ _ _[3]
justify-content: space-around
_ [1] _ _ [2] _ _ [3] _
←→ ←→ ←→
(espaço dobrado entre itens)
justify-content: space-evenly
_ _[1]_ _[2]_ _[3]_ _
←→ ←→ ←→ ←→
(espaço idêntico em todos os gaps)
No DevTools: no painel Elements, ao selecionar um flex container, o Chrome exibe um ícone de grade ao lado de
display: flexna aba Styles. Clicando nele, abre um editor visual interativo de Flexbox que permite testar todos os valores dejustify-contentealign-itemsem tempo real — uma ferramenta essencial para entender o comportamento de cada valor.
8.3.6 — align-items — alinhamento no eixo cruzado¶
align-items define como os flex items são alinhados ao longo do eixo cruzado:
.container {
align-items: stretch; /* padrão: itens se esticam para preencher o container */
align-items: flex-start; /* itens alinhados no início do eixo cruzado */
align-items: flex-end; /* itens alinhados no final do eixo cruzado */
align-items: center; /* itens centralizados no eixo cruzado */
align-items: baseline; /* itens alinhados pela linha de base do texto */
}
align-items: stretch (padrão) align-items: center
┌──────────────────────┐ ┌──────────────────────┐
│ ┌────┐ ┌──────┐ ┌──┐ │ │ ┌────┐ │
│ │ │ │ │ │ │ │ │ │ │ ┌──────┐ │
│ │ 1 │ │ 2 │ │3 │ │ │ │ 1 │ │ 2 │ │
│ │ │ │ │ │ │ │ │ │ │ └──────┘ │
│ └────┘ └──────┘ └──┘ │ │ └────┘ ┌──┐ │
└──────────────────────┘ │ │3 │ │
itens esticam na altura do │ └──┘ │
container └──────────────────────┘
itens centralizados
baseline é especialmente útil quando itens têm fontes de tamanhos diferentes e precisam ser alinhados pelo texto:
.toolbar {
display: flex;
align-items: baseline; /* alinha pelo texto de cada item */
}
8.3.7 — align-content — alinhamento de múltiplas linhas¶
align-content só tem efeito quando flex-wrap: wrap está ativo e há múltiplas linhas de flex items. Ele controla a distribuição das linhas no eixo cruzado — análogo ao justify-content, mas para linhas em vez de itens:
.container {
flex-wrap: wrap;
align-content: flex-start; /* linhas no início */
align-content: flex-end; /* linhas no final */
align-content: center; /* linhas centralizadas */
align-content: space-between; /* espaço igual entre linhas */
align-content: space-around; /* espaço ao redor das linhas */
align-content: stretch; /* padrão: linhas se esticam */
}
Confusão comum:
align-itemsalinha os itens dentro de cada linha;align-contentdistribui as linhas no container. Em containers de linha única,align-contentnão tem efeito.
8.3.8 — gap, row-gap e column-gap¶
A propriedade gap define o espaçamento entre flex items sem usar margins — o que evita o problema clássico de "margem na última coluna":
.container {
display: flex;
gap: 1rem; /* espaçamento igual em todos os eixos */
gap: 1rem 2rem; /* row-gap | column-gap */
row-gap: 1rem; /* apenas entre linhas */
column-gap: 2rem; /* apenas entre colunas */
}
Sem gap (usando margin): Com gap:
[1][margin][2][margin][3][margin] [1][gap][2][gap][3]
↑ ↑
margem indesejada sem margem extra
no último item
gap é a forma recomendada de criar espaçamento em layouts flex modernos — mais semântico e menos propenso a erros do que margins nos itens.
8.4 — Propriedades dos itens flex¶
Vídeo curto explicativo (link será adicionado posteriormente)
As propriedades dos itens controlam como cada flex item individualmente cresce, encolhe e se posiciona dentro do container.
8.4.1 — flex-grow — capacidade de crescimento¶
flex-grow define a proporção na qual um item pode crescer para ocupar o espaço disponível no container. O valor padrão é 0 — os itens não crescem além de seu tamanho base:
/* Três itens: apenas o segundo cresce */
.item-1 { flex-grow: 0; } /* não cresce */
.item-2 { flex-grow: 1; } /* ocupa todo o espaço disponível */
.item-3 { flex-grow: 0; } /* não cresce */
/* Dois itens com crescimento proporcional */
.item-principal { flex-grow: 2; } /* recebe 2/3 do espaço disponível */
.item-lateral { flex-grow: 1; } /* recebe 1/3 do espaço disponível */
Container: 900px | Item 1: 100px base | Item 2: 100px base | Item 3: 100px base
Espaço disponível: 900 - 300 = 600px
flex-grow: 0, 1, 0:
[100px][ 700px (100+600) ][100px]
flex-grow: 1, 1, 1:
[ 300px ][ 300px ][ 300px ]
(600px divididos igualmente entre os três)
8.4.2 — flex-shrink — capacidade de encolhimento¶
flex-shrink define a proporção na qual um item pode encolher quando o espaço é insuficiente. O valor padrão é 1 — todos os itens encolhem proporcionalmente:
.item-fixo { flex-shrink: 0; } /* não encolhe — mantém tamanho base */
.item-flexivel { flex-shrink: 1; } /* encolhe normalmente (padrão) */
.item-rapido { flex-shrink: 3; } /* encolhe 3x mais rápido que os outros */
Caso de uso comum: um ícone ou logo em uma navbar que não deve encolher, enquanto o restante dos itens se adapta:
.navbar-logo {
flex-shrink: 0; /* logo nunca encolhe */
width: 120px;
}
.navbar-links {
flex-shrink: 1; /* links podem encolher */
}
8.4.3 — flex-basis — tamanho base¶
flex-basis define o tamanho inicial de um item antes de flex-grow e flex-shrink serem aplicados. Funciona como width (em flex-direction: row) ou como height (em flex-direction: column):
.item {
flex-basis: auto; /* padrão: usa width/height do item */
flex-basis: 0; /* tamanho inicial zero — cresce a partir do zero */
flex-basis: 200px; /* tamanho base fixo de 200px */
flex-basis: 33.33%; /* tamanho base de 1/3 do container */
}
flex-basis: 0 vs flex-basis: auto: quando flex-basis: 0, o espaço disponível é distribuído proporcionalmente sem considerar o conteúdo dos itens. Com flex-basis: auto, o conteúdo é considerado antes da distribuição.
8.4.4 — flex — shorthand e valores comuns¶
O shorthand flex combina flex-grow, flex-shrink e flex-basis:
.item {
flex: <grow> <shrink> <basis>;
flex: 1; /* flex: 1 1 0% — cresce, encolhe, base zero */
flex: auto; /* flex: 1 1 auto — cresce, encolhe, base automática */
flex: none; /* flex: 0 0 auto — não cresce, não encolhe */
flex: 0 auto; /* flex: 0 1 auto — não cresce, encolhe */
flex: 2 1 300px; /* cresce 2x, encolhe 1x, base 300px */
}
Os valores mais utilizados na prática:
/* flex: 1 — o item mais comum: cresce proporcionalmente */
.coluna { flex: 1; }
/* Colunas com proporções diferentes */
.coluna-principal { flex: 2; } /* ocupa 2/3 */
.coluna-lateral { flex: 1; } /* ocupa 1/3 */
/* flex: none — item com tamanho fixo, não se adapta */
.sidebar-fixa { flex: none; width: 250px; }
/* flex: 0 0 auto — equivalente ao none */
.logo { flex: 0 0 auto; }
Boa prática: prefira o shorthand
flexàs propriedades individuaisflex-grow,flex-shrinkeflex-basis. O shorthand define valores padrão inteligentes para os componentes não especificados — por exemplo,flex: 1defineflex-basis: 0%, que geralmente é o comportamento desejado.
8.4.5 — align-self — alinhamento individual¶
align-self sobrescreve o align-items do container para um item específico. Aceita os mesmos valores de align-items:
.container {
display: flex;
align-items: center; /* todos os itens centralizados por padrão */
}
.item-topo {
align-self: flex-start; /* este item se alinha no topo */
}
.item-base {
align-self: flex-end; /* este item se alinha na base */
}
.item-esticado {
align-self: stretch; /* este item se estica para preencher */
}
8.4.6 — order — reordenação visual¶
order controla a ordem visual dos flex items sem alterar a ordem no HTML. O valor padrão é 0; valores menores aparecem primeiro, valores maiores aparecem depois:
.item-a { order: 2; } /* aparece por último */
.item-b { order: -1; } /* aparece antes de todos (ordem 0) */
.item-c { order: 1; } /* aparece segundo */
/* HTML: A, B, C → Visual: B(-1), A(0→padrão não declarado), C(1), A(2)... */
/* Na prática: B, C, A */
Caso de uso legítimo: reordenar elementos em diferentes breakpoints para responsividade — por exemplo, mover uma barra lateral para antes do conteúdo principal em mobile:
@media (max-width: 768px) {
.sidebar { order: -1; } /* sobe para antes do main em mobile */
.conteudo { order: 1; }
}
⚠️ Atenção crítica — ver seção 8.6:
orderaltera apenas a apresentação visual — a ordem de leitura dos leitores de tela e a navegação por teclado seguem a ordem do DOM. Usarorderpara reordenar conteúdo semanticamente importante cria uma experiência inacessível para usuários de tecnologias assistivas.
8.5 — Padrões práticos de layout com Flexbox¶
Vídeo curto explicativo (link será adicionado posteriormente)
Compreender as propriedades do Flexbox isoladamente não é suficiente — a habilidade real está em combiná-las para resolver problemas concretos de layout. Esta seção apresenta os padrões mais frequentes no desenvolvimento web moderno.
8.5.1 — Navbar responsiva¶
Uma barra de navegação com logo à esquerda e links à direita é um dos layouts mais comuns da Web, e um dos que mais se beneficiam do Flexbox:
<header class="navbar">
<a href="/" class="navbar__logo">
<img src="logo.svg" alt="IFAL" />
</a>
<nav class="navbar__links">
<a href="/">Início</a>
<a href="/cursos">Cursos</a>
<a href="/sobre">Sobre</a>
<a href="/contato">Contato</a>
</nav>
</header>
.navbar {
display: flex;
justify-content: space-between; /* logo esquerda, links direita */
align-items: center; /* centraliza verticalmente */
padding: 1rem 2rem;
background-color: var(--cor-primaria);
}
.navbar__logo img {
height: 40px;
flex-shrink: 0; /* logo não encolhe */
}
.navbar__links {
display: flex; /* os links também são um flex container */
gap: 2rem;
align-items: center;
}
.navbar__links a {
color: white;
text-decoration: none;
font-size: 0.95rem;
}
/* Responsividade: empilha em mobile */
@media (max-width: 768px) {
.navbar {
flex-direction: column;
gap: 1rem;
}
.navbar__links {
gap: 1rem;
}
}
8.5.2 — Centralização perfeita¶
Centralizar um elemento tanto horizontal quanto verticalmente foi historicamente um dos problemas mais difíceis do CSS. Com Flexbox, torna-se trivial:
/* Centralização no container */
.container-centralizado {
display: flex;
justify-content: center; /* eixo principal: horizontal */
align-items: center; /* eixo cruzado: vertical */
min-height: 100vh; /* ou qualquer altura definida */
}
/* Centralização de um único item usando margin: auto */
.item-centralizado {
margin: auto;
/* margin: auto em flex items absorve todo o espaço disponível
em todas as direções — centralizando o item */
}
margin: auto em flex items é uma técnica poderosa e menos conhecida: quando aplicada a um flex item, a margem auto absorve todo o espaço disponível na direção correspondente:
.navbar {
display: flex;
align-items: center;
padding: 0 2rem;
}
.navbar__logo { margin-right: auto; } /* empurra tudo para a direita */
/* Resultado: logo à esquerda, demais itens à direita */
/* sem usar justify-content: space-between */
8.5.3 — Cards em linha com altura igual¶
Um dos problemas clássicos do layout web é garantir que cards em linha tenham a mesma altura, independentemente do tamanho do conteúdo interno:
<section class="cards">
<article class="card">
<img src="img1.jpg" alt="..." />
<div class="card__corpo">
<h2>Título curto</h2>
<p>Descrição breve.</p>
</div>
<footer class="card__rodape">
<a href="#">Saiba mais</a>
</footer>
</article>
<article class="card">
<img src="img2.jpg" alt="..." />
<div class="card__corpo">
<h2>Título mais longo que o anterior</h2>
<p>Descrição bem mais longa que ocupa mais linhas de texto.</p>
</div>
<footer class="card__rodape">
<a href="#">Saiba mais</a>
</footer>
</article>
</section>
/* Container de cards */
.cards {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
}
/* Cada card: flex container em coluna */
.card {
display: flex;
flex-direction: column; /* empilha conteúdo verticalmente */
flex: 1 1 280px; /* cresce, encolhe, mínimo de 280px */
border-radius: var(--raio-borda);
box-shadow: var(--sombra-md);
overflow: hidden;
}
.card img {
width: 100%;
height: 200px;
object-fit: cover; /* corta a imagem para preencher o espaço */
}
.card__corpo {
flex: 1; /* ocupa todo o espaço disponível — empurra o rodapé para baixo */
padding: 1.5rem;
}
/* O rodapé fica sempre na base do card, independente do conteúdo */
.card__rodape {
padding: 1rem 1.5rem;
border-top: 1px solid #eee;
}
O segredo deste padrão é flex: 1 no .card__corpo: ele faz com que o corpo do card ocupe todo o espaço vertical disponível, independentemente do tamanho do conteúdo — o que empurra o rodapé para a base de todos os cards de forma igual.
8.5.4 — Footer grudado no rodapé (sticky footer)¶
Um problema clássico: o footer deve ficar na base da viewport em páginas com pouco conteúdo, e após o conteúdo em páginas longas:
<body>
<header>...</header>
<main>...</main>
<footer>...</footer>
</body>
body {
display: flex;
flex-direction: column;
min-height: 100vh; /* body ocupa pelo menos toda a altura da tela */
}
main {
flex: 1; /* main cresce para ocupar todo o espaço disponível */
/* header e footer ficam com seu tamanho natural */
/* footer é empurrado para o final */
}
Este é um dos padrões mais elegantes do Flexbox: três linhas de CSS resolvem um problema que antes exigia posicionamento absoluto ou cálculos com calc().
8.5.5 — Layout de formulário com labels e inputs alinhados¶
<form class="formulario">
<div class="campo">
<label for="nome">Nome:</label>
<input type="text" id="nome" name="nome" />
</div>
<div class="campo">
<label for="email">E-mail:</label>
<input type="email" id="email" name="email" />
</div>
<div class="acoes">
<button type="reset">Limpar</button>
<button type="submit">Enviar</button>
</div>
</form>
.formulario {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 500px;
}
/* Cada campo: label + input lado a lado */
.campo {
display: flex;
align-items: center;
gap: 1rem;
}
.campo label {
flex: 0 0 100px; /* largura fixa: não cresce, não encolhe */
text-align: right;
font-weight: 600;
}
.campo input {
flex: 1; /* input ocupa todo o espaço restante */
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
/* Botões de ação alinhados à direita */
.acoes {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
8.6 — Flexbox e acessibilidade¶
Vídeo curto explicativo (link será adicionado posteriormente)
O Flexbox introduz uma capacidade que, mal utilizada, cria problemas sérios de acessibilidade: a possibilidade de separar a ordem visual dos elementos da sua ordem no DOM.
8.6.1 — A propriedade order e a ordem de leitura¶
A especificação CSS deixa claro: propriedades como order e flex-direction: row-reverse afetam apenas a apresentação visual. A ordem em que os elementos aparecem no DOM continua sendo a ordem utilizada por:
- Leitores de tela (NVDA, JAWS, VoiceOver) ao ler o conteúdo sequencialmente
- Navegação por teclado ao avançar pelo
Tab - Seleção de texto ao arrastar o cursor
- Mecanismos de busca ao indexar o conteúdo
Isso significa que se você usa order para apresentar visualmente o elemento B antes do elemento A, um usuário de leitor de tela ouvirá A antes de B — criando uma experiência desconexada entre o que é visto e o que é ouvido.
/* PROBLEMÁTICO: reordenação que cria desconexão semântica */
.card-destaque { order: -1; } /* aparece visualmente primeiro */
/* mas no DOM ainda é o terceiro elemento — leitor de tela lê por último */
8.6.2 — Reordenação visual vs ordem do DOM¶
A regra é direta: se a ordem visual importa para a compreensão do conteúdo, ela deve ser refletida na ordem do DOM.
order e reordenação por Flexbox são aceitáveis quando:
- A reordenação é puramente estética (ex.: mover um elemento decorativo)
- O conteúdo faz sentido em qualquer ordem (ex.: uma galeria de imagens independentes)
- A reordenação é aplicada apenas para efeitos visuais em viewports específicos onde a lógica de leitura não muda
order e reordenação por Flexbox não são aceitáveis quando:
- A ordem dos elementos é parte do significado do conteúdo (ex.: etapas de um processo, hierarquia de informação)
- O elemento reordenado é interativo (link, botão, campo) — a navegação por teclado seguirá a ordem do DOM
/* USO ACEITÁVEL: reordenação estética em galeria */
.galeria__destaque {
order: -1; /* imagem de destaque aparece primeiro visualmente */
/* todas as imagens são equivalentes — qualquer ordem faz sentido */
}
/* USO PROBLEMÁTICO: reordenação de conteúdo sequencial */
.passo-3 { order: 1; } /* Passo 3 aparece visualmente antes do Passo 1 */
.passo-1 { order: 2; } /* leitor de tela lê na ordem do DOM: 3, 1, 2 */
.passo-2 { order: 3; } /* confuso para usuários de tecnologia assistiva */
/* SOLUÇÃO: reorganize o DOM, não a apresentação visual */
A diretriz WCAG 2.1 relevante é o critério de sucesso 1.3.2 — Sequência com Significado (nível A): "Se a sequência em que o conteúdo é apresentado afeta seu significado, uma sequência de leitura correta pode ser determinada programaticamente."
Referências: - MDN — Flexbox e acessibilidade - W3C — CSS Flexible Box Layout Module Level 1 - CSS Tricks — A Complete Guide to Flexbox - Flexbox Froggy — exercício interativo
Atividades — Capítulo 8¶
1. Em um flex container com flex-direction: column, qual propriedade controla o alinhamento horizontal dos itens?
2. Três flex items têm flex-grow: 1, flex-grow: 2 e flex-grow: 1, respectivamente. O container tem 200px de espaço disponível após o tamanho base dos itens. Quanto espaço cada item recebe?
3. Por que o uso de order para reordenar conteúdo semanticamente sequencial (como etapas de um processo) é considerado uma falha de acessibilidade?
- GitHub Classroom: Construir uma página com: (1) navbar com logo e links usando Flexbox; (2) seção de cards responsivos com altura igual e footer sempre na base; (3) sticky footer aplicado ao
<body>. Todos os layouts devem usar apenas Flexbox, semfloatoupositionpara estrutura. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 7 — Fundamentos do CSS :material-arrow-right: Ir ao Capítulo 9 — Layout com Grid
Capítulo 9 — Layout com Grid¶
Vídeo curto explicativo (link será adicionado posteriormente)
9.1 — O que é CSS Grid e quando usar¶
Vídeo curto explicativo (link será adicionado posteriormente)
O CSS Grid Layout é o sistema de layout bidimensional nativo do CSS — projetado especificamente para organizar elementos em linhas e colunas simultaneamente. Enquanto o Flexbox opera em uma única dimensão (uma linha ou uma coluna por vez), o Grid permite posicionar elementos em uma grade com controle preciso sobre ambos os eixos ao mesmo tempo.
Publicado como recomendação pelo W3C em 2017 e com suporte amplo em todos os navegadores modernos desde então, o CSS Grid representa a ferramenta mais poderosa já disponibilizada nativamente pelo CSS para criação de layouts — eliminando a necessidade dos sistemas de grid baseados em frameworks como Bootstrap para a maioria dos casos de uso.
9.1.1 — O problema que o Grid resolve¶
Antes do CSS Grid, criar layouts de duas dimensões — como a estrutura clássica de uma página web com cabeçalho, conteúdo principal, sidebar e rodapé — exigia combinações complexas de floats, posicionamento absoluto ou frameworks externos:
/* Era pré-Grid: layout com float */
.header { width: 100%; }
.main { float: left; width: 70%; }
.sidebar { float: right; width: 28%; margin-left: 2%; }
.footer { clear: both; width: 100%; }
/* Problemas: clearfix necessário, altura igual entre colunas impossível,
reordenação para mobile requer JavaScript ou marcação adicional */
Com CSS Grid, o mesmo layout é declarado de forma clara, semântica e manutenível:
.pagina {
display: grid;
grid-template-columns: 1fr 300px;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"main sidebar"
"footer footer";
min-height: 100vh;
}
9.1.2 — Conceito de grid container, grid items, linhas, colunas e células¶
O Grid opera em dois níveis, assim como o Flexbox:
- Grid container: o elemento ao qual
display: gridé aplicado. Define a estrutura da grade. - Grid items: os filhos diretos do grid container. São posicionados nas células da grade.
A grade em si é composta por:
- Colunas (columns): divisões verticais da grade
- Linhas (rows): divisões horizontais da grade
- Células (cells): a intersecção de uma linha com uma coluna — a unidade mínima da grade
- Áreas (areas): agrupamento retangular de uma ou mais células adjacentes
9.1.3 — Terminologia: grid lines, tracks, areas e gaps¶
O CSS Grid possui uma terminologia precisa que é essencial dominar para interpretar a especificação e a documentação:
col 1 col 2 col 3
| | | |
linha 1| [1,1] | [1,2] | [1,3] |
| | | |
linha 2| [2,1] | [2,2] | [2,3] |
| | | |
linha 3| [3,1] | [3,2] | [3,3] |
| | | |
Grid lines (linhas de grade): as linhas divisórias numeradas
→ colunas: 1, 2, 3, 4 (n colunas = n+1 linhas verticais)
→ linhas: 1, 2, 3, 4 (n linhas = n+1 linhas horizontais)
Grid tracks (trilhas): o espaço entre duas grid lines adjacentes
→ uma coluna é uma trilha vertical
→ uma linha é uma trilha horizontal
Grid area: retângulo formado por grid lines
→ pode abranger múltiplas células
Gap: espaço entre trilhas (anteriormente chamado de grid-gap)
Imagem sugerida: diagrama visual da grade com grid lines numeradas em azul, células destacadas em cinza claro, e um exemplo de grid area abrangendo múltiplas células em laranja — com labels de cada conceito.
(imagem será adicionada posteriormente)
As grid lines são numeradas a partir de 1, não de 0. Em uma grade de 3 colunas, as linhas verticais são numeradas 1, 2, 3 e 4. Também podem ser referenciadas de trás para frente com valores negativos: -1 é sempre a última linha, -2 a penúltima, etc.
9.1.4 — Grid vs Flexbox: escolhendo o modelo certo¶
A tabela abaixo complementa a visão apresentada no Capítulo 8, agora com o Grid disponível como referência concreta:
| Critério | Flexbox | Grid |
|---|---|---|
| Dimensões | 1D: linha ou coluna | 2D: linha e coluna |
| Ponto de partida | Conteúdo determina layout | Layout determina posição do conteúdo |
| Alinhamento | No eixo principal ou cruzado | Em ambos os eixos simultaneamente |
| Melhor para | Componentes (navbar, cards, forms) | Estruturas de página, galerias, dashboards |
| Posicionamento preciso | Limitado | Preciso por linhas e áreas nomeadas |
| Responsividade intrínseca | Via flex-wrap |
Via auto-fill/auto-fit + minmax() |
Na prática profissional, Grid e Flexbox são complementares e frequentemente usados juntos no mesmo projeto: Grid para a estrutura macro da página, Flexbox para os componentes internos.
Referência: MDN — CSS Grid Layout
9.2 — Definindo a grade: colunas e linhas¶
Vídeo curto explicativo (link será adicionado posteriormente)
9.2.1 — grid-template-columns e grid-template-rows¶
Estas duas propriedades definem a estrutura explícita da grade — quantas colunas e linhas existem, e qual o tamanho de cada trilha:
.container {
display: grid;
/* Três colunas: 200px, 1fr, 300px */
grid-template-columns: 200px 1fr 300px;
/* Duas linhas: primeira com 80px, segunda automática */
grid-template-rows: 80px auto;
}
/* Exemplos variados */
.grade-simples {
grid-template-columns: 1fr 1fr 1fr; /* três colunas iguais */
grid-template-columns: 25% 50% 25%; /* porcentagens */
grid-template-columns: 200px 1fr; /* fixa + flexível */
grid-template-columns: auto auto auto; /* automático pelo conteúdo */
}
9.2.2 — A unidade fr — fração do espaço disponível¶
A unidade fr (fraction) é exclusiva do CSS Grid e representa uma fração do espaço disponível no container após subtrair espaços fixos (colunas em px, %, gaps):
.container {
display: grid;
width: 900px;
gap: 20px;
/* Três colunas iguais: cada uma recebe 1/3 do espaço disponível */
grid-template-columns: 1fr 1fr 1fr;
/* Espaço disponível: 900px - (2 × 20px gap) = 860px → cada coluna ≈ 286px */
/* Proporções diferentes */
grid-template-columns: 2fr 1fr;
/* Coluna 1: 2/3 de 860px ≈ 573px | Coluna 2: 1/3 de 860px ≈ 287px */
/* Coluna fixa + coluna flexível */
grid-template-columns: 300px 1fr;
/* Coluna 1: 300px fixos | Coluna 2: 900px - 300px - 20px gap = 580px */
}
A distinção entre fr e % é importante: % é calculado sobre o tamanho total do container (incluindo gaps), enquanto fr é calculado sobre o espaço restante após descontar espaços fixos — tornando fr mais previsível em layouts com gaps.
9.2.3 — A função repeat()¶
repeat() evita a repetição manual de trilhas idênticas:
.container {
/* Sem repeat: verboso */
grid-template-columns: 1fr 1fr 1fr 1fr;
/* Com repeat: conciso */
grid-template-columns: repeat(4, 1fr);
/* Padrão repetido */
grid-template-columns: repeat(3, 1fr 2fr); /* 6 colunas: 1fr 2fr 1fr 2fr 1fr 2fr */
/* Valores mistos */
grid-template-columns: 200px repeat(3, 1fr) 200px;
/* resultado: 200px 1fr 1fr 1fr 200px */
/* Linhas também */
grid-template-rows: repeat(4, 150px);
grid-template-rows: 80px repeat(3, 1fr) 60px;
}
9.2.4 — A função minmax()¶
minmax(mínimo, máximo) define um intervalo de tamanho para uma trilha — ela pode crescer até o máximo e encolher até o mínimo:
.container {
/* Cada coluna tem no mínimo 200px e no máximo 1fr */
grid-template-columns: repeat(3, minmax(200px, 1fr));
/* Linhas com altura mínima, crescendo com o conteúdo */
grid-template-rows: repeat(4, minmax(100px, auto));
/* Sidebar com largura entre 200px e 300px */
grid-template-columns: minmax(200px, 300px) 1fr;
}
minmax() é especialmente útil em combinação com auto-fill/auto-fit para criar grids intrinsecamente responsivos.
9.2.5 — auto-fill vs auto-fit — grids intrinsecamente responsivos¶
Esta é uma das funcionalidades mais poderosas do CSS Grid: criar grades que se adaptam automaticamente ao espaço disponível sem nenhuma media query:
/* auto-fill: cria o máximo de colunas que caibam,
mantendo colunas vazias se houver espaço sobrando */
.grade-auto-fill {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
}
/* auto-fit: cria o máximo de colunas que caibam,
mas EXPANDE as existentes para preencher o espaço vazio */
.grade-auto-fit {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
Container: 800px | minmax(250px, 1fr)
Em 800px → 3 colunas cabem (3 × 250px = 750px ≤ 800px)
Em 600px → 2 colunas cabem (2 × 250px = 500px ≤ 600px)
Em 300px → 1 coluna cabe
auto-fill com 2 itens em container de 800px:
[ item 1 ][ item 2 ][ ][ ]
↑ colunas vazias mantidas
auto-fit com 2 itens em container de 800px:
[ item 1 ][ item 2 ]
↑ colunas vazias colapsadas, itens expandem
Quando usar cada um:
- auto-fill: quando a grade deve manter sua estrutura mesmo com poucos itens (ex.: grade de produtos que pode ter uma ou muitas linhas)
- auto-fit: quando os itens devem expandir para preencher o container quando há poucos (ex.: cards que devem sempre cobrir a largura total)
9.3 — Posicionamento de itens¶
Vídeo curto explicativo (link será adicionado posteriormente)
9.3.1 — Fluxo automático do grid¶
Por padrão, os grid items são posicionados automaticamente nas células da grade em ordem — preenchendo linha por linha, da esquerda para a direita:
<div class="container">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</div>
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
/* Resultado do fluxo automático:
[1][2][3]
[4][5] ← quinta célula vazia
*/
O fluxo automático é suficiente para a maioria dos casos — grades de cards, galerias, listas de produtos. O posicionamento explícito é necessário quando um item precisa ocupar uma posição ou tamanho específico na grade.
9.3.2 — grid-column e grid-row — posicionamento por linhas¶
grid-column e grid-row posicionam um item especificando entre quais grid lines ele deve se estender:
.item {
/* grid-column: linha-início / linha-fim */
grid-column: 1 / 3; /* da linha vertical 1 até a 3 (2 colunas) */
grid-row: 2 / 4; /* da linha horizontal 2 até a 4 (2 linhas) */
}
/* Exemplos */
.header {
grid-column: 1 / 4; /* ocupa todas as 3 colunas */
grid-row: 1 / 2; /* primeira linha */
}
.footer {
grid-column: 1 / -1; /* da primeira à última linha (-1 = última) */
grid-row: 4 / 5;
}
.destaque {
grid-column: 2 / 4; /* colunas 2 e 3 */
grid-row: 1 / 3; /* linhas 1 e 2 */
}
Referência por linha negativa: -1 sempre se refere à última grid line, -2 à penúltima, independentemente do número total de colunas ou linhas:
/* Ocupa a largura inteira da grade, independente de quantas colunas há */
.largura-total {
grid-column: 1 / -1;
}
9.3.3 — span — extensão por múltiplas células¶
Em vez de especificar a linha final, span indica quantas trilhas o item deve ocupar a partir de sua posição:
.item {
/* Equivalentes: ambos ocupam 2 colunas a partir da coluna 2 */
grid-column: 2 / 4;
grid-column: 2 / span 2;
/* Span sem posição inicial: o item ocupa 3 colunas a partir
de onde o fluxo automático o colocar */
grid-column: span 3;
grid-row: span 2;
}
span é especialmente útil em grades com fluxo automático onde a posição inicial não é determinística — apenas o tamanho do item é fixado:
/* Grade de galeria: algumas imagens são maiores */
.imagem-destaque {
grid-column: span 2;
grid-row: span 2;
/* ocupa 2×2 células, em qualquer posição que o fluxo colocar */
}
9.3.4 — grid-area — posicionamento com nome¶
grid-area é um shorthand que combina grid-row-start, grid-column-start, grid-row-end e grid-column-end:
.item {
/* grid-area: row-start / col-start / row-end / col-end */
grid-area: 1 / 2 / 3 / 4;
/* equivale a:
grid-row: 1 / 3;
grid-column: 2 / 4; */
}
O uso mais importante de grid-area, porém, é como identificador de área nomeada — explorado na próxima seção.
9.4 — Áreas nomeadas¶
Vídeo curto explicativo (link será adicionado posteriormente)
As áreas nomeadas são uma das funcionalidades mais expressivas do CSS Grid. Elas permitem declarar o layout da página como um mapa visual em ASCII diretamente no CSS — tornando o código autoexplicativo e extremamente fácil de modificar.
9.4.1 — grid-template-areas — definindo o mapa visual do layout¶
.pagina {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 80px 1fr 60px;
grid-template-areas:
"header header"
"sidebar main "
"footer footer";
min-height: 100vh;
gap: 1rem;
}
A string de grid-template-areas é um mapa visual direto do layout: cada linha da string corresponde a uma linha da grade, cada palavra a uma célula. Células com o mesmo nome formam uma área retangular contígua.
Regra importante: áreas nomeadas devem ser sempre retangulares e contíguas. Não é possível criar uma área em L ou T, por exemplo. Tentativas de criar áreas não retangulares produzem um valor inválido ignorado pelo navegador.
9.4.2 — Atribuindo itens a áreas com grid-area¶
Depois de definir as áreas no container, cada item recebe o nome da área correspondente:
<div class="pagina">
<header class="cabecalho">Cabeçalho</header>
<nav class="barra-lateral">Sidebar</nav>
<main class="conteudo">Conteúdo Principal</main>
<footer class="rodape">Rodapé</footer>
</div>
.pagina {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 80px 1fr 60px;
grid-template-areas:
"header header"
"sidebar main "
"footer footer";
min-height: 100vh;
gap: 1rem;
}
/* Cada item recebe o nome da sua área */
.cabecalho { grid-area: header; }
.barra-lateral { grid-area: sidebar; }
.conteudo { grid-area: main; }
.rodape { grid-area: footer; }
O resultado é um layout de página completo, semântico e visualmente declarado no CSS — sem nenhum posicionamento absoluto ou float.
9.4.3 — Células vazias com . (ponto)¶
Quando uma célula da grade não pertence a nenhuma área nomeada, usa-se . (ponto) como placeholder:
.dashboard {
display: grid;
grid-template-columns: 200px 1fr 1fr;
grid-template-rows: 60px repeat(3, 1fr);
grid-template-areas:
"nav header header "
"nav card-a card-b "
"nav card-c . " /* última célula vazia */
"nav footer footer ";
}
Múltiplos pontos na mesma célula (... ou . . .) também são válidos e equivalentes a um único ponto — alguns desenvolvedores usam múltiplos pontos para alinhar visualmente o mapa.
9.4.4 — Mudando o layout com media queries¶
Áreas nomeadas são especialmente poderosas para layouts responsivos — basta redefinir grid-template-areas em diferentes breakpoints:
/* Layout padrão: desktop */
.pagina {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 80px 1fr 60px;
grid-template-areas:
"header header"
"sidebar main "
"footer footer";
gap: 1rem;
}
/* Layout tablet: sidebar abaixo do conteúdo */
@media (max-width: 1024px) {
.pagina {
grid-template-columns: 1fr;
grid-template-rows: 80px 1fr auto 60px;
grid-template-areas:
"header "
"main "
"sidebar"
"footer ";
}
}
/* Layout mobile: coluna única, sidebar oculta */
@media (max-width: 600px) {
.pagina {
grid-template-columns: 1fr;
grid-template-rows: 60px 1fr 60px;
grid-template-areas:
"header"
"main "
"footer";
}
.barra-lateral {
display: none;
}
}
/* Os itens NÃO precisam ser alterados — apenas o container muda */
.cabecalho { grid-area: header; }
.barra-lateral { grid-area: sidebar; }
.conteudo { grid-area: main; }
.rodape { grid-area: footer; }
Esta é uma das vantagens mais significativas das áreas nomeadas: os itens não precisam ser alterados nas media queries — apenas o mapa do container é redesenhado. Isso torna layouts responsivos complexos muito mais fáceis de manter.
9.5 — Alinhamento no Grid¶
Vídeo curto explicativo (link será adicionado posteriormente)
O Grid compartilha o sistema de alinhamento do Flexbox, mas com uma camada adicional de controle: é possível alinhar tanto os itens dentro de suas células quanto a grade inteira dentro do container.
9.5.1 — justify-items e align-items¶
Estas propriedades controlam como os itens são alinhados dentro de suas células:
justify-items: alinhamento no eixo inline (horizontal em escrita ocidental)align-items: alinhamento no eixo block (vertical)
.container {
display: grid;
grid-template-columns: repeat(3, 200px);
grid-template-rows: repeat(3, 150px);
/* Padrão: stretch — itens preenchem a célula */
justify-items: stretch; /* padrão */
align-items: stretch; /* padrão */
/* Centralizar todos os itens em suas células */
justify-items: center;
align-items: center;
/* Outros valores */
justify-items: start;
justify-items: end;
align-items: start;
align-items: end;
align-items: baseline;
}
justify-items: stretch (padrão) justify-items: center
┌──────────────────┐ ┌──────────────────┐
│ ┌──────────────┐ │ │ ┌────────┐ │
│ │ item │ │ │ │ item │ │
│ └──────────────┘ │ │ └────────┘ │
└──────────────────┘ └──────────────────┘
item preenche a célula item centralizado
9.5.2 — justify-content e align-content¶
Quando a grade é menor que o container (quando as trilhas têm tamanho fixo e há espaço sobrando), estas propriedades controlam como a grade como um todo é posicionada no container:
.container {
display: grid;
width: 900px;
height: 600px;
grid-template-columns: repeat(3, 200px); /* 3 × 200px = 600px < 900px */
grid-template-rows: repeat(2, 150px); /* 2 × 150px = 300px < 600px */
/* Distribuir as colunas no espaço disponível */
justify-content: center;
justify-content: space-between;
justify-content: space-evenly;
justify-content: start; /* padrão */
justify-content: end;
/* Distribuir as linhas no espaço disponível */
align-content: center;
align-content: space-between;
align-content: end;
align-content: start; /* padrão */
}
9.5.3 — justify-self e align-self¶
Sobrescrevem justify-items e align-items para um item específico:
.item-especial {
justify-self: end; /* alinha à direita na célula */
align-self: start; /* alinha no topo da célula */
}
.item-centralizado {
justify-self: center;
align-self: center;
}
.item-esticado {
justify-self: stretch; /* estica para preencher a célula */
align-self: stretch;
}
9.5.4 — place-items e place-content — shorthands¶
.container {
/* place-items: align-items justify-items */
place-items: center; /* centraliza em ambos os eixos */
place-items: start end; /* align: start | justify: end */
/* place-content: align-content justify-content */
place-content: center;
place-content: space-between center;
/* place-self (em itens): align-self justify-self */
}
.item {
place-self: center; /* centraliza o item na célula */
}
Centralização com Grid em duas linhas:
.container {
display: grid;
place-items: center;
min-height: 100vh;
}
/* O filho direto é centralizado horizontal e verticalmente */
9.6 — Grid implícito e controle de fluxo¶
Vídeo curto explicativo (link será adicionado posteriormente)
A grade explícita é a estrutura definida por grid-template-columns, grid-template-rows e grid-template-areas. Quando itens são posicionados fora dessa estrutura — seja por posicionamento explícito além dos limites ou por fluxo automático que ultrapassa as linhas definidas —, o navegador cria automaticamente uma grade implícita para acomodá-los.
9.6.1 — grid-auto-rows e grid-auto-columns¶
Controlam o tamanho das trilhas criadas automaticamente pelo grid implícito:
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
/* Apenas 1 linha explícita definida: 200px */
grid-template-rows: 200px;
/* Linhas adicionais (implícitas) criadas automaticamente */
grid-auto-rows: 150px;
/* Sem grid-auto-rows, as linhas implícitas teriam altura mínima pelo conteúdo */
}
/* Uso com minmax: linha implícita com altura mínima */
.container {
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: minmax(150px, auto);
/* cada linha tem no mínimo 150px e cresce com o conteúdo */
}
grid-auto-rows: minmax(150px, auto) é um padrão muito utilizado em grades de cards: garante altura mínima consistente mas permite que o card cresça se o conteúdo for maior.
9.6.2 — grid-auto-flow — direção do fluxo automático¶
Controla como itens sem posicionamento explícito são colocados na grade:
.container {
grid-auto-flow: row; /* padrão: preenche linha por linha */
grid-auto-flow: column; /* preenche coluna por coluna */
grid-auto-flow: row dense; /* linha por linha, preenchendo lacunas */
grid-auto-flow: column dense; /* coluna por coluna, preenchendo lacunas */
}
Com grid-auto-flow: column, as trilhas implícitas criadas são colunas (não linhas), e grid-auto-columns controla seu tamanho:
.container {
display: grid;
grid-template-rows: repeat(3, 100px); /* 3 linhas fixas */
grid-auto-flow: column; /* itens fluem por colunas */
grid-auto-columns: 150px; /* colunas implícitas com 150px */
}
9.6.3 — grid-auto-flow: dense — preenchimento de lacunas¶
Quando alguns itens têm span maior que outros, podem surgir lacunas na grade. O valor dense instrui o algoritmo de posicionamento a tentar preencher essas lacunas com itens menores que venham depois:
.galeria {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 200px;
grid-auto-flow: row dense; /* preenche lacunas com itens subsequentes */
gap: 1rem;
}
.imagem-grande { grid-column: span 2; grid-row: span 2; }
.imagem-larga { grid-column: span 2; }
.imagem-alta { grid-row: span 2; }
/* imagens normais não têm span */
Sem dense: Com dense:
[G][G][ ][ ] [G][G][A][B]
[G][G][A][ ] [G][G][C][D]
[L][L][B][C] [L][L][E][F]
↑ lacunas ↑ lacunas preenchidas
⚠️ Atenção:
densepode alterar a ordem visual dos itens em relação à ordem do DOM — itens menores podem "saltar" para posições anteriores para preencher lacunas. Assim como a propriedadeorderdo Flexbox, isso pode criar problemas de acessibilidade quando a ordem de apresentação é semanticamente relevante.
9.7 — Padrões práticos de layout com Grid¶
Vídeo curto explicativo (link será adicionado posteriormente)
9.7.1 — Layout de página completo¶
O padrão mais fundamental do CSS Grid: estrutura de página com header, sidebar, conteúdo principal e footer:
<body class="pagina">
<header class="cabecalho">
<h1>IFAL — Programação Web 1</h1>
<nav>...</nav>
</header>
<aside class="barra-lateral">
<nav aria-label="Sumário">...</nav>
</aside>
<main class="conteudo-principal">
<article>...</article>
</main>
<footer class="rodape">
<p>© 2026 IFAL</p>
</footer>
</body>
.pagina {
display: grid;
grid-template-columns: 260px 1fr;
grid-template-rows: 70px 1fr auto;
grid-template-areas:
"header header"
"sidebar main "
"footer footer";
min-height: 100vh;
gap: 0; /* gaps gerenciados por padding nos itens */
}
.cabecalho { grid-area: header; background: var(--cor-primaria); }
.barra-lateral { grid-area: sidebar; background: var(--cor-fundo); }
.conteudo-principal { grid-area: main; padding: 2rem; }
.rodape { grid-area: footer; background: var(--cor-primaria); }
/* Responsivo */
@media (max-width: 768px) {
.pagina {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main "
"footer";
}
.barra-lateral { display: none; }
}
9.7.2 — Grade de cards responsiva sem media queries¶
O padrão mais elegante do CSS Grid moderno: uma grade que se adapta completamente ao viewport sem uma única media query:
<section class="grade-cards">
<article class="card">...</article>
<article class="card">...</article>
<article class="card">...</article>
<!-- quantos cards forem necessários -->
</section>
.grade-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
padding: 2rem;
}
.card {
display: flex; /* card interno usa Flexbox */
flex-direction: column;
background: white;
border-radius: var(--raio-borda);
box-shadow: var(--sombra-md);
overflow: hidden;
}
.card img {
width: 100%;
height: 200px;
object-fit: cover;
}
.card__corpo {
flex: 1;
padding: 1.5rem;
}
.card__rodape {
padding: 1rem 1.5rem;
border-top: 1px solid #eee;
}
Este padrão — repeat(auto-fit, minmax(280px, 1fr)) — é um dos mais valiosos do CSS moderno. Ele resolve automaticamente: em viewports largos, muitas colunas; em viewports estreitos, menos colunas; em mobile, coluna única. Tudo sem uma linha de media query.
9.7.3 — Layout de revista (magazine layout)¶
Layouts editoriais com elementos de tamanhos variados e posicionamento preciso:
<section class="revista">
<article class="artigo artigo--destaque">Artigo principal</article>
<article class="artigo artigo--secundario-a">Secundário A</article>
<article class="artigo artigo--secundario-b">Secundário B</article>
<article class="artigo artigo--pequeno-a">Pequeno A</article>
<article class="artigo artigo--pequeno-b">Pequeno B</article>
<article class="artigo artigo--pequeno-c">Pequeno C</article>
</section>
.revista {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(3, 250px);
gap: 1rem;
}
/* Artigo principal: ocupa 2 colunas e 2 linhas */
.artigo--destaque {
grid-column: 1 / 3;
grid-row: 1 / 3;
}
/* Secundários: uma coluna, duas linhas */
.artigo--secundario-a {
grid-column: 3 / 4;
grid-row: 1 / 3;
}
.artigo--secundario-b {
grid-column: 4 / 5;
grid-row: 1 / 3;
}
/* Pequenos: uma coluna, uma linha — fluxo automático na linha 3 */
/* (não precisam de posicionamento explícito) */
9.7.4 — Galeria de imagens com células de tamanho variado¶
Uma galeria onde algumas imagens são maiores, usando auto-flow: dense para preencher lacunas:
.galeria {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-rows: 200px;
grid-auto-flow: dense;
gap: 0.5rem;
}
.galeria__item {
overflow: hidden;
border-radius: var(--raio-borda-sm);
}
.galeria__item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 300ms ease;
}
.galeria__item:hover img {
transform: scale(1.05);
}
/* Imagens de destaque */
.galeria__item--largo { grid-column: span 2; }
.galeria__item--alto { grid-row: span 2; }
.galeria__item--grande { grid-column: span 2; grid-row: span 2; }
9.8 — Grid e acessibilidade¶
Vídeo curto explicativo (link será adicionado posteriormente)
9.8.1 — Ordem do DOM vs ordem visual no Grid¶
Assim como no Flexbox, o CSS Grid permite separar a ordem visual dos elementos da sua ordem no DOM — por meio de posicionamento explícito, grid-auto-flow e order. As mesmas implicações de acessibilidade se aplicam:
- Leitores de tela leem o conteúdo na ordem do DOM
- Navegação por teclado segue a ordem do DOM
- Seleção de texto segue a ordem do DOM
Portanto, qualquer reordenação visual que altere a sequência semântica do conteúdo viola o critério WCAG 2.1 1.3.2 — Sequência com Significado (nível A).
/* PROBLEMÁTICO: ordem visual desconectada do DOM */
.artigo-principal { grid-area: destaque; } /* visualmente primeiro */
.artigo-recente { grid-area: topo; } /* visualmente segundo */
/* No DOM, artigo-recente vem antes de artigo-principal —
leitor de tela lê recente antes de principal */
9.8.2 — Boas práticas de reordenação responsiva¶
A reordenação por media queries com grid-template-areas é geralmente segura quando aplicada a componentes onde a ordem não é semanticamente crítica. A regra prática:
Seguro: reordenar componentes de layout estrutural (mover sidebar de baixo para o lado, por exemplo) quando ambas as posições fazem sentido para a leitura.
Problemático: reordenar conteúdo sequencial (etapas, artigos em ordem cronológica, listas de prioridade) onde a posição visual comunica importância ou sequência.
/* SEGURO: sidebar pode estar em qualquer posição */
@media (max-width: 768px) {
.pagina {
grid-template-areas:
"header"
"main " /* main antes da sidebar em mobile */
"sidebar"
"footer";
}
}
/* O conteúdo principal ainda faz sentido independente da posição da sidebar */
/* PROBLEMÁTICO: etapas reordenadas visualmente */
@media (max-width: 768px) {
.processo {
grid-template-areas:
"etapa-3" /* visualmente primeiro em mobile */
"etapa-1"
"etapa-2";
}
}
/* Leitor de tela ainda lê etapa-1, etapa-2, etapa-3 na ordem do DOM */
Solução quando a reordenação é inevitável: reorganize o DOM para corresponder à ordem de leitura desejada e use Grid para a apresentação visual em desktop — não o contrário.
Referências: - MDN — CSS Grid Layout - W3C — CSS Grid Layout Module Level 1 - CSS Tricks — A Complete Guide to Grid - Grid Garden — exercício interativo - Layout Land — Jen Simmons (YouTube)
Atividades — Capítulo 9¶
1. Uma grade tem grid-template-columns: repeat(3, 1fr) em um container de 900px com gap: 30px. Qual é a largura de cada coluna?
2. Qual é a diferença prática entre auto-fill e auto-fit em repeat(auto-fill/auto-fit, minmax(200px, 1fr))?
3. Por que o uso de grid-auto-flow: dense pode criar problemas de acessibilidade?
4. Qual é a vantagem de usar grid-template-areas com media queries em vez de redefinir grid-column e grid-row nos itens?
- GitHub Classroom: Construir uma página de dashboard com: (1) layout de página completo usando
grid-template-areascom responsividade para mobile via media query; (2) grade de cards responsiva comauto-fit+minmax()sem media queries; (3) seção de destaque com posicionamento explícito por linhas (grid-column: span). (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 8 — Layout com Flexbox :material-arrow-right: Ir ao Capítulo 10 — Design Responsivo
Capítulo 10 — Design Responsivo¶
Vídeo curto explicativo (link será adicionado posteriormente)
10.1 — O que é design responsivo¶
Vídeo curto explicativo (link será adicionado posteriormente)
O design responsivo (responsive web design) é a abordagem de desenvolvimento que permite que uma página web se adapte e apresente uma experiência adequada em qualquer dispositivo — independentemente do tamanho da tela, da resolução, da orientação ou das capacidades do navegador. O termo foi cunhado por Ethan Marcotte em um artigo seminal publicado na revista A List Apart em maio de 2010, e rapidamente se tornou o paradigma dominante do desenvolvimento front-end moderno.
A motivação para o design responsivo é direta: ao contrário de uma publicação impressa — que possui dimensões físicas fixas e conhecidas no momento da criação —, uma página web é acessada em um espectro de dispositivos de características radicalmente diferentes. Segundo dados do StatCounter (2025), o tráfego web mobile representa globalmente mais de 60% do total de acessos. No Brasil, esse percentual é ainda mais expressivo, com dispositivos móveis respondendo por aproximadamente 65% das sessões.
10.1.1 — O problema da multiplicidade de dispositivos¶
O desenvolvedor web contemporâneo projeta para um espectro que inclui:
- Smartphones com telas de 320px a 430px de largura
- Tablets entre 600px e 1024px
- Laptops entre 1024px e 1440px
- Monitores widescreen de 1440px a 2560px ou mais
- TVs com navegadores embutidos
- Dispositivos wearable com telas mínimas
- Leitores de tela sem dimensão visual
Antes do design responsivo, a resposta comum a essa diversidade era manter duas versões separadas do site — uma para desktop (www.site.com) e uma para mobile (m.site.com). Essa abordagem gerou problemas graves: duplicação de conteúdo, inconsistência entre versões, custo de manutenção dobrado e ausência de cobertura para o vasto espaço entre os dois extremos.
O design responsivo resolve esse problema com uma única base de código que se adapta fluidamente a qualquer contexto.
10.1.2 — Os três pilares do design responsivo¶
Ethan Marcotte definiu o design responsivo como a combinação de três técnicas fundamentais:
1. Grade fluida (fluid grid): utilizar unidades relativas (%, fr, em, rem) em vez de pixels fixos para dimensionamento e layout — permitindo que a estrutura da página se expanda e contraia proporcionalmente ao viewport.
2. Imagens flexíveis (flexible images): garantir que imagens e outros elementos de mídia nunca ultrapassem os limites do seu container — evitando overflow horizontal em telas pequenas.
3. Media queries: aplicar regras CSS específicas condicionalmente, com base nas características do dispositivo — permitindo ajustes de layout, tipografia e apresentação em breakpoints definidos.
Os Capítulos 8 e 9 já cobriram a grade fluida com Flexbox e Grid. Este capítulo aprofunda as media queries e as técnicas modernas que complementam e às vezes substituem a abordagem clássica de breakpoints.
10.1.3 — Viewport: o que é e por que importa¶
O viewport é a área retangular do navegador onde o conteúdo web é renderizado — em essência, a janela visível da página. Em desktops, o viewport corresponde aproximadamente à área da janela do navegador descontando barras de ferramentas.
Em dispositivos móveis, contudo, existe uma distinção importante entre dois tipos de viewport:
Viewport de layout (layout viewport): a área em que o navegador renderiza a página. Por padrão, a maioria dos navegadores móveis define o layout viewport como 980px de largura — uma herança do design desktop — e depois escala o resultado para caber na tela física. Isso faz com que páginas não otimizadas para mobile apareçam "encolhidas" e ilegíveis.
Viewport visual (visual viewport): a área efetivamente visível na tela do dispositivo, que pode ser menor que o layout viewport quando o usuário dá zoom.
A solução para o layout viewport inflado é a meta tag viewport.
10.1.4 — A meta tag viewport e seu papel no mobile¶
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
Esta declaração, que deve estar presente em todo documento HTML responsivo, instrui o navegador a:
width=device-width: definir o layout viewport com a largura real do dispositivo (em vez dos 980px padrão)initial-scale=1.0: não aplicar zoom inicial — a página é exibida em escala 1:1
Sem essa meta tag, as media queries baseadas em max-width simplesmente não funcionam corretamente em dispositivos móveis — o navegador aplica a versão desktop porque o layout viewport reportado é de 980px, não da largura real do dispositivo.
<!-- Obrigatório em todo projeto responsivo -->
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Página Responsiva</title>
<link rel="stylesheet" href="css/style.css" />
</head>
Referência: MDN — Viewport meta tag
10.2 — Estratégia mobile-first vs desktop-first¶
Vídeo curto explicativo (link será adicionado posteriormente)
A escolha entre escrever CSS para mobile primeiro (mobile-first) ou para desktop primeiro (desktop-first) não é apenas uma preferência estilística — ela define a direção das media queries, a ordem de sobrescrita de estilos e, em última análise, a arquitetura de todo o CSS responsivo do projeto.
10.2.1 — O que significa cada abordagem¶
Mobile-first: os estilos base do CSS são escritos para telas pequenas. Media queries com min-width adicionam progressivamente complexidade para telas maiores:
/* Mobile-first: estilos base para telas pequenas */
.container {
display: flex;
flex-direction: column; /* empilhado em mobile */
padding: 1rem;
}
/* Tablet: a partir de 768px, muda para linha */
@media (min-width: 768px) {
.container {
flex-direction: row;
padding: 2rem;
}
}
/* Desktop: a partir de 1024px, adiciona mais espaço */
@media (min-width: 1024px) {
.container {
max-width: 1200px;
margin: 0 auto;
padding: 3rem;
}
}
Desktop-first: os estilos base são escritos para telas grandes. Media queries com max-width removem ou simplificam para telas menores:
/* Desktop-first: estilos base para telas grandes */
.container {
display: flex;
flex-direction: row;
max-width: 1200px;
margin: 0 auto;
padding: 3rem;
}
/* Tablet: até 1024px, reduz espaço */
@media (max-width: 1024px) {
.container {
padding: 2rem;
}
}
/* Mobile: até 768px, empilha */
@media (max-width: 768px) {
.container {
flex-direction: column;
padding: 1rem;
}
}
10.2.2 — Por que mobile-first é a abordagem recomendada¶
A preferência pelo mobile-first não é arbitrária — ela tem justificativas técnicas, estratégicas e de desempenho:
Desempenho em dispositivos móveis: navegadores móveis baixam e processam todo o CSS antes de renderizar a página. Com desktop-first, um dispositivo móvel processa todos os estilos complexos de desktop e depois os sobrescreve com simplificações — trabalho desnecessário. Com mobile-first, os estilos simples são aplicados primeiro; as media queries de desktop são ignoradas em dispositivos que não as atendem.
Priorização do conteúdo: forçar-se a projetar para a menor tela primeiro obriga o desenvolvedor a identificar o conteúdo verdadeiramente essencial — aquele que merece espaço na tela de 375px. Esta disciplina tende a produzir interfaces mais focadas e objetivas em todos os tamanhos de tela.
Progressão natural: é conceitualmente mais simples adicionar complexidade (colunas, espaçamentos maiores, elementos adicionais) para telas maiores do que remover complexidade para telas menores.
Alinhamento com o mercado: com mobile representando a maioria dos acessos globais, projetar mobile como experiência primária e desktop como aprimoramento é a ordem correta de prioridade.
10.2.3 — Impacto na escrita das media queries¶
A direção da abordagem define diretamente os operadores das media queries:
| Abordagem | Operador | Direção |
|---|---|---|
| Mobile-first | min-width |
Pequeno → Grande (additive) |
| Desktop-first | max-width |
Grande → Pequeno (subtractive) |
/* Mobile-first: cada media query ADICIONA ao anterior */
/* Estilos base → mobile */
.nav { flex-direction: column; }
/* + tablet */
@media (min-width: 768px) {
.nav { flex-direction: row; }
}
/* + desktop */
@media (min-width: 1024px) {
.nav { gap: 2rem; }
}
10.3 — Media queries¶
Vídeo curto explicativo (link será adicionado posteriormente)
As media queries são o mecanismo do CSS que permite aplicar regras condicionalmente com base nas características do dispositivo ou do ambiente de exibição. Elas são o terceiro pilar do design responsivo e a ferramenta mais direta para adaptar layouts a diferentes contextos.
10.3.1 — Sintaxe básica: @media¶
@media tipo-de-midia and (feature: valor) {
/* regras CSS aplicadas somente quando a condição é verdadeira */
}
/* Exemplos */
@media screen and (min-width: 768px) {
.container { max-width: 1200px; }
}
@media print {
.navbar, .sidebar { display: none; }
body { font-size: 12pt; color: black; }
}
A estrutura de uma media query é composta por:
- @media — declaração de início
- Tipo de mídia (opcional) — screen, print, all
- and — operador lógico (quando tipo + feature)
- Feature query entre parênteses — a condição a verificar
10.3.2 — Tipos de mídia¶
| Tipo | Descrição |
|---|---|
all |
Todos os dispositivos (padrão quando omitido) |
screen |
Telas: monitores, smartphones, tablets |
print |
Impressão e pré-visualização de impressão |
speech |
Sintetizadores de voz (leitores de tela) |
/* Estilos específicos para impressão */
@media print {
/* Oculta elementos desnecessários na impressão */
nav, aside, .btn, .cookie-banner {
display: none !important;
}
/* Tipografia adequada para papel */
body {
font-family: Georgia, serif;
font-size: 12pt;
line-height: 1.5;
color: black;
background: white;
}
/* Garante que links sejam identificáveis */
a[href]::after {
content: " (" attr(href) ")";
font-size: 0.8em;
color: #555;
}
/* Evita quebras de página dentro de elementos importantes */
article, figure, table {
page-break-inside: avoid;
}
}
10.3.3 — Feature queries: dimensão, orientação e preferências do usuário¶
Dimensão do viewport
/* min-width: aplica a partir de N pixels (mobile-first) */
@media (min-width: 768px) { /* tablet e acima */ }
@media (min-width: 1024px) { /* desktop e acima */ }
@media (min-width: 1440px) { /* widescreen */ }
/* max-width: aplica até N pixels (desktop-first) */
@media (max-width: 1023px) { /* abaixo de desktop */ }
@media (max-width: 767px) { /* abaixo de tablet = mobile */ }
/* Intervalo: combinação de min e max */
@media (min-width: 768px) and (max-width: 1023px) {
/* apenas tablet */
}
/* Altura do viewport */
@media (min-height: 800px) {
.hero { min-height: 100vh; }
}
Orientação
@media (orientation: portrait) {
/* tela mais alta do que larga — típico de mobile em pé */
.galeria { grid-template-columns: repeat(2, 1fr); }
}
@media (orientation: landscape) {
/* tela mais larga do que alta — mobile deitado, desktop */
.galeria { grid-template-columns: repeat(4, 1fr); }
}
prefers-color-scheme — tema claro ou escuro
Permite adaptar o visual às preferências de tema do sistema operacional do usuário — uma funcionalidade com impacto direto em acessibilidade e conforto visual:
/* Tema claro: padrão */
:root {
--cor-fundo: #F7F5F2;
--cor-texto: #333333;
--cor-primaria: #12243A;
--cor-card: #FFFFFF;
}
/* Tema escuro: aplicado automaticamente quando o SO está em dark mode */
@media (prefers-color-scheme: dark) {
:root {
--cor-fundo: #1a1a2e;
--cor-texto: #e0e0e0;
--cor-primaria: #A8D8EA;
--cor-card: #16213e;
}
/* Com variáveis CSS, toda a paleta muda com apenas estas linhas */
}
Esta abordagem, combinada com variáveis CSS (Capítulo 7, seção 7.12), é o padrão moderno para implementação de tema escuro — sem duplicar regras CSS.
prefers-reduced-motion — respeito por preferências de movimento
Usuários com epilepsia fotossensível, vertigem ou distúrbios vestibulares podem configurar seu sistema operacional para reduzir animações. Esta media query permite respeitar essa preferência:
/* Animações padrão */
.btn {
transition: transform 300ms ease, background-color 200ms ease;
}
.btn:hover {
transform: scale(1.05);
}
.loading-spinner {
animation: girar 1s linear infinite;
}
/* Remove ou simplifica animações quando o usuário prefere menos movimento */
@media (prefers-reduced-motion: reduce) {
.btn {
transition: background-color 200ms ease; /* mantém apenas cor */
}
.btn:hover {
transform: none; /* remove escala */
}
.loading-spinner {
animation: none; /* remove animação contínua */
}
/* Regra global: desabilita todas as transições e animações */
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Conexão com acessibilidade:
prefers-reduced-motioné relevante para o critério WCAG 2.1 2.3.3 — Animação por Interação (nível AAA) e é considerada boa prática mesmo no nível AA. Sua implementação é simples e o impacto para usuários afetados é significativo.
prefers-contrast — contraste elevado
@media (prefers-contrast: more) {
:root {
--cor-texto: #000000;
--cor-fundo: #FFFFFF;
--cor-borda: #000000;
}
.btn {
border: 2px solid currentColor;
}
}
10.3.4 — Operadores lógicos: and, not e or (,)¶
/* and: todas as condições devem ser verdadeiras */
@media screen and (min-width: 768px) and (orientation: landscape) {
/* tela, largura ≥ 768px E orientação paisagem */
}
/* , (vírgula = or): pelo menos uma condição deve ser verdadeira */
@media (max-width: 767px), (orientation: portrait) {
/* mobile OU orientação retrato */
}
/* not: nega a condição */
@media not print {
/* tudo exceto impressão */
}
/* not em feature específica */
@media (not (prefers-color-scheme: dark)) {
/* apenas quando NÃO está em dark mode */
}
10.3.5 — Breakpoints: o que são, como definir e valores comuns¶
Breakpoints são os valores de largura nos quais o layout muda de forma significativa. Não existe um conjunto universalmente "correto" de breakpoints — eles devem emergir do conteúdo e do design, não de modelos de dispositivos específicos.
Valores comuns (referência, não prescrição):
/* Sistema de breakpoints típico — mobile-first */
:root {
/* sm: dispositivos móveis grandes / landscape */
/* @media (min-width: 480px) */
/* md: tablets */
/* @media (min-width: 768px) */
/* lg: laptops / desktop pequeno */
/* @media (min-width: 1024px) */
/* xl: desktop */
/* @media (min-width: 1280px) */
/* 2xl: widescreen */
/* @media (min-width: 1536px) */
}
A abordagem correta: definir breakpoints onde o layout precisa mudar, não onde um dispositivo específico começa. O processo recomendado é redimensionar o navegador lentamente e adicionar um breakpoint quando o layout "quebrar" — quando o texto ficar ilegível, as colunas ficarem estreitas demais ou o espaçamento inadequado.
/* Breakpoints baseados no conteúdo, não em dispositivos */
@media (min-width: 600px) {
/* o layout de coluna única fica inadequado a partir daqui */
.grade { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 900px) {
/* três colunas cabem confortavelmente a partir daqui */
.grade { grid-template-columns: repeat(3, 1fr); }
}
10.3.6 — A sintaxe moderna de range¶
A especificação Media Queries Level 4 introduziu uma sintaxe de intervalo mais legível e expressiva, já suportada pelos navegadores modernos:
/* Sintaxe clássica */
@media (min-width: 768px) and (max-width: 1023px) { }
/* Sintaxe moderna de range — equivalente e mais legível */
@media (768px <= width <= 1023px) { }
@media (width >= 768px) { }
@media (width < 1024px) { }
/* Exemplos */
@media (width >= 768px) {
.container { max-width: 1200px; }
}
@media (600px <= width <= 900px) {
.grade { grid-template-columns: repeat(2, 1fr); }
}
Compatibilidade: a sintaxe de range tem suporte em Chrome 113+, Firefox 63+, Safari 16.4+. Para projetos que precisam suportar navegadores mais antigos, a sintaxe clássica com
min-width/max-widthainda é necessária. Verifique o suporte atual em caniuse.com/css-media-range-syntax.
10.4 — Layout adaptativo¶
Vídeo curto explicativo (link será adicionado posteriormente)
10.4.1 — Mudança de layout com Flexbox em breakpoints¶
/* Mobile-first: empilhado por padrão */
.secao-hero {
display: flex;
flex-direction: column;
gap: 2rem;
padding: 2rem 1rem;
}
.secao-hero__texto { order: 2; } /* texto abaixo da imagem em mobile */
.secao-hero__imagem { order: 1; }
/* Tablet e acima: lado a lado */
@media (min-width: 768px) {
.secao-hero {
flex-direction: row;
align-items: center;
padding: 4rem 2rem;
}
.secao-hero__texto {
flex: 1;
order: 1; /* texto volta à esquerda */
}
.secao-hero__imagem {
flex: 0 0 400px;
order: 2;
}
}
/* Desktop: mais espaçamento */
@media (min-width: 1024px) {
.secao-hero {
padding: 6rem 4rem;
gap: 4rem;
}
}
10.4.2 — Mudança de layout com Grid e grid-template-areas¶
O padrão mais elegante para layouts responsivos complexos — o container muda, os itens permanecem intocados:
/* Mobile: coluna única */
.pagina {
display: grid;
grid-template-areas:
"header"
"main "
"aside "
"footer";
grid-template-rows: auto 1fr auto auto;
}
/* Tablet: sidebar ao lado */
@media (min-width: 768px) {
.pagina {
grid-template-columns: 1fr 240px;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"main aside "
"footer footer";
}
}
/* Desktop: sidebar mais larga */
@media (min-width: 1024px) {
.pagina {
grid-template-columns: 1fr 300px;
max-width: 1400px;
margin: 0 auto;
}
}
/* Itens: não mudam em nenhum breakpoint */
.cabecalho { grid-area: header; }
.conteudo { grid-area: main; }
.lateral { grid-area: aside; }
.rodape { grid-area: footer; }
10.4.3 — Ocultando e revelando elementos por breakpoint¶
/* Ocultar em mobile, mostrar a partir de tablet */
.apenas-desktop {
display: none;
}
@media (min-width: 768px) {
.apenas-desktop {
display: block; /* ou flex, grid, inline, etc. */
}
}
/* Ocultar a partir de tablet (visível apenas em mobile) */
.apenas-mobile {
display: block;
}
@media (min-width: 768px) {
.apenas-mobile {
display: none;
}
}
⚠️ Acessibilidade:
display: noneremove o elemento tanto visualmente quanto da árvore de acessibilidade — leitores de tela não o percebem. Se o conteúdo é importante para acessibilidade mas deve ser ocultado visualmente, use a técnica de visually hidden:.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }Esta classe oculta o elemento visualmente mas o mantém acessível para leitores de tela — usada para rótulos e textos de contexto que só fazem sentido auditivamente.
10.4.4 — Reordenação responsiva com order e suas implicações¶
Como discutido nos Capítulos 8 e 9, order altera apenas a apresentação visual — leitores de tela e navegação por teclado seguem a ordem do DOM. A recomendação é:
Quando a reordenação é para conveniência visual (ex.: imagem antes do texto em mobile), order é aceitável desde que a experiência de leitura faça sentido em qualquer ordem:
/* Imagem aparece visualmente antes do texto em mobile */
.hero__imagem { order: 1; }
.hero__texto { order: 2; }
/* Em desktop, inverte: texto à esquerda, imagem à direita */
@media (min-width: 768px) {
.hero__texto { order: 1; }
.hero__imagem { order: 2; }
}
Quando a ordem tem significado semântico, a solução correta é organizar o DOM na ordem que faz sentido para leitura e usar CSS para o layout visual — não usar order para simular uma ordem diferente.
10.5 — Técnicas modernas de responsividade¶
Vídeo curto explicativo (link será adicionado posteriormente)
As técnicas desta seção representam a evolução do design responsivo além das media queries — abordagens que permitem layouts fluidos e adaptativos sem (ou com menos) breakpoints explícitos.
10.5.1 — Imagens responsivas: max-width e object-fit¶
max-width: 100% — a regra fundamental
A regra mais simples e mais importante para imagens responsivas: nunca deixar uma imagem ultrapassar o seu container:
/* Reset de imagens responsivas — deve estar no CSS global */
img,
video,
canvas,
svg {
display: block;
max-width: 100%;
height: auto; /* mantém proporção */
}
Com max-width: 100%, a imagem ocupa no máximo 100% da largura do seu container — se o container encolher, a imagem encolhe proporcionalmente. Se a imagem for naturalmente menor que o container, ela permanece em seu tamanho original.
object-fit — controle de proporção em containers dimensionados
Quando uma imagem precisa preencher um container com dimensões fixas (como cards de altura igual), object-fit controla como a imagem se comporta:
.card__imagem {
width: 100%;
height: 220px; /* altura fixa */
object-fit: cover; /* preenche o container, pode cortar */
object-position: center top; /* foca no topo da imagem */
}
/* Valores de object-fit */
img {
object-fit: fill; /* estica para preencher — distorce */
object-fit: contain; /* cabe inteira — pode deixar espaço */
object-fit: cover; /* preenche e corta — mais usado */
object-fit: none; /* tamanho original — pode transbordar */
object-fit: scale-down; /* menor entre none e contain */
}
10.5.2 — Tipografia fluida com clamp()¶
A abordagem clássica de tipografia responsiva usa media queries para definir tamanhos em breakpoints:
/* Abordagem clássica: saltos abruptos nos breakpoints */
h1 { font-size: 1.75rem; }
@media (min-width: 768px) { h1 { font-size: 2.25rem; } }
@media (min-width: 1024px) { h1 { font-size: 3rem; } }
A função clamp() permite tipografia que escala continuamente entre um mínimo e um máximo, sem saltos:
/* clamp(mínimo, preferido, máximo) */
h1 {
font-size: clamp(1.75rem, 4vw, 3rem);
/* Em 320px: 1.75rem (mínimo ativo: 4vw = 12.8px < 28px) */
/* Em 700px: ~1.75rem (4vw ≈ 28px = 1.75rem) */
/* Em 900px: ~2.25rem (4vw = 36px) */
/* Em 1200px: 3rem (máximo ativo: 4vw = 48px > 48px) */
}
h2 { font-size: clamp(1.375rem, 3vw, 2.25rem); }
h3 { font-size: clamp(1.125rem, 2.5vw, 1.75rem); }
p { font-size: clamp(1rem, 1.5vw, 1.125rem); }
Calculando o valor preferido para clamp()
O valor intermediário de clamp() geralmente usa vw (ou uma combinação de vw + rem) para criar uma escala suave. Uma fórmula útil para calcular o valor preferido:
valor-vw = (tamanho-max - tamanho-min) / (viewport-max - viewport-min) × 100
Ferramentas online como Fluid Type Scale (utopia.fyi) geram automaticamente escalas tipográficas fluidas com clamp().
10.5.3 — Espaçamento fluido com clamp()¶
O mesmo princípio se aplica a margens, paddings e gaps — criando espaçamentos que escalam suavemente com o viewport:
:root {
/* Espaçamentos fluidos */
--espaco-sm: clamp(0.75rem, 2vw, 1rem);
--espaco-md: clamp(1rem, 3vw, 2rem);
--espaco-lg: clamp(2rem, 5vw, 4rem);
--espaco-xl: clamp(3rem, 8vw, 6rem);
}
.secao {
padding: var(--espaco-xl) var(--espaco-lg);
}
.grade {
gap: var(--espaco-md);
}
h1 {
margin-bottom: var(--espaco-sm);
}
Esta abordagem, combinada com variáveis CSS, cria um sistema de espaçamento que se adapta a qualquer viewport sem uma única media query para espaçamento.
10.5.4 — Layout fluido com unidades relativas¶
Layouts baseados em % e fr são intrinsecamente fluidos — eles se adaptam ao viewport sem media queries. A combinação de unidades relativas com limites (max-width, min-width, clamp()) cria layouts que funcionam em toda a faixa de tamanhos:
/* Container fluido com largura máxima */
.container {
width: 90%; /* fluido */
max-width: 1200px; /* nunca passa disso */
margin: 0 auto; /* centralizado */
}
/* Grade de cards fluida — sem media queries */
.grade-fluida {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
gap: clamp(1rem, 3vw, 2rem);
}
/* min(280px, 100%): em viewports < 280px, o card ocupa 100% em vez de 280px */
O padrão min(280px, 100%) é um refinamento importante: em vez de minmax(280px, 1fr) — que pode forçar overflow horizontal em viewports menores que 280px —, min(280px, 100%) garante que o item nunca seja maior que seu container:
/* Mais robusto que minmax(280px, 1fr) */
.grade-robusta {
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
}
10.5.5 — Container queries: responsividade baseada no container¶
As container queries são uma das adições mais significativas ao CSS nos últimos anos — suportadas em todos os navegadores modernos desde 2023. Elas permitem que um componente se adapte ao tamanho do seu container imediato, não do viewport global.
Por que container queries importam
O problema das media queries tradicionais é que elas são globais: um componente de card não sabe em qual contexto está sendo usado. O mesmo card pode aparecer numa coluna larga na página inicial e numa coluna estreita na sidebar — mas com media queries, ele só consegue reagir ao viewport, não ao seu container real.
/* Media query tradicional: reage ao viewport */
@media (min-width: 768px) {
.card { flex-direction: row; }
}
/* Problema: o card usa layout de linha mesmo quando está numa coluna estreita */
Com container queries, o card reage ao seu próprio container:
/* 1. Definir o container de referência */
.card-wrapper {
container-type: inline-size; /* monitora a largura inline */
container-name: card; /* nome opcional para referência */
}
/* 2. Estilos base do card (mobile-first) */
.card {
display: flex;
flex-direction: column;
}
/* 3. Container query: quando o container tiver ≥ 400px */
@container card (min-width: 400px) {
.card {
flex-direction: row;
align-items: center;
}
.card__imagem {
flex: 0 0 160px;
}
}
Sintaxe completa:
/* container-type: define como o container é medido */
.wrapper {
container-type: inline-size; /* monitora largura (mais comum) */
container-type: size; /* monitora largura E altura */
container-type: normal; /* padrão: não é container */
}
/* @container: a query em si */
@container (min-width: 600px) {
/* sem nome: aplica ao container mais próximo */
}
@container card (min-width: 400px) {
/* com nome: aplica ao container chamado "card" */
}
/* Unidades de container: cqi, cqb, cqw, cqh */
.card__titulo {
font-size: clamp(1rem, 5cqi, 1.5rem);
/* 5cqi = 5% da largura inline do container */
}
Exemplo prático: card adaptativo
<!-- Mesmo componente em dois contextos diferentes -->
<div class="grade-principal">
<div class="card-wrapper">
<article class="card">...</article>
</div>
</div>
<aside class="sidebar">
<div class="card-wrapper">
<article class="card">...</article> <!-- mesmo HTML -->
</div>
</aside>
.card-wrapper {
container-type: inline-size;
}
/* Card: layout coluna por padrão */
.card {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
}
/* Card: layout linha quando container ≥ 380px */
@container (min-width: 380px) {
.card {
flex-direction: row;
align-items: center;
}
.card__imagem {
flex: 0 0 120px;
height: 120px;
}
}
/* Card: mais espaçamento quando container ≥ 600px */
@container (min-width: 600px) {
.card {
padding: 1.5rem;
gap: 1.5rem;
}
.card__imagem {
flex: 0 0 200px;
height: 160px;
}
}
Na grade principal (container largo), o card usa layout horizontal com imagem grande. Na sidebar (container estreito), o mesmo card usa layout vertical — automaticamente, sem media queries baseadas no viewport.
Referências: - MDN — Container queries - Can I Use — Container queries - Una Kravets — Container queries explained
Referências gerais do capítulo: - MDN — Design responsivo - MDN — Media queries - Ethan Marcotte — Responsive Web Design (artigo original) - Utopia — Fluid type & space scales - Every Layout — Layouts sem media queries
Atividades — Capítulo 10¶
1. Por que a meta tag <meta name="viewport" content="width=device-width, initial-scale=1.0"> é obrigatória em páginas responsivas?
2. Qual é a vantagem técnica da abordagem mobile-first em relação ao desktop-first para dispositivos móveis?
3. Qual é a principal vantagem das container queries em relação às media queries tradicionais para componentes reutilizáveis como cards?
4. O valor font-size: clamp(1rem, 4vw, 2.5rem) em um viewport de 400px resulta em qual tamanho (considerando 1rem = 16px)?
- GitHub Classroom: Tornar o projeto do bimestre completamente responsivo aplicando: meta tag viewport, estratégia mobile-first, ao menos três breakpoints com media queries, tipografia fluida com
clamp()nos títulos, eprefers-color-schemepara suporte a tema escuro via variáveis CSS. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 9 — Layout com Grid :material-arrow-right: Ir ao Capítulo 11 — Variáveis CSS e Design System
Capítulo 11 — Variáveis CSS e Design System¶
Vídeo curto explicativo (link será adicionado posteriormente)
11.1 — O que é um Design System¶
Vídeo curto explicativo (link será adicionado posteriormente)
Um Design System é um conjunto de padrões, componentes, diretrizes e ferramentas que governam a linguagem visual e de interação de um produto digital — ou de uma família de produtos. Mais do que uma coleção de elementos visuais, um Design System é uma infraestrutura de design e desenvolvimento compartilhada: um contrato entre designers e desenvolvedores que estabelece como os elementos da interface devem ser construídos, nomeados, documentados e reutilizados de forma consistente ao longo do tempo e entre equipes.
A motivação para a existência de Design Systems é diretamente rastreável a problemas recorrentes em projetos de software de médio e grande porte: inconsistências visuais entre telas de uma mesma aplicação, retrabalho na implementação de componentes semelhantes, dificuldade de manutenção quando regras visuais mudam, e ausência de vocabulário comum entre as equipes de design e desenvolvimento. O Design System resolve esses problemas ao centralizar as decisões de design em uma fonte única de verdade (single source of truth).
11.1.1 — Definição e propósito¶
Nathan Curtis, um dos pesquisadores mais influentes na área, define Design System como "o conjunto completo de padrões de design, documentação e princípios, juntamente com o kit de ferramentas (padrões de UI, biblioteca de código) para alcançar esses padrões". Esta definição aponta para três dimensões interdependentes:
Dimensão visual: tokens de design (cores, tipografia, espaçamento, sombras, bordas), princípios de composição e hierarquia visual.
Dimensão de componentes: biblioteca de componentes reutilizáveis — botões, formulários, cards, navegação, modais — com variantes documentadas e comportamentos definidos.
Dimensão de documentação: diretrizes de uso, exemplos de aplicação correta e incorreta, princípios de acessibilidade, guias de contribuição para equipes que mantêm e evoluem o sistema.
O propósito central é a consistência com eficiência: garantir que elementos similares sejam tratados de forma idêntica em toda a aplicação, sem que cada desenvolvedor precise tomar as mesmas decisões visuais do zero.
11.1.2 — Design System vs biblioteca de componentes vs style guide¶
Estes três termos são frequentemente usados de forma intercambiável no mercado, o que gera confusão. A distinção conceitual é precisa:
Style guide: documento — frequentemente estático — que descreve a identidade visual de uma marca ou produto: paleta de cores, tipografia, logotipo e suas regras de uso. É a camada mais superficial e não necessariamente inclui código.
Biblioteca de componentes: coleção de componentes de interface implementados em código (HTML/CSS/JavaScript ou um framework específico como React). Foca na implementação, não necessariamente na documentação de princípios ou no porquê das decisões.
Design System: o conjunto mais abrangente — inclui os tokens de design, a biblioteca de componentes, o style guide e a documentação de princípios, padrões de acessibilidade e diretrizes de contribuição. É um sistema vivo, mantido como produto por uma equipe.
Style Guide ⊂ Biblioteca de Componentes ⊂ Design System
Para os fins deste capítulo, construiremos o núcleo técnico de um Design System: os tokens de design implementados como variáveis CSS e uma biblioteca mínima de componentes reutilizáveis.
11.1.3 — Por que Design Systems importam para Sistemas de Informação¶
A relevância dos Design Systems para profissionais de Sistemas de Informação vai além do desenvolvimento front-end:
Sistemas corporativos e dashboards — os produtos mais comuns em projetos de SI — são caracterizados por grande quantidade de telas com elementos repetitivos: tabelas de dados, formulários de cadastro, painéis de controle, relatórios. Sem um sistema de componentes, cada tela é implementada de forma ad hoc, acumulando inconsistências e duplicações.
Manutenção a longo prazo — sistemas de informação frequentemente têm ciclos de vida de décadas. Um Design System bem estruturado permite que mudanças visuais globais (como uma nova identidade corporativa ou uma migração para tema escuro) sejam aplicadas alterando tokens, não caçando instâncias espalhadas por centenas de arquivos CSS.
Trabalho em equipe — projetos de SI são desenvolvidos por equipes, frequentemente com rotatividade de membros. Um Design System documentado reduz o tempo de onboarding e garante que novos membros sigam os mesmos padrões sem depender de conhecimento tácito.
Acessibilidade sistemática — incorporar requisitos de acessibilidade (contrastes mínimos, tamanhos de alvo, estados de foco) nos próprios componentes do Design System garante que toda interface construída com eles herde essas propriedades — em vez de depender da memória de cada desenvolvedor em cada implementação.
11.1.4 — Exemplos reais: Material Design, Fluent, Carbon e Radix¶
Analisar Design Systems de organizações de referência é uma forma eficaz de compreender sua estrutura e ambição:
Material Design (Google) — m3.material.io O Design System mais influente da última década. Sua terceira versão (Material You / M3) introduziu o conceito de color tokens dinâmicos gerados a partir de uma cor-semente, com suporte nativo a tema claro/escuro. É a base visual do ecossistema Android e de produtos Google como Gmail, Maps e Docs.
Fluent Design System (Microsoft) — fluent2.microsoft.design Governa a interface de produtos Microsoft: Windows, Office, Teams, Azure. Notável pela sua implementação de tokens em múltiplas camadas (global → alias → component) e pela documentação extensiva de padrões de acessibilidade baseados nas WCAG.
Carbon Design System (IBM) — carbondesignsystem.com Design System de código aberto amplamente adotado em sistemas corporativos e enterprise. Particularmente relevante para SI por sua ênfase em componentes de dados: tabelas, gráficos, formulários complexos e dashboards.
Radix UI — radix-ui.com Design System focado em acessibilidade, com componentes headless (sem estilo predefinido) que implementam padrões WAI-ARIA corretamente. Usado como base por Shadcn/UI e outros sistemas populares.
Exercício de análise: acesse tokens.studio e examine como tokens de design são estruturados em projetos reais. Compare a hierarquia de tokens do Material Design (disponível em github.com/material-components) com a do Carbon Design System.
11.2 — Variáveis CSS como fundação do sistema¶
Vídeo curto explicativo (link será adicionado posteriormente)
11.2.1 — Revisão: Custom Properties no contexto de sistemas¶
As variáveis CSS (Custom Properties), introduzidas no Capítulo 7 (seção 7.12), são o mecanismo técnico que torna possível implementar um Design System diretamente em CSS puro — sem dependência de pré-processadores como Sass ou de ferramentas de build. Sua capacidade de herança, escopo e modificação em tempo de execução (via JavaScript ou media queries) as torna significativamente mais poderosas do que variáveis de pré-processadores.
No contexto de um Design System, variáveis CSS cumprem um papel que vai além da conveniência de evitar repetição: elas implementam o conceito de token de design — a unidade atômica do sistema de design.
11.2.2 — Tokens de design: o que são e por que existem¶
Tokens de design são as decisões de design armazenadas como pares nome-valor. O conceito foi formalizado pela equipe do Salesforce Lightning Design System em 2014 e tornou-se o padrão da indústria para representar decisões visuais de forma portável, independente de tecnologia.
Um token não é apenas uma variável com um valor — é um nome com significado semântico que representa uma decisão de design específica:
/* Isso NÃO é um token — é apenas um valor nomeado */
--azul: #0057B8;
/* Isso É um token — representa uma decisão semântica */
--cor-interativa-padrao: #0057B8;
/* Significa: "a cor padrão de elementos interativos neste sistema" */
A diferença é sutil mas fundamental: quando a decisão de design muda — por exemplo, a cor interativa passa a ser verde em vez de azul —, o token semântico --cor-interativa-padrao é atualizado em um único lugar, e todos os componentes que o referenciam refletem a mudança automaticamente.
11.2.3 — Hierarquia de tokens: primitivos, semânticos e de componente¶
A arquitetura de tokens mais robusta organiza os valores em três camadas hierárquicas, um padrão adotado por Material Design, Fluent e Carbon:
Camada 1 — Tokens primitivos (Global Tokens)
São os valores brutos do sistema — todas as cores, tamanhos e pesos disponíveis, sem qualquer intenção semântica. Funcionam como a paleta completa de possibilidades:
:root {
/* Escala de azul */
--azul-50: #EFF6FF;
--azul-100: #DBEAFE;
--azul-200: #BFDBFE;
--azul-300: #93C5FD;
--azul-400: #60A5FA;
--azul-500: #3B82F6;
--azul-600: #2563EB;
--azul-700: #1D4ED8;
--azul-800: #1E40AF;
--azul-900: #1E3A8A;
/* Escala de cinza */
--cinza-50: #F9FAFB;
--cinza-100: #F3F4F6;
--cinza-200: #E5E7EB;
--cinza-300: #D1D5DB;
--cinza-400: #9CA3AF;
--cinza-500: #6B7280;
--cinza-600: #4B5563;
--cinza-700: #374151;
--cinza-800: #1F2937;
--cinza-900: #111827;
/* Escala tipográfica */
--tamanho-xs: 0.75rem;
--tamanho-sm: 0.875rem;
--tamanho-base: 1rem;
--tamanho-lg: 1.125rem;
--tamanho-xl: 1.25rem;
--tamanho-2xl: 1.5rem;
--tamanho-3xl: 1.875rem;
--tamanho-4xl: 2.25rem;
/* Escala de espaçamento */
--espaco-1: 0.25rem;
--espaco-2: 0.5rem;
--espaco-3: 0.75rem;
--espaco-4: 1rem;
--espaco-6: 1.5rem;
--espaco-8: 2rem;
--espaco-12: 3rem;
--espaco-16: 4rem;
}
Camada 2 — Tokens semânticos (Alias Tokens)
Referenciam tokens primitivos e atribuem intenção semântica. Esta é a camada que conecta decisões de design a propósitos de interface:
:root {
/* Cores semânticas — referenciam primitivos */
--cor-primaria: var(--azul-700);
--cor-primaria-hover: var(--azul-800);
--cor-primaria-suave: var(--azul-100);
--cor-texto-padrao: var(--cinza-900);
--cor-texto-secundario: var(--cinza-600);
--cor-texto-desabilitado: var(--cinza-400);
--cor-texto-inverso: var(--cinza-50);
--cor-fundo-pagina: var(--cinza-50);
--cor-fundo-card: #FFFFFF;
--cor-fundo-sutil: var(--cinza-100);
--cor-borda-padrao: var(--cinza-200);
--cor-borda-forte: var(--cinza-400);
--cor-sucesso: #16A34A;
--cor-sucesso-suave: #DCFCE7;
--cor-aviso: #D97706;
--cor-aviso-suave: #FEF3C7;
--cor-erro: #DC2626;
--cor-erro-suave: #FEE2E2;
--cor-informacao: var(--azul-600);
--cor-informacao-suave: var(--azul-100);
/* Tipografia semântica */
--fonte-corpo: 'Inter', system-ui, sans-serif;
--fonte-titulo: 'Inter', system-ui, sans-serif;
--fonte-codigo: 'Fira Code', 'Cascadia Code', monospace;
--tamanho-corpo: var(--tamanho-base);
--tamanho-label: var(--tamanho-sm);
--tamanho-caption: var(--tamanho-xs);
--peso-regular: 400;
--peso-medio: 500;
--peso-semibold: 600;
--peso-bold: 700;
--altura-linha-corpo: 1.6;
--altura-linha-titulo: 1.2;
--altura-linha-codigo: 1.5;
/* Bordas semânticas */
--raio-sm: 4px;
--raio-md: 8px;
--raio-lg: 12px;
--raio-xl: 16px;
--raio-circulo: 9999px;
/* Sombras semânticas */
--sombra-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--sombra-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--sombra-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--sombra-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
/* Transições semânticas */
--transicao-rapida: 150ms ease;
--transicao-padrao: 200ms ease;
--transicao-lenta: 300ms ease;
/* Z-index semântico */
--z-base: 0;
--z-elevado: 10;
--z-sticky: 100;
--z-overlay: 1000;
--z-modal: 1100;
--z-toast: 1200;
}
Camada 3 — Tokens de componente (Component Tokens)
Específicos de cada componente — referenciam tokens semânticos e permitem personalização granular sem quebrar o sistema:
/* Tokens do componente Button */
:root {
--btn-padding-v: var(--espaco-2);
--btn-padding-h: var(--espaco-4);
--btn-raio: var(--raio-md);
--btn-tamanho-fonte: var(--tamanho-base);
--btn-peso-fonte: var(--peso-semibold);
--btn-transicao: var(--transicao-padrao);
/* Variante primária */
--btn-primario-fundo: var(--cor-primaria);
--btn-primario-texto: var(--cor-texto-inverso);
--btn-primario-hover: var(--cor-primaria-hover);
/* Variante secundária */
--btn-secundario-fundo: transparent;
--btn-secundario-texto: var(--cor-primaria);
--btn-secundario-borda: var(--cor-primaria);
/* Variante de perigo */
--btn-perigo-fundo: var(--cor-erro);
--btn-perigo-texto: var(--cor-texto-inverso);
}
/* Tokens do componente Card */
:root {
--card-fundo: var(--cor-fundo-card);
--card-raio: var(--raio-lg);
--card-sombra: var(--sombra-md);
--card-padding: var(--espaco-6);
--card-borda: 1px solid var(--cor-borda-padrao);
}
11.2.4 — Nomenclatura de tokens: convenções e boas práticas¶
A nomenclatura consistente é um dos aspectos mais importantes de um Design System — nomes ruins tornam o sistema difícil de aprender e usar. Algumas convenções amplamente adotadas:
Estrutura de nome: --[categoria]-[propriedade]-[variante]-[estado]
/* Exemplos seguindo a convenção */
--cor-texto-primario /* categoria: cor | propriedade: texto | variante: primario */
--cor-fundo-card-hover /* + estado: hover */
--tamanho-fonte-titulo-lg /* + variante de tamanho */
--espaco-padding-btn-sm /* espaçamento específico de componente */
Princípios de nomenclatura:
- Semântico, não descritivo:
--cor-primariaé melhor que--azul— o nome descreve o propósito, não o valor - Escalável:
--espaco-4(múltiplo de 4) é melhor que--espaco-medio— permite adicionar--espaco-5sem quebrar a semântica - Predizível: seguir um padrão consistente permite que desenvolvedores adivinhem nomes corretos sem consultar documentação
- Kebab-case: usar hífens (
--cor-fundo-card), nunca underscores ou camelCase
11.3 — Paleta de cores sistemática¶
Vídeo curto explicativo (link será adicionado posteriormente)
11.3.1 — Construindo escalas de cor com HSL¶
O sistema de cor HSL (Hue, Saturation, Lightness) é o mais adequado para construir escalas de cor sistemáticas porque permite criar variações mantendo identidade visual: fixar matiz (hue) e saturação enquanto varia a luminosidade produz uma família de cores coerente.
:root {
/* Escala de azul construída com HSL
Matiz fixo: 217° | Saturação fixo: 91% | Luminosidade variável */
--azul-50: hsl(217, 91%, 97%);
--azul-100: hsl(217, 91%, 92%);
--azul-200: hsl(217, 91%, 84%);
--azul-300: hsl(217, 91%, 74%);
--azul-400: hsl(217, 91%, 62%);
--azul-500: hsl(217, 91%, 52%); /* cor base */
--azul-600: hsl(217, 91%, 42%);
--azul-700: hsl(217, 91%, 34%);
--azul-800: hsl(217, 91%, 26%);
--azul-900: hsl(217, 91%, 18%);
--azul-950: hsl(217, 91%, 12%);
}
Esta abordagem garante que todas as variações da mesma cor mantenham coerência visual — a escala de azul-50 ao azul-950 é reconhecível como uma família.
Construindo múltiplas escalas:
:root {
/* Escala de cinza neutro */
--cinza-50: hsl(220, 14%, 97%);
--cinza-100: hsl(220, 14%, 94%);
--cinza-500: hsl(220, 9%, 46%);
--cinza-900: hsl(220, 26%, 14%);
/* Escala de verde para feedback de sucesso */
--verde-50: hsl(138, 76%, 97%);
--verde-100: hsl(141, 79%, 91%);
--verde-500: hsl(142, 71%, 45%);
--verde-700: hsl(142, 64%, 29%);
/* Escala de vermelho para feedback de erro */
--vermelho-50: hsl(0, 86%, 97%);
--vermelho-100: hsl(0, 93%, 94%);
--vermelho-500: hsl(0, 84%, 60%);
--vermelho-700: hsl(0, 74%, 42%);
/* Escala de amarelo para avisos */
--amarelo-50: hsl(48, 100%, 96%);
--amarelo-100: hsl(48, 96%, 89%);
--amarelo-500: hsl(38, 92%, 50%);
--amarelo-700: hsl(32, 81%, 29%);
}
11.3.2 — Tokens semânticos de cor¶
Com a paleta primitiva definida, os tokens semânticos mapeiam propósitos de interface a valores concretos:
:root {
/* Cores de ação e marca */
--cor-primaria: var(--azul-700);
--cor-primaria-hover: var(--azul-800);
--cor-primaria-ativa: var(--azul-900);
--cor-primaria-suave: var(--azul-100);
--cor-primaria-borda: var(--azul-300);
/* Cores de feedback */
--cor-sucesso: var(--verde-700);
--cor-sucesso-fundo: var(--verde-50);
--cor-sucesso-borda: var(--verde-100);
--cor-sucesso-texto: var(--verde-700);
--cor-aviso: var(--amarelo-700);
--cor-aviso-fundo: var(--amarelo-50);
--cor-aviso-borda: var(--amarelo-100);
--cor-erro: var(--vermelho-700);
--cor-erro-fundo: var(--vermelho-50);
--cor-erro-borda: var(--vermelho-100);
--cor-erro-texto: var(--vermelho-700);
--cor-informacao: var(--azul-700);
--cor-informacao-fundo: var(--azul-50);
--cor-informacao-borda: var(--azul-100);
/* Cores de superfície */
--cor-fundo-pagina: var(--cinza-50);
--cor-fundo-card: #FFFFFF;
--cor-fundo-sutil: var(--cinza-100);
--cor-fundo-forte: var(--cinza-200);
/* Cores de texto */
--cor-texto-padrao: var(--cinza-900);
--cor-texto-secundario: var(--cinza-600);
--cor-texto-sutil: var(--cinza-400);
--cor-texto-desabilitado: var(--cinza-300);
--cor-texto-inverso: #FFFFFF;
--cor-texto-link: var(--azul-700);
--cor-texto-link-hover: var(--azul-800);
/* Cores de borda */
--cor-borda-sutil: var(--cinza-100);
--cor-borda-padrao: var(--cinza-200);
--cor-borda-forte: var(--cinza-400);
--cor-borda-foco: var(--azul-500);
}
11.3.3 — Tema claro e escuro com tokens semânticos¶
A arquitetura de tokens semânticos é o que torna o tema escuro elegante e manutenível. Com tokens bem definidos, alternar entre temas requer apenas redefinir os tokens semânticos — os componentes não precisam ser alterados:
/* Tema claro: padrão */
:root,
[data-tema="claro"] {
--cor-fundo-pagina: var(--cinza-50);
--cor-fundo-card: #FFFFFF;
--cor-fundo-sutil: var(--cinza-100);
--cor-texto-padrao: var(--cinza-900);
--cor-texto-secundario: var(--cinza-600);
--cor-borda-padrao: var(--cinza-200);
--cor-primaria: var(--azul-700);
--cor-primaria-hover: var(--azul-800);
--sombra-card: var(--sombra-md);
}
/* Tema escuro: preferência do sistema */
@media (prefers-color-scheme: dark) {
:root {
--cor-fundo-pagina: hsl(220, 26%, 10%);
--cor-fundo-card: hsl(220, 22%, 15%);
--cor-fundo-sutil: hsl(220, 18%, 18%);
--cor-texto-padrao: var(--cinza-100);
--cor-texto-secundario: var(--cinza-400);
--cor-borda-padrao: hsl(220, 15%, 25%);
--cor-primaria: var(--azul-400);
--cor-primaria-hover: var(--azul-300);
--sombra-card: 0 4px 6px -1px rgb(0 0 0 / 0.4);
}
}
/* Tema escuro: classe manual (para toggle via JavaScript) */
[data-tema="escuro"] {
--cor-fundo-pagina: hsl(220, 26%, 10%);
--cor-fundo-card: hsl(220, 22%, 15%);
/* ... mesmas redefinições */
}
// Toggle de tema via JavaScript
const btn = document.querySelector('.btn-tema');
btn.addEventListener('click', () => {
const atual = document.documentElement.dataset.tema;
document.documentElement.dataset.tema = atual === 'escuro' ? 'claro' : 'escuro';
});
11.3.4 — Contraste e acessibilidade na paleta¶
A construção da paleta deve incorporar verificações de contraste desde o início — não como ajuste posterior. As WCAG 2.1 exigem razões mínimas de contraste de 4,5:1 para texto normal e 3:1 para texto grande (nível AA).
/* Pares de cor com razão de contraste verificada */
/* --cor-texto-padrao sobre --cor-fundo-pagina */
/* cinza-900 (#111827) sobre cinza-50 (#F9FAFB): ~17:1 ✅ AAA */
/* --cor-texto-secundario sobre --cor-fundo-pagina */
/* cinza-600 (#4B5563) sobre cinza-50: ~7:1 ✅ AAA */
/* --cor-texto-inverso sobre --cor-primaria */
/* Branco sobre azul-700 (#1D4ED8): ~5.7:1 ✅ AA */
/* ⚠️ Verificar sempre: */
/* cinza-400 (#9CA3AF) sobre branco: ~2.5:1 ❌ — apenas para texto decorativo */
Ferramenta: use o WebAIM Contrast Checker ou o painel de cores do Chrome DevTools para verificar cada par de cor antes de adicioná-lo ao sistema.
11.4 — Tipografia sistemática¶
Vídeo curto explicativo (link será adicionado posteriormente)
11.4.1 — Escala tipográfica: racional e modular¶
Uma escala tipográfica é um conjunto de tamanhos de fonte que seguem uma razão matemática, produzindo uma progressão harmônica e visualmente coerente. A escala mais comum na Web é baseada em uma razão modular — cada tamanho é o anterior multiplicado por uma constante.
Razões modulares comuns:
| Razão | Nome | Exemplo (base 1rem) |
|---|---|---|
| 1.125 | Major Second | 1, 1.125, 1.266, 1.424... |
| 1.200 | Minor Third | 1, 1.2, 1.44, 1.728... |
| 1.250 | Major Third | 1, 1.25, 1.563, 1.953... |
| 1.333 | Perfect Fourth | 1, 1.333, 1.777, 2.369... |
| 1.500 | Perfect Fifth | 1, 1.5, 2.25, 3.375... |
/* Escala com razão 1.25 (Major Third) — equilibrada para interfaces */
:root {
--tamanho-xs: 0.64rem; /* 0.8 × 0.8rem */
--tamanho-sm: 0.8rem; /* base × 0.8 */
--tamanho-base: 1rem; /* 16px — base da escala */
--tamanho-md: 1.25rem; /* base × 1.25 */
--tamanho-lg: 1.563rem; /* base × 1.25² */
--tamanho-xl: 1.953rem; /* base × 1.25³ */
--tamanho-2xl: 2.441rem; /* base × 1.25⁴ */
--tamanho-3xl: 3.052rem; /* base × 1.25⁵ */
}
Ferramenta: Modular Scale e Utopia permitem gerar e visualizar escalas tipográficas modulares interativamente.
11.4.2 — Tokens de fonte¶
:root {
/* Famílias */
--fonte-sem-serifa: 'Inter', 'Helvetica Neue', Arial, sans-serif;
--fonte-serifa: 'Merriweather', Georgia, 'Times New Roman', serif;
--fonte-codigo: 'Fira Code', 'Cascadia Code', Consolas, monospace;
/* Tamanhos — escala sistemática */
--fonte-xs: clamp(0.64rem, 0.6rem + 0.2vw, 0.72rem);
--fonte-sm: clamp(0.8rem, 0.75rem + 0.25vw, 0.875rem);
--fonte-base: clamp(1rem, 0.95rem + 0.25vw, 1.0625rem);
--fonte-md: clamp(1.125rem, 1rem + 0.625vw, 1.375rem);
--fonte-lg: clamp(1.25rem, 1rem + 1.25vw, 1.75rem);
--fonte-xl: clamp(1.5rem, 1rem + 2.5vw, 2.25rem);
--fonte-2xl: clamp(1.875rem, 1rem + 4.375vw, 3rem);
--fonte-3xl: clamp(2.25rem, 1rem + 6.25vw, 3.75rem);
/* Pesos */
--peso-fino: 100;
--peso-leve: 300;
--peso-regular: 400;
--peso-medio: 500;
--peso-semibold: 600;
--peso-bold: 700;
--peso-extrabold: 800;
--peso-black: 900;
/* Alturas de linha */
--linha-apertada: 1.1;
--linha-compacta: 1.3;
--linha-normal: 1.5;
--linha-relaxada: 1.6;
--linha-solta: 1.8;
/* Espaçamento entre letras */
--tracking-apertado: -0.05em;
--tracking-normal: 0;
--tracking-largo: 0.05em;
--tracking-muito-largo: 0.1em;
}
11.4.3 — Escala tipográfica fluida com clamp()¶
A integração de clamp() nos tokens de fonte (demonstrada acima) cria uma tipografia que escala suavemente entre dispositivos — sem media queries para tamanhos de fonte. Os valores de clamp() seguem a lógica:
clamp(tamanho-mobile, valor-fluido, tamanho-desktop)
O valor fluido é calculado para que a transição entre os extremos seja linear e proporcional ao viewport. A ferramenta Utopia automatiza esses cálculos.
11.4.4 — Hierarquia tipográfica aplicada¶
Com os tokens definidos, a hierarquia tipográfica da aplicação é implementada de forma centralizada:
/* Estilos base usando tokens semânticos */
body {
font-family: var(--fonte-sem-serifa);
font-size: var(--fonte-base);
font-weight: var(--peso-regular);
line-height: var(--linha-relaxada);
color: var(--cor-texto-padrao);
}
h1 {
font-size: var(--fonte-3xl);
font-weight: var(--peso-bold);
line-height: var(--linha-apertada);
letter-spacing: var(--tracking-apertado);
color: var(--cor-texto-padrao);
}
h2 {
font-size: var(--fonte-2xl);
font-weight: var(--peso-bold);
line-height: var(--linha-compacta);
}
h3 {
font-size: var(--fonte-xl);
font-weight: var(--peso-semibold);
line-height: var(--linha-compacta);
}
h4 {
font-size: var(--fonte-lg);
font-weight: var(--peso-semibold);
}
p {
font-size: var(--fonte-base);
line-height: var(--linha-relaxada);
max-width: 70ch; /* limita a largura de leitura: ~70 caracteres */
}
small, .caption {
font-size: var(--fonte-sm);
color: var(--cor-texto-secundario);
}
code {
font-family: var(--fonte-codigo);
font-size: 0.9em;
background-color: var(--cor-fundo-sutil);
padding: 0.15em 0.4em;
border-radius: var(--raio-sm);
}
11.5 — Espaçamento sistemático¶
Vídeo curto explicativo (link será adicionado posteriormente)
11.5.1 — Sistemas de espaçamento: base-4 e base-8¶
Um sistema de espaçamento consistente é o que diferencia uma interface visualmente coerente de uma coleção de elementos com espaçamentos arbitrários. Os dois sistemas mais adotados na indústria são baseados em múltiplos de 4px ou 8px.
Sistema base-4: todos os valores de espaçamento são múltiplos de 4px (0.25rem). Oferece granularidade maior — adequado para interfaces densas como dashboards corporativos.
Sistema base-8: todos os valores são múltiplos de 8px (0.5rem). Mais restritivo e opinativo — produz interfaces com ritmo visual mais pronunciado.
/* Sistema base-4 (em rem com base 16px) */
:root {
--espaco-1: 0.25rem; /* 4px */
--espaco-2: 0.5rem; /* 8px */
--espaco-3: 0.75rem; /* 12px */
--espaco-4: 1rem; /* 16px */
--espaco-5: 1.25rem; /* 20px */
--espaco-6: 1.5rem; /* 24px */
--espaco-8: 2rem; /* 32px */
--espaco-10: 2.5rem; /* 40px */
--espaco-12: 3rem; /* 48px */
--espaco-16: 4rem; /* 64px */
--espaco-20: 5rem; /* 80px */
--espaco-24: 6rem; /* 96px */
--espaco-32: 8rem; /* 128px */
}
11.5.2 — Tokens de espaçamento e sua aplicação¶
Com a escala primitiva definida, os tokens semânticos de espaçamento mapeiam intenções de uso:
:root {
/* Espaçamentos de componente */
--padding-btn-v: var(--espaco-2);
--padding-btn-h: var(--espaco-4);
--padding-btn-v-sm: var(--espaco-1);
--padding-btn-h-sm: var(--espaco-3);
--padding-btn-v-lg: var(--espaco-3);
--padding-btn-h-lg: var(--espaco-6);
--padding-card: var(--espaco-6);
--padding-card-sm: var(--espaco-4);
--padding-input-v: var(--espaco-2);
--padding-input-h: var(--espaco-3);
/* Espaçamentos de layout */
--gap-componentes: var(--espaco-4);
--gap-secoes: var(--espaco-12);
--padding-pagina-v: var(--espaco-8);
--padding-pagina-h: var(--espaco-6);
/* Espaçamentos de texto */
--margem-paragrafo: var(--espaco-4);
--margem-titulo: var(--espaco-6);
}
11.5.3 — Espaçamento fluido com clamp()¶
Assim como a tipografia, o espaçamento pode ser fluido — escalando entre valores mínimos e máximos conforme o viewport:
:root {
/* Espaçamentos fluidos — escalam com o viewport */
--espaco-fluido-sm: clamp(var(--espaco-4), 3vw, var(--espaco-6));
--espaco-fluido-md: clamp(var(--espaco-6), 5vw, var(--espaco-12));
--espaco-fluido-lg: clamp(var(--espaco-8), 8vw, var(--espaco-20));
--espaco-fluido-xl: clamp(var(--espaco-12), 10vw, var(--espaco-32));
}
/* Aplicação: seções da página com espaçamento fluido */
.secao {
padding-top: var(--espaco-fluido-lg);
padding-bottom: var(--espaco-fluido-lg);
padding-left: var(--espaco-fluido-md);
padding-right: var(--espaco-fluido-md);
}
11.6 — Componentização¶
Vídeo curto explicativo (link será adicionado posteriormente)
11.6.1 — O que é um componente CSS¶
No contexto de um Design System baseado em CSS puro, um componente é um conjunto de regras CSS que implementam um padrão de interface reutilizável — definido por uma classe base, variantes opcionais e estados interativos. Cada componente consome tokens de design em vez de valores hardcoded, garantindo que mudanças nos tokens se propagam automaticamente.
A estrutura de um componente segue o padrão:
- Classe base: (.btn) — estilos compartilhados por todas as variantes
- Classes de variante: (.btn--primario, .btn--secundario) — diferenciações visuais
- Classes de tamanho: (.btn--sm, .btn--lg) — escalas de dimensão
- Pseudo-classes de estado: (:hover, :focus-visible, :active, :disabled) — comportamento interativo
11.6.2 — Botões: variantes, estados e tokens¶
O botão é o componente mais fundamental de qualquer sistema — e o mais revelador de quão bem o sistema está estruturado:
/* ─── Componente: Button ─────────────────────────── */
.btn {
/* Layout */
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--espaco-2);
/* Dimensões */
padding: var(--padding-btn-v) var(--padding-btn-h);
/* Tipografia */
font-family: var(--fonte-sem-serifa);
font-size: var(--fonte-base);
font-weight: var(--peso-semibold);
line-height: 1;
white-space: nowrap;
/* Aparência */
border: 2px solid transparent;
border-radius: var(--raio-md);
cursor: pointer;
/* Interação */
transition:
background-color var(--transicao-padrao),
color var(--transicao-padrao),
border-color var(--transicao-padrao),
box-shadow var(--transicao-padrao),
transform var(--transicao-rapida);
/* Acessibilidade */
text-decoration: none;
user-select: none;
}
/* Foco acessível: visível para navegação por teclado */
.btn:focus-visible {
outline: 3px solid var(--cor-borda-foco);
outline-offset: 2px;
}
/* Estado desabilitado */
.btn:disabled,
.btn[aria-disabled="true"] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* ── Variantes ─── */
.btn--primario {
background-color: var(--cor-primaria);
color: var(--cor-texto-inverso);
}
.btn--primario:hover:not(:disabled) {
background-color: var(--cor-primaria-hover);
}
.btn--primario:active:not(:disabled) {
background-color: var(--cor-primaria-ativa);
transform: translateY(1px);
}
.btn--secundario {
background-color: transparent;
color: var(--cor-primaria);
border-color: var(--cor-primaria);
}
.btn--secundario:hover:not(:disabled) {
background-color: var(--cor-primaria-suave);
}
.btn--fantasma {
background-color: transparent;
color: var(--cor-texto-padrao);
}
.btn--fantasma:hover:not(:disabled) {
background-color: var(--cor-fundo-sutil);
}
.btn--perigo {
background-color: var(--cor-erro);
color: var(--cor-texto-inverso);
}
.btn--perigo:hover:not(:disabled) {
background-color: hsl(0, 74%, 37%);
}
/* ── Tamanhos ─── */
.btn--sm {
padding: var(--padding-btn-v-sm) var(--padding-btn-h-sm);
font-size: var(--fonte-sm);
border-radius: var(--raio-sm);
}
.btn--lg {
padding: var(--padding-btn-v-lg) var(--padding-btn-h-lg);
font-size: var(--fonte-md);
border-radius: var(--raio-lg);
}
/* ── Largura total ─── */
.btn--bloco {
width: 100%;
}
Uso em HTML:
<!-- Variantes -->
<button class="btn btn--primario" type="button">Salvar</button>
<button class="btn btn--secundario" type="button">Cancelar</button>
<button class="btn btn--fantasma" type="button">Ver mais</button>
<button class="btn btn--perigo" type="button">Excluir</button>
<!-- Tamanhos -->
<button class="btn btn--primario btn--sm" type="button">Pequeno</button>
<button class="btn btn--primario" type="button">Médio (padrão)</button>
<button class="btn btn--primario btn--lg" type="button">Grande</button>
<!-- Desabilitado (acessível) -->
<button class="btn btn--primario" type="button" disabled aria-disabled="true">
Indisponível
</button>
<!-- Com ícone -->
<button class="btn btn--primario" type="button">
<svg aria-hidden="true" focusable="false" width="16" height="16">...</svg>
Enviar
</button>
11.6.3 — Cards: estrutura, variantes e composição¶
/* ─── Componente: Card ───────────────────────────── */
.card {
background-color: var(--cor-fundo-card);
border: var(--card-borda);
border-radius: var(--raio-lg);
box-shadow: var(--sombra-sm);
overflow: hidden;
/* Card como flex container vertical */
display: flex;
flex-direction: column;
}
/* Regiões do card */
.card__imagem {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.card__corpo {
flex: 1;
padding: var(--padding-card);
display: flex;
flex-direction: column;
gap: var(--espaco-3);
}
.card__titulo {
font-size: var(--fonte-lg);
font-weight: var(--peso-semibold);
line-height: var(--linha-compacta);
color: var(--cor-texto-padrao);
margin: 0;
}
.card__descricao {
font-size: var(--fonte-base);
color: var(--cor-texto-secundario);
line-height: var(--linha-relaxada);
margin: 0;
flex: 1; /* empurra o rodapé para baixo */
}
.card__rodape {
padding: var(--espaco-4) var(--padding-card);
border-top: 1px solid var(--cor-borda-sutil);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--espaco-2);
}
/* ── Variantes ─── */
/* Card interativo: hover e foco */
.card--interativo {
cursor: pointer;
transition: box-shadow var(--transicao-padrao), transform var(--transicao-padrao);
text-decoration: none;
color: inherit;
}
.card--interativo:hover {
box-shadow: var(--sombra-lg);
transform: translateY(-2px);
}
.card--interativo:focus-visible {
outline: 3px solid var(--cor-borda-foco);
outline-offset: 2px;
}
/* Card horizontal: imagem ao lado */
.card--horizontal {
flex-direction: row;
}
.card--horizontal .card__imagem {
width: 200px;
aspect-ratio: auto;
height: 100%;
flex-shrink: 0;
}
/* Card de destaque */
.card--destaque {
border-top: 4px solid var(--cor-primaria);
}
/* Card sem borda */
.card--sem-sombra {
box-shadow: none;
}
11.6.4 — Inputs e formulários com tokens¶
/* ─── Componente: Form Field ─────────────────────── */
.campo {
display: flex;
flex-direction: column;
gap: var(--espaco-1);
}
.campo__label {
font-size: var(--fonte-sm);
font-weight: var(--peso-medio);
color: var(--cor-texto-padrao);
}
.campo__label--obrigatorio::after {
content: " *";
color: var(--cor-erro);
aria-hidden: true;
}
.campo__input {
width: 100%;
padding: var(--padding-input-v) var(--padding-input-h);
font-family: var(--fonte-sem-serifa);
font-size: var(--fonte-base);
color: var(--cor-texto-padrao);
background-color: var(--cor-fundo-card);
border: 1px solid var(--cor-borda-forte);
border-radius: var(--raio-md);
transition: border-color var(--transicao-rapida), box-shadow var(--transicao-rapida);
appearance: none;
}
.campo__input::placeholder {
color: var(--cor-texto-sutil);
}
.campo__input:hover:not(:disabled) {
border-color: var(--cor-primaria);
}
.campo__input:focus {
outline: none;
border-color: var(--cor-borda-foco);
box-shadow: 0 0 0 3px hsl(from var(--cor-borda-foco) h s l / 0.2);
}
/* Estado de erro */
.campo--erro .campo__input {
border-color: var(--cor-erro);
}
.campo--erro .campo__input:focus {
box-shadow: 0 0 0 3px hsl(from var(--cor-erro) h s l / 0.2);
}
.campo__mensagem-erro {
font-size: var(--fonte-sm);
color: var(--cor-erro-texto);
display: flex;
align-items: center;
gap: var(--espaco-1);
}
/* Estado de sucesso */
.campo--sucesso .campo__input {
border-color: var(--cor-sucesso);
}
.campo__mensagem-sucesso {
font-size: var(--fonte-sm);
color: var(--cor-sucesso-texto);
}
/* Dica de ajuda */
.campo__dica {
font-size: var(--fonte-sm);
color: var(--cor-texto-secundario);
}
/* Campo desabilitado */
.campo__input:disabled {
background-color: var(--cor-fundo-sutil);
color: var(--cor-texto-desabilitado);
cursor: not-allowed;
border-color: var(--cor-borda-padrao);
}
Uso em HTML:
<!-- Campo padrão -->
<div class="campo">
<label class="campo__label campo__label--obrigatorio" for="nome">
Nome completo
</label>
<input
class="campo__input"
type="text"
id="nome"
name="nome"
required
aria-required="true"
/>
</div>
<!-- Campo com erro -->
<div class="campo campo--erro">
<label class="campo__label" for="email">E-mail</label>
<input
class="campo__input"
type="email"
id="email"
name="email"
aria-invalid="true"
aria-describedby="email-erro"
/>
<p class="campo__mensagem-erro" id="email-erro" role="alert">
⚠ Informe um e-mail válido.
</p>
</div>
<!-- Campo com dica -->
<div class="campo">
<label class="campo__label" for="senha">Senha</label>
<input
class="campo__input"
type="password"
id="senha"
name="senha"
aria-describedby="senha-dica"
/>
<p class="campo__dica" id="senha-dica">
Mínimo de 8 caracteres, incluindo letras e números.
</p>
</div>
11.6.5 — Layouts reutilizáveis: container, grid e stack¶
Além de componentes de interface, um Design System inclui primitivas de layout — padrões estruturais reutilizáveis:
/* ─── Primitiva: Container ───────────────────────── */
.container {
width: 100%;
max-width: 1200px;
margin-inline: auto;
padding-inline: var(--espaco-fluido-md);
}
.container--estreito { max-width: 800px; }
.container--largo { max-width: 1400px; }
.container--fluido { max-width: none; }
/* ─── Primitiva: Grid ────────────────────────────── */
.grid {
display: grid;
gap: var(--gap-componentes);
}
.grid--2 { grid-template-columns: repeat(2, 1fr); }
.grid--3 { grid-template-columns: repeat(3, 1fr); }
.grid--4 { grid-template-columns: repeat(4, 1fr); }
/* Grid responsivo sem media queries */
.grid--auto {
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
}
@media (max-width: 768px) {
.grid--2,
.grid--3,
.grid--4 {
grid-template-columns: 1fr;
}
}
/* ─── Primitiva: Stack ───────────────────────────── */
/* Stack: empilha elementos verticalmente com espaçamento consistente */
.stack {
display: flex;
flex-direction: column;
}
.stack--xs { gap: var(--espaco-1); }
.stack--sm { gap: var(--espaco-2); }
.stack--md { gap: var(--espaco-4); } /* padrão */
.stack--lg { gap: var(--espaco-8); }
.stack--xl { gap: var(--espaco-16); }
/* ─── Primitiva: Cluster ─────────────────────────── */
/* Cluster: agrupa itens inline com wrap e espaçamento */
.cluster {
display: flex;
flex-wrap: wrap;
gap: var(--espaco-3);
align-items: center;
}
/* ─── Primitiva: Sidebar Layout ─────────────────── */
/* Sidebar: dois elementos lado a lado onde um é fixo e o outro cresce */
.com-sidebar {
display: flex;
flex-wrap: wrap;
gap: var(--espaco-8);
}
.com-sidebar > :first-child {
flex-basis: 300px; /* largura da sidebar */
flex-grow: 1;
}
.com-sidebar > :last-child {
flex-basis: 0;
flex-grow: 999; /* cresce muito mais que a sidebar */
min-width: 50%; /* não fica menor que 50% */
}
11.7 — Organização e manutenibilidade¶
Vídeo curto explicativo (link será adicionado posteriormente)
11.7.1 — Estrutura de arquivos para um mini Design System¶
css/
├── tokens/
│ ├── _primitivos.css /* Escalas brutas: cores, tamanhos, espaços */
│ ├── _semanticos.css /* Tokens semânticos: cor-primaria, fonte-titulo... */
│ └── _temas.css /* Tema claro, escuro, high-contrast */
│
├── base/
│ ├── _reset.css /* Normalização cross-browser */
│ └── _tipografia.css /* Estilos base de h1-h6, p, a, code... */
│
├── layout/
│ ├── _container.css /* .container e variantes */
│ ├── _grid.css /* .grid e variantes */
│ └── _stack.css /* .stack, .cluster, .com-sidebar */
│
├── components/
│ ├── _button.css /* .btn e variantes */
│ ├── _card.css /* .card e variantes */
│ ├── _form.css /* .campo, .campo__input... */
│ ├── _badge.css /* .badge */
│ ├── _alert.css /* .alerta */
│ └── _nav.css /* .nav, .navbar */
│
├── utilities/
│ └── _utilities.css /* Classes utilitárias: .sr-only, .truncado... */
│
└── main.css /* Arquivo de entrada: importa tudo */
main.css — arquivo de entrada:
/* ─── Tokens ─── */
@import url('tokens/_primitivos.css');
@import url('tokens/_semanticos.css');
@import url('tokens/_temas.css');
/* ─── Base ─── */
@import url('base/_reset.css');
@import url('base/_tipografia.css');
/* ─── Layout ─── */
@import url('layout/_container.css');
@import url('layout/_grid.css');
@import url('layout/_stack.css');
/* ─── Componentes ─── */
@import url('components/_button.css');
@import url('components/_card.css');
@import url('components/_form.css');
@import url('components/_badge.css');
@import url('components/_alert.css');
@import url('components/_nav.css');
/* ─── Utilitários ─── */
@import url('utilities/_utilities.css');
11.7.2 — Camadas CSS com @layer¶
A regra @layer (CSS Cascade Layers), suportada em todos os navegadores modernos desde 2022, permite organizar explicitamente as camadas de especificidade do CSS — eliminando conflitos sem recorrer a !important:
/* Declaração da ordem das camadas — feita uma vez, no topo do main.css */
@layer reset, tokens, base, layout, componentes, utilitarios;
/* Cada camada tem sua especificidade isolada das demais */
/* A ordem de declaração define a prioridade: utilitarios > componentes > ... */
@layer reset {
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
}
@layer tokens {
:root {
--cor-primaria: var(--azul-700);
--fonte-base: 1rem;
/* ... */
}
}
@layer base {
body {
font-family: var(--fonte-sem-serifa);
color: var(--cor-texto-padrao);
}
h1 { font-size: var(--fonte-3xl); }
}
@layer componentes {
.btn {
/* especificidade: (0, 1, 0) dentro da camada componentes */
padding: var(--padding-btn-v) var(--padding-btn-h);
}
.btn--primario {
background-color: var(--cor-primaria);
}
}
@layer utilitarios {
/* Utilitários sempre vencem componentes — sem !important */
.oculto { display: none; }
.sr-only { /* visually hidden */ }
.truncado { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.texto-centro { text-align: center; }
}
Por que @layer é importante para sistemas:
Sem @layer, a especificidade dos seletores determina qual regra prevalece — e isso frequentemente exige !important para que utilitários sobrescrevam componentes. Com @layer, a ordem de prioridade é declarada explicitamente: a camada utilitarios sempre vence componentes, independente da especificidade dos seletores dentro de cada camada.
Referência: MDN — @layer
11.7.3 — Documentação mínima de componentes¶
Um Design System sem documentação é apenas código. A documentação mínima de cada componente deve incluir:
1. Propósito: o que o componente é e quando deve ser usado.
2. Variantes: todas as variações disponíveis com exemplos visuais.
3. Estados: como o componente se comporta em hover, foco, ativo, desabilitado e erro.
4. Tokens utilizados: quais variáveis CSS o componente consome — facilitando a customização.
5. Exemplos de uso correto e incorreto: o que se deve e o que não se deve fazer.
6. Requisitos de acessibilidade: atributos ARIA necessários, navegação por teclado esperada.
Para projetos acadêmicos e portfólios, uma documentação em formato HTML estático já é suficiente — cada componente exibido com seus exemplos de código e notas de uso. Ferramentas como Storybook (storybook.js.org) automatizam a geração dessa documentação para componentes JavaScript, mas estão fora do escopo desta disciplina.
Referências: - MDN — CSS Custom Properties - W3C — CSS Cascading and Inheritance Level 5 - Design Tokens Community Group - Storybook — Component documentation - Every Layout — Layout primitives - Open Props — Design tokens open source
Atividades — Capítulo 11¶
1. Qual é a diferença entre um token primitivo e um token semântico em um Design System?
2. Por que a arquitetura de tokens semânticos facilita a implementação de tema escuro?
3. Qual é a vantagem do uso de @layer em comparação com a arquitetura CSS tradicional sem camadas?
- GitHub Classroom: Construir um mini Design System com: arquivo de tokens em três camadas (primitivos, semânticos, componente), implementação do componente
.btncom ao menos três variantes e todos os estados interativos, implementação do componente.cardcom variante horizontal, tema escuro viaprefers-color-scheme, e organização em@layer. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 10 — Design Responsivo :material-arrow-right: Ir ao Capítulo 12 — Framework CSS: Tailwind
Capítulo 12 — Framework CSS: Tailwind CSS¶
Vídeo curto explicativo (link será adicionado posteriormente)
12.1 — O que é um framework CSS e por que usar¶
Vídeo curto explicativo (link será adicionado posteriormente)
Um framework CSS é um conjunto pré-construído de abstrações, convenções e ferramentas que acelera o desenvolvimento de interfaces ao fornecer soluções padronizadas para problemas recorrentes de estilização e layout. Em vez de escrever CSS do zero para cada projeto, o desenvolvedor parte de uma base consolidada — reduzindo o tempo de configuração inicial, promovendo consistência visual e beneficiando-se de decisões de design já validadas pela comunidade.
A adoção de frameworks CSS tornou-se praticamente universal no desenvolvimento front-end moderno. Compreender como e por que eles funcionam — incluindo suas limitações — é uma competência fundamental para qualquer desenvolvedor web.
12.1.1 — O problema que frameworks resolvem¶
O desenvolvimento de CSS em projetos de médio e grande porte enfrenta um conjunto de problemas recorrentes que frameworks foram criados para mitigar:
Inconsistência visual: sem um sistema de referência compartilhado, diferentes desenvolvedores de uma mesma equipe produzem estilos inconsistentes — margens diferentes para espaçamentos similares, botões com variações sutis de cor, tipografia sem hierarquia coerente.
Retrabalho de decisões: cada projeto recria soluções para os mesmos problemas — sistema de grid, componentes de botão, formulários, navegação responsiva. Frameworks encapsulam essas soluções, permitindo que a equipe foque no que é específico do produto.
Escalabilidade do CSS: CSS cresce sem estrutura inerente. Em projetos longos, a folha de estilos frequentemente acumula regras redundantes, conflitos de especificidade e seletores frágeis. Frameworks impõem convenções que limitam esse crescimento desordenado.
Curva de entrada para equipes: novos membros integram-se mais rapidamente quando o projeto usa um framework conhecido — as convenções são documentadas externamente, não precisam ser aprendidas a partir do código legado.
12.1.2 — Categorias de frameworks: utility-first vs component-based¶
Os frameworks CSS modernos se dividem em duas filosofias fundamentais:
Frameworks baseados em componentes (component-based): fornecem componentes de interface prontos — botões, cards, modais, navbars, grids — que o desenvolvedor instancia adicionando classes predefinidas ao HTML. O Bootstrap é o exemplo mais conhecido. O desenvolvedor consome componentes sem necessariamente escrever CSS.
Frameworks utility-first: fornecem classes de baixo nível (utilities) que mapeiam diretamente para propriedades CSS individuais. O desenvolvedor constrói a interface compondo essas classes no HTML, sem componentes prontos. O Tailwind CSS é o principal representante desta categoria.
<!-- Component-based (Bootstrap): componente pronto -->
<button class="btn btn-primary btn-lg">Salvar</button>
<!-- Utility-first (Tailwind): composto de utilities -->
<button class="bg-blue-700 text-white font-semibold px-6 py-3
rounded-lg hover:bg-blue-800 transition-colors">
Salvar
</button>
A distinção não é apenas sintática — ela reflete abordagens diferentes de controle, flexibilidade e manutenção, exploradas em profundidade na seção 12.2.
12.1.3 — Tailwind CSS no contexto do mercado¶
O Tailwind CSS foi criado por Adam Wathan e lançado em 2017. Em um período relativamente curto, tornou-se o framework CSS mais amplamente adotado no desenvolvimento front-end moderno — superando o Bootstrap em downloads npm pela primeira vez em 2022 e mantendo crescimento consistente desde então.
Segundo a pesquisa State of CSS 2024, o Tailwind possui taxa de satisfação superior a 80% entre desenvolvedores que o utilizam — uma das mais altas da categoria. É o framework padrão em ecossistemas como Next.js, Nuxt.js e Laravel, e é utilizado por empresas como GitHub, Shopify, OpenAI e Vercel.
A versão atual é o Tailwind CSS v4 (2025), que introduziu um novo motor de build baseado em Rust (Oxide), configuração via CSS em vez de JavaScript e melhorias significativas de desempenho. Este capítulo cobre os conceitos fundamentais que são estáveis entre versões, com exemplos baseados nas convenções do Tailwind v3/v4.
Referência: Tailwind CSS — Documentação oficial
12.1.4 — Tailwind vs Bootstrap: diferenças filosóficas e práticas¶
A comparação entre Tailwind e Bootstrap é inevitável — ambos dominam o mercado de frameworks CSS, mas representam filosofias opostas. Compreender as diferenças ajuda a escolher a ferramenta adequada para cada contexto.
| Critério | Tailwind CSS | Bootstrap |
|---|---|---|
| Filosofia | Utility-first | Component-based |
| Componentes prontos | Nenhum (apenas utilities) | Extenso (botões, modais, navbars...) |
| Customização | Alta — tudo é configurável | Média — requer sobrescrever variáveis Sass |
| Tamanho do CSS | Mínimo — apenas classes usadas (PurgeCSS) | Maior — importa componentes não usados |
| Curva de aprendizado | Maior — requer conhecer as utilities | Menor — basta adicionar classes de componentes |
| Velocidade inicial | Lenta — constrói do zero | Rápida — componentes prontos |
| Flexibilidade visual | Muito alta | Limitada pelo design do Bootstrap |
| Aparência padrão | Nenhuma | "Cara de Bootstrap" reconhecível |
| Versão atual | v4 (2025) | v5.3 (2024) |
Quando escolher Tailwind: projetos onde a identidade visual é importante e não se quer a aparência genérica do Bootstrap; equipes com desenvolvedores front-end experientes; produtos que evoluem rapidamente e precisam de flexibilidade.
Quando escolher Bootstrap: prototipação rápida onde a aparência visual não é crítica; sistemas internos e ferramentas administrativas onde velocidade de desenvolvimento supera customização; equipes com pouca experiência em CSS que se beneficiam de componentes prontos.
Posição deste material: este capítulo cobre o Tailwind CSS conforme o plano de ensino. Caso o projeto ou estágio exija Bootstrap, a documentação oficial (getbootstrap.com) é autoexplicativa após dominar os fundamentos de CSS dos capítulos anteriores.
12.2 — Filosofia utility-first¶
Vídeo curto explicativo (link será adicionado posteriormente)
12.2.1 — O que são classes utilitárias¶
Uma classe utilitária (utility class) é uma classe CSS com propósito único — ela aplica exatamente uma propriedade CSS com um valor específico. Em vez de criar abstrações semânticas, classes utilitárias são funcionais e diretas:
/* Classes utilitárias — cada uma faz uma coisa */
.flex { display: flex; }
.items-center { align-items: center; }
.gap-4 { gap: 1rem; }
.text-lg { font-size: 1.125rem; }
.font-bold { font-weight: 700; }
.text-white { color: #ffffff; }
.bg-blue-700 { background-color: #1d4ed8; }
.rounded-lg { border-radius: 0.5rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
A premissa do utility-first é que interfaces são compostas adicionando essas classes diretamente ao HTML — sem criar classes semânticas intermediárias para a maioria dos casos:
<!-- CSS semântico: classe com nome descritivo, CSS separado -->
<nav class="navbar">...</nav>
<!-- Utility-first: propriedades expressas diretamente no HTML -->
<nav class="flex items-center justify-between px-6 py-4 bg-slate-900">...</nav>
12.2.2 — Utility-first vs CSS semântico: a tensão e o equilíbrio¶
A abordagem utility-first gera uma tensão filosófica com a separação clássica entre HTML (estrutura) e CSS (apresentação) — um dos princípios fundamentais apresentados no Capítulo 7. Ao colocar informações visuais diretamente no HTML via classes, o utility-first viola aparentemente essa separação.
Adam Wathan argumenta, em seu ensaio CSS Utility Classes and "Separation of Concerns", que a separação relevante não é entre HTML e CSS, mas entre componentes de interface reutilizáveis. Em uma aplicação moderna baseada em componentes, o HTML de um botão não é "um documento separado do CSS" — é parte de um componente coeso. A questão não é onde as classes estão, mas se o componente é reutilizável.
Críticas legítimas ao utility-first:
- HTML verboso: classes longas reduzem a legibilidade do markup, especialmente para desenvolvedores acostumados ao CSS semântico
- Dificuldade de manutenção em componentes repetidos: sem extração de componentes, a mesma lista de classes precisa ser duplicada em cada instância
- Curva de aprendizado: exige memorizar ou consultar frequentemente a nomenclatura das utilities
Vantagens práticas:
- Sem conflitos de nomenclatura: não há necessidade de inventar nomes de classes para cada elemento
- CSS nunca cresce: o arquivo CSS final contém apenas as utilities utilizadas — não importa quantas páginas o projeto tem
- Iteração visual rápida: mudanças visuais são feitas diretamente no HTML sem alternar entre arquivos
- Consistência por convenção: todos os valores de espaçamento, cor e tipografia vêm da escala do Tailwind — não de valores arbitrários
12.2.3 — Comparação: CSS tradicional vs Tailwind para o mesmo componente¶
<!-- CSS Tradicional -->
<article class="card">
<img class="card__imagem" src="foto.jpg" alt="Descrição" />
<div class="card__corpo">
<h2 class="card__titulo">Título do Card</h2>
<p class="card__descricao">Descrição do conteúdo...</p>
</div>
<footer class="card__rodape">
<a class="btn btn--primario" href="#">Saiba mais</a>
</footer>
</article>
/* CSS separado: ~30 linhas */
.card {
background: white;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
}
.card__imagem { width: 100%; height: 200px; object-fit: cover; }
.card__corpo { flex: 1; padding: 1.5rem; }
.card__titulo { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; }
/* ... */
<!-- Tailwind: tudo no HTML, ~0 linhas de CSS personalizado -->
<article class="bg-white rounded-lg shadow-md overflow-hidden flex flex-col">
<img class="w-full h-48 object-cover" src="foto.jpg" alt="Descrição" />
<div class="flex-1 p-6">
<h2 class="text-xl font-semibold mb-2">Título do Card</h2>
<p class="text-slate-600 leading-relaxed">Descrição do conteúdo...</p>
</div>
<footer class="px-6 pb-6">
<a class="inline-flex items-center px-4 py-2 bg-blue-700 text-white
font-semibold rounded-lg hover:bg-blue-800 transition-colors"
href="#">
Saiba mais
</a>
</footer>
</article>
12.2.4 — Quando utility-first faz sentido e quando não faz¶
Faz sentido quando: - O projeto é construído com componentes (React, Vue, templates reutilizáveis) - A equipe valoriza iteração visual rápida - A identidade visual é importante e diferenciada - O projeto precisa de CSS mínimo em produção
Não faz sentido quando: - O projeto é um documento HTML estático simples sem componentes - A equipe tem resistência à verbosidade no HTML - Há necessidade de componentes visuais prontos rapidamente (Bootstrap é mais eficiente) - O projeto usa um CMS que gera HTML e não permite controle das classes
12.3 — Instalação e configuração¶
Vídeo curto explicativo (link será adicionado posteriormente)
12.3.1 — Usando Tailwind via CDN (para prototipação)¶
A forma mais rápida de experimentar o Tailwind é via CDN — sem instalação, ideal para protótipos e atividades iniciais:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tailwind via CDN</title>
<!-- Tailwind CSS via CDN — apenas para prototipação -->
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-50 text-slate-900 p-8">
<h1 class="text-4xl font-bold text-blue-700 mb-4">Olá, Tailwind!</h1>
<p class="text-lg text-slate-600">
Esta página usa Tailwind CSS via CDN.
</p>
</body>
</html>
⚠️ Limitação do CDN: a versão CDN do Tailwind carrega o framework inteiro no browser e processa as classes em tempo de execução — produzindo um arquivo CSS grande e sem otimização. É adequada apenas para prototipação e aprendizado. Nunca use a CDN em produção — utilize a instalação via npm com o processo de build para obter apenas as classes efetivamente usadas.
12.3.2 — Instalação via npm e CLI¶
Para projetos reais, o Tailwind é instalado como dependência de desenvolvimento e processado por uma ferramenta de build:
Passo 1 — Inicializar o projeto e instalar o Tailwind:
# Inicializar package.json (se ainda não existir)
npm init -y
# Instalar Tailwind CSS e suas dependências
npm install -D tailwindcss postcss autoprefixer
# Gerar os arquivos de configuração
npx tailwindcss init -p
Passo 2 — Configurar o tailwind.config.js:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
// content: informa ao Tailwind quais arquivos escanear
// para detectar as classes utilizadas
content: [
"./src/**/*.{html,js}",
"./*.html",
],
theme: {
extend: {
// personalizações do tema (seção 12.8)
},
},
plugins: [],
}
Passo 3 — Criar o arquivo CSS de entrada:
/* src/input.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Passo 4 — Executar o processo de build:
# Build único
npx tailwindcss -i ./src/input.css -o ./dist/output.css
# Build em modo watch (recompila ao salvar)
npx tailwindcss -i ./src/input.css -o ./dist/output.css --watch
Passo 5 — Vincular o CSS gerado ao HTML:
<head>
<link rel="stylesheet" href="./dist/output.css" />
</head>
Scripts no package.json para facilitar:
{
"scripts": {
"dev": "tailwindcss -i ./src/input.css -o ./dist/output.css --watch",
"build": "tailwindcss -i ./src/input.css -o ./dist/output.css --minify"
}
}
Com esses scripts, npm run dev inicia o modo de desenvolvimento e npm run build gera o CSS minificado para produção.
12.3.3 — O arquivo tailwind.config.js¶
O arquivo de configuração é o coração da customização do Tailwind. Sua estrutura principal:
// tailwind.config.js
module.exports = {
// Arquivos a escanear para detecção de classes
content: ["./src/**/*.{html,js,ts,jsx,tsx}"],
// darkMode: 'media' (sistema) ou 'class' (manual via classe .dark)
darkMode: 'media',
theme: {
// theme.XXX: SUBSTITUI os valores padrão
screens: {
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
'2xl': '1536px',
},
// theme.extend.XXX: ADICIONA aos valores padrão
extend: {
colors: {
'primaria': {
50: '#eff6ff',
500: '#3b82f6',
700: '#1d4ed8',
900: '#1e3a8a',
},
},
fontFamily: {
'sem-serifa': ['Inter', 'system-ui', 'sans-serif'],
},
spacing: {
'18': '4.5rem',
'88': '22rem',
},
},
},
plugins: [],
}
12.3.4 — Integração com o processo de build¶
Em projetos mais complexos, o Tailwind se integra com ferramentas de build como Vite, Webpack ou Parcel. A integração com Vite — o bundler mais popular atualmente — é a mais simples:
# Criar projeto Vite
npm create vite@latest meu-projeto -- --template vanilla
cd meu-projeto
npm install
# Adicionar Tailwind
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js para Vite
module.exports = {
content: ["./index.html", "./src/**/*.{js,ts}"],
theme: { extend: {} },
plugins: [],
}
/* src/style.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Com essa configuração, npm run dev inicia o servidor de desenvolvimento com hot reload e recompilação automática do Tailwind.
12.4 — Classes utilitárias essenciais¶
Vídeo curto explicativo (link será adicionado posteriormente)
O Tailwind possui centenas de classes utilitárias. Esta seção cobre as mais utilizadas — organizadas por categoria — com a correspondência direta ao CSS que cada uma gera.
Imagem sugerida: captura da extensão Tailwind CSS IntelliSense no VS Code mostrando o autocomplete de classes com preview do CSS gerado — demonstrando como a ferramenta facilita o aprendizado das utilities.
(imagem será adicionada posteriormente)
Dica de produtividade: instale a extensão Tailwind CSS IntelliSense no VS Code. Ela fornece autocomplete, preview do CSS gerado ao passar o mouse sobre uma classe e detecção de erros. É indispensável para trabalhar com Tailwind de forma eficiente.
12.4.1 — Layout: display, position, overflow¶
<!-- Display -->
<div class="block">...</div> <!-- display: block -->
<span class="inline-block">...</span> <!-- display: inline-block -->
<div class="flex">...</div> <!-- display: flex -->
<div class="inline-flex">...</div> <!-- display: inline-flex -->
<div class="grid">...</div> <!-- display: grid -->
<div class="hidden">...</div> <!-- display: none -->
<!-- Position -->
<div class="relative">...</div> <!-- position: relative -->
<div class="absolute">...</div> <!-- position: absolute -->
<div class="fixed">...</div> <!-- position: fixed -->
<div class="sticky">...</div> <!-- position: sticky -->
<!-- Coordenadas de posicionamento -->
<div class="absolute top-0 right-0">...</div> <!-- top: 0; right: 0 -->
<div class="absolute inset-0">...</div> <!-- top/right/bottom/left: 0 -->
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<!-- centralização com transform -->
</div>
<!-- Z-index -->
<div class="z-10">...</div> <!-- z-index: 10 -->
<div class="z-50">...</div> <!-- z-index: 50 -->
<div class="z-[1000]">...</div> <!-- valor arbitrário: z-index: 1000 -->
<!-- Overflow -->
<div class="overflow-hidden">...</div> <!-- overflow: hidden -->
<div class="overflow-auto">...</div> <!-- overflow: auto -->
<div class="overflow-x-auto">...</div> <!-- overflow-x: auto -->
12.4.2 — Flexbox e Grid com Tailwind¶
<!-- ── Flexbox ── -->
<div class="flex flex-row gap-4 items-center justify-between">
<!-- display: flex; flex-direction: row; gap: 1rem;
align-items: center; justify-content: space-between -->
</div>
<!-- flex-direction -->
<div class="flex flex-col">...</div> <!-- column -->
<div class="flex flex-row-reverse">...</div> <!-- row-reverse -->
<!-- flex-wrap -->
<div class="flex flex-wrap gap-4">...</div> <!-- flex-wrap: wrap -->
<!-- justify-content -->
<div class="flex justify-start">...</div>
<div class="flex justify-center">...</div>
<div class="flex justify-end">...</div>
<div class="flex justify-between">...</div>
<div class="flex justify-around">...</div>
<div class="flex justify-evenly">...</div>
<!-- align-items -->
<div class="flex items-start">...</div>
<div class="flex items-center">...</div>
<div class="flex items-end">...</div>
<div class="flex items-stretch">...</div>
<div class="flex items-baseline">...</div>
<!-- flex items -->
<div class="flex-1">...</div> <!-- flex: 1 1 0% -->
<div class="flex-auto">...</div> <!-- flex: 1 1 auto -->
<div class="flex-none">...</div> <!-- flex: none -->
<div class="shrink-0">...</div> <!-- flex-shrink: 0 -->
<div class="grow">...</div> <!-- flex-grow: 1 -->
<!-- align-self -->
<div class="self-center">...</div>
<div class="self-start">...</div>
<!-- ── Grid ── -->
<div class="grid grid-cols-3 gap-6">
<!-- display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem -->
</div>
<!-- Colunas -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">...</div>
<!-- Grid responsivo sem media queries -->
<div class="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-4">...</div>
<!-- Posicionamento de itens -->
<div class="col-span-2">...</div> <!-- grid-column: span 2 -->
<div class="col-span-full">...</div> <!-- grid-column: 1 / -1 -->
<div class="row-span-2">...</div> <!-- grid-row: span 2 -->
12.4.3 — Espaçamento: padding, margin, gap¶
O Tailwind usa uma escala numérica onde cada unidade equivale a 0.25rem (4px com base 16px):
<!-- Padding -->
<div class="p-4">...</div> <!-- padding: 1rem (4 × 0.25rem) -->
<div class="px-6">...</div> <!-- padding-left/right: 1.5rem -->
<div class="py-3">...</div> <!-- padding-top/bottom: 0.75rem -->
<div class="pt-2 pb-4">...</div> <!-- top: 0.5rem; bottom: 1rem -->
<div class="ps-4">...</div> <!-- padding-inline-start: 1rem (RTL-aware) -->
<!-- Margin -->
<div class="m-4">...</div> <!-- margin: 1rem -->
<div class="mx-auto">...</div> <!-- margin-left/right: auto (centraliza) -->
<div class="mt-8 mb-4">...</div>
<div class="-mt-2">...</div> <!-- margin-top: -0.5rem (negativo) -->
<!-- Gap (em flex e grid) -->
<div class="flex gap-4">...</div> <!-- gap: 1rem -->
<div class="grid gap-x-6 gap-y-4">...</div> <!-- column-gap: 1.5rem; row-gap: 1rem -->
<!-- Referência da escala de espaçamento -->
<!-- 0=0, 0.5=0.125rem, 1=0.25rem, 2=0.5rem, 3=0.75rem, 4=1rem,
5=1.25rem, 6=1.5rem, 7=1.75rem, 8=2rem, 10=2.5rem, 12=3rem,
16=4rem, 20=5rem, 24=6rem, 32=8rem, 40=10rem, 48=12rem, 64=16rem -->
12.4.4 — Dimensionamento: width, height, max/min¶
<!-- Width -->
<div class="w-full">...</div> <!-- width: 100% -->
<div class="w-1/2">...</div> <!-- width: 50% -->
<div class="w-1/3">...</div> <!-- width: 33.333% -->
<div class="w-64">...</div> <!-- width: 16rem -->
<div class="w-screen">...</div> <!-- width: 100vw -->
<div class="w-fit">...</div> <!-- width: fit-content -->
<div class="w-auto">...</div> <!-- width: auto -->
<!-- Height -->
<div class="h-full">...</div> <!-- height: 100% -->
<div class="h-screen">...</div> <!-- height: 100vh -->
<div class="h-48">...</div> <!-- height: 12rem -->
<div class="h-fit">...</div> <!-- height: fit-content -->
<div class="min-h-screen">...</div> <!-- min-height: 100vh -->
<!-- Max/Min width -->
<div class="max-w-sm">...</div> <!-- max-width: 24rem -->
<div class="max-w-md">...</div> <!-- max-width: 28rem -->
<div class="max-w-lg">...</div> <!-- max-width: 32rem -->
<div class="max-w-xl">...</div> <!-- max-width: 36rem -->
<div class="max-w-2xl">...</div> <!-- max-width: 42rem -->
<div class="max-w-4xl">...</div> <!-- max-width: 56rem -->
<div class="max-w-6xl">...</div> <!-- max-width: 72rem -->
<div class="max-w-7xl">...</div> <!-- max-width: 80rem -->
<div class="max-w-full">...</div> <!-- max-width: 100% -->
<div class="max-w-none">...</div> <!-- max-width: none -->
<!-- Aspect ratio -->
<div class="aspect-video">...</div> <!-- aspect-ratio: 16 / 9 -->
<div class="aspect-square">...</div> <!-- aspect-ratio: 1 / 1 -->
12.4.5 — Tipografia¶
<!-- font-size -->
<p class="text-xs">...</p> <!-- 0.75rem -->
<p class="text-sm">...</p> <!-- 0.875rem -->
<p class="text-base">...</p> <!-- 1rem -->
<p class="text-lg">...</p> <!-- 1.125rem -->
<p class="text-xl">...</p> <!-- 1.25rem -->
<p class="text-2xl">...</p> <!-- 1.5rem -->
<p class="text-3xl">...</p> <!-- 1.875rem -->
<p class="text-4xl">...</p> <!-- 2.25rem -->
<p class="text-5xl">...</p> <!-- 3rem -->
<p class="text-6xl">...</p> <!-- 3.75rem -->
<!-- font-weight -->
<p class="font-thin">...</p> <!-- 100 -->
<p class="font-light">...</p> <!-- 300 -->
<p class="font-normal">...</p> <!-- 400 -->
<p class="font-medium">...</p> <!-- 500 -->
<p class="font-semibold">...</p> <!-- 600 -->
<p class="font-bold">...</p> <!-- 700 -->
<p class="font-extrabold">...</p> <!-- 800 -->
<p class="font-black">...</p> <!-- 900 -->
<!-- line-height -->
<p class="leading-none">...</p> <!-- 1 -->
<p class="leading-tight">...</p> <!-- 1.25 -->
<p class="leading-snug">...</p> <!-- 1.375 -->
<p class="leading-normal">...</p> <!-- 1.5 -->
<p class="leading-relaxed">...</p> <!-- 1.625 -->
<p class="leading-loose">...</p> <!-- 2 -->
<!-- text-align -->
<p class="text-left">...</p>
<p class="text-center">...</p>
<p class="text-right">...</p>
<p class="text-justify">...</p>
<!-- letter-spacing -->
<p class="tracking-tight">...</p> <!-- -0.05em -->
<p class="tracking-normal">...</p> <!-- 0 -->
<p class="tracking-wide">...</p> <!-- 0.025em -->
<p class="tracking-wider">...</p> <!-- 0.05em -->
<p class="tracking-widest">...</p> <!-- 0.1em -->
<!-- text-decoration -->
<a class="underline">...</a>
<a class="no-underline">...</a>
<p class="line-through">...</p>
<!-- text-transform -->
<p class="uppercase">...</p>
<p class="lowercase">...</p>
<p class="capitalize">...</p>
<!-- Truncamento *)
<p class="truncate">Texto longo que será truncado...</p>
<!-- overflow: hidden; text-overflow: ellipsis; white-space: nowrap -->
12.4.6 — Cores: text, background, border¶
O Tailwind inclui uma paleta de cores extensa com escalas de 50 a 950 para cada tom. A nomenclatura segue o padrão {propriedade}-{cor}-{escala}:
<!-- text color -->
<p class="text-slate-900">...</p> <!-- cor escura para texto -->
<p class="text-slate-600">...</p> <!-- cinza médio secundário -->
<p class="text-blue-700">...</p> <!-- azul para links/destaques -->
<p class="text-white">...</p> <!-- branco -->
<!-- background color -->
<div class="bg-white">...</div>
<div class="bg-slate-50">...</div> <!-- fundo levemente cinza -->
<div class="bg-slate-900">...</div> <!-- fundo escuro -->
<div class="bg-blue-700">...</div> <!-- fundo azul (primário) -->
<div class="bg-blue-50">...</div> <!-- azul muito claro (destaque sutil) -->
<!-- border color -->
<div class="border border-slate-200">...</div> <!-- borda cinza clara -->
<div class="border border-blue-500">...</div> <!-- borda azul -->
<!-- Cores com opacidade -->
<div class="bg-blue-700/80">...</div> <!-- bg com 80% de opacidade -->
<p class="text-slate-900/70">...</p> <!-- texto com 70% de opacidade -->
<div class="border border-black/10">...</div> <!-- borda preta 10% opacidade -->
Paleta de cores do Tailwind (tons principais):
| Cor | Uso típico |
|---|---|
slate |
Texto, fundos neutros, bordas |
gray |
Alternativa neutra ao slate |
zinc |
Neutro com tom levemente quente |
red |
Erros, perigo, alertas críticos |
orange |
Avisos, destaques |
yellow |
Avisos suaves |
green |
Sucesso, confirmação |
blue |
Ação primária, links, informação |
indigo |
Alternativa ao azul |
purple |
Destaques visuais |
pink |
Uso decorativo |
12.4.7 — Bordas: border, border-radius, outline¶
<!-- border -->
<div class="border">...</div> <!-- border: 1px solid -->
<div class="border-2">...</div> <!-- border-width: 2px -->
<div class="border-4">...</div> <!-- border-width: 4px -->
<div class="border-t">...</div> <!-- border-top apenas -->
<div class="border-b border-slate-200">...</div> <!-- bottom com cor -->
<div class="border-0">...</div> <!-- remove borda -->
<!-- border-radius -->
<div class="rounded-none">...</div> <!-- 0 -->
<div class="rounded-sm">...</div> <!-- 0.125rem -->
<div class="rounded">...</div> <!-- 0.25rem -->
<div class="rounded-md">...</div> <!-- 0.375rem -->
<div class="rounded-lg">...</div> <!-- 0.5rem -->
<div class="rounded-xl">...</div> <!-- 0.75rem -->
<div class="rounded-2xl">...</div> <!-- 1rem -->
<div class="rounded-full">...</div> <!-- 9999px — círculo/pílula -->
<!-- outline (foco) -->
<button class="focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-blue-600">
Botão acessível
</button>
<!-- ring (foco — alternativa com box-shadow) *)
<input class="focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
focus:outline-none" />
12.4.8 — Sombras e opacidade¶
<!-- box-shadow -->
<div class="shadow-sm">...</div> <!-- sombra sutil *)
<div class="shadow">...</div> <!-- sombra padrão *)
<div class="shadow-md">...</div> <!-- sombra média *)
<div class="shadow-lg">...</div> <!-- sombra grande *)
<div class="shadow-xl">...</div> <!-- sombra extra grande *)
<div class="shadow-2xl">...</div> <!-- sombra máxima *)
<div class="shadow-none">...</div> <!-- remove sombra *)
<!-- opacity *)
<div class="opacity-0">...</div> <!-- opacity: 0 (invisível) *)
<div class="opacity-50">...</div> <!-- opacity: 0.5 *)
<div class="opacity-75">...</div> <!-- opacity: 0.75 *)
<div class="opacity-100">...</div> <!-- opacity: 1 (opaco) *)
12.4.9 — Transições e animações básicas¶
<!-- transition *)
<button class="transition-colors duration-200">...</button>
<!-- transition: color, background-color, border-color... 200ms *)
<div class="transition-all duration-300 ease-in-out">...</div>
<!-- transition: all 300ms ease-in-out *)
<div class="transition-transform duration-150">...</div>
<!-- duration -->
<div class="duration-75">...</div> <!-- 75ms *)
<div class="duration-100">...</div> <!-- 100ms *)
<div class="duration-150">...</div> <!-- 150ms *)
<div class="duration-200">...</div> <!-- 200ms *)
<div class="duration-300">...</div> <!-- 300ms *)
<div class="duration-500">...</div> <!-- 500ms *)
<div class="duration-700">...</div> <!-- 700ms *)
<!-- easing *)
<div class="ease-linear">...</div>
<div class="ease-in">...</div>
<div class="ease-out">...</div>
<div class="ease-in-out">...</div>
<!-- transform *)
<div class="hover:scale-105">...</div> <!-- scale(1.05) *)
<div class="hover:-translate-y-1">...</div> <!-- translateY(-0.25rem) *)
<div class="hover:rotate-3">...</div> <!-- rotate(3deg) *)
<!-- animate (animações predefinidas) *)
<div class="animate-spin">...</div> <!-- rotação contínua *)
<div class="animate-ping">...</div> <!-- pulso expansivo *)
<div class="animate-pulse">...</div> <!-- pulso de opacidade *)
<div class="animate-bounce">...</div> <!-- salto *)
12.5 — Responsividade com Tailwind¶
Vídeo curto explicativo (link será adicionado posteriormente)
12.5.1 — O sistema de breakpoints do Tailwind¶
O Tailwind implementa um sistema de breakpoints baseado em min-width — mobile-first por padrão:
| Prefixo | Breakpoint | CSS gerado |
|---|---|---|
| (nenhum) | < 640px | estilos base (mobile) |
sm: |
≥ 640px | @media (min-width: 640px) |
md: |
≥ 768px | @media (min-width: 768px) |
lg: |
≥ 1024px | @media (min-width: 1024px) |
xl: |
≥ 1280px | @media (min-width: 1280px) |
2xl: |
≥ 1536px | @media (min-width: 1536px) |
12.5.2 — Prefixos responsivos¶
Qualquer utility pode ser prefixada com um breakpoint para condicionar sua aplicação:
<!-- Sem prefixo: aplica em todos os tamanhos (mobile-first) -->
<!-- Com prefixo: aplica apenas a partir daquele breakpoint -->
<div class="text-base md:text-lg lg:text-xl">
Texto que cresce com o viewport
</div>
<div class="flex flex-col md:flex-row gap-4 md:gap-6">
Empilhado em mobile, lado a lado a partir de md
</div>
<div class="hidden md:block">
Oculto em mobile, visível a partir de md
</div>
<div class="block md:hidden">
Visível apenas em mobile
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
Grade que aumenta colunas progressivamente
</div>
12.5.3 — Mobile-first por padrão¶
O sistema de breakpoints do Tailwind reforça a abordagem mobile-first apresentada no Capítulo 10: os estilos sem prefixo são os estilos base (mobile), e os prefixos adicionam sobrescrições progressivas para viewports maiores:
<!-- Mobile-first com Tailwind -->
<nav class="
flex flex-col gap-2 p-4
md:flex-row md:items-center md:justify-between md:px-8 md:py-4
lg:px-12
">
<!-- Mobile: coluna, padding pequeno -->
<!-- md: linha, centralizado, padding médio -->
<!-- lg: padding maior -->
</nav>
12.5.4 — Exemplos práticos de layout responsivo¶
Hero section responsiva:
<section class="
flex flex-col gap-8 px-4 py-12
md:flex-row md:items-center md:gap-12 md:px-8 md:py-20
lg:px-16 lg:py-28 lg:gap-16
max-w-7xl mx-auto
">
<div class="flex-1 space-y-6">
<h1 class="text-3xl font-bold leading-tight md:text-4xl lg:text-5xl">
Título principal da seção
</h1>
<p class="text-lg text-slate-600 leading-relaxed max-w-prose">
Descrição da seção...
</p>
<div class="flex flex-col sm:flex-row gap-3">
<a href="#" class="px-6 py-3 bg-blue-700 text-white font-semibold
rounded-lg text-center hover:bg-blue-800 transition-colors">
Ação primária
</a>
<a href="#" class="px-6 py-3 border-2 border-blue-700 text-blue-700
font-semibold rounded-lg text-center
hover:bg-blue-50 transition-colors">
Ação secundária
</a>
</div>
</div>
<div class="flex-1">
<img src="hero.jpg" alt="Imagem hero"
class="w-full rounded-2xl shadow-xl" />
</div>
</section>
12.6 — Estados e variantes¶
Vídeo curto explicativo (link será adicionado posteriormente)
12.6.1 — Pseudo-classes: hover:, focus:, active:, disabled:¶
<!-- hover -->
<button class="bg-blue-700 hover:bg-blue-800 text-white px-4 py-2 rounded-lg
transition-colors duration-200">
Botão com hover
</button>
<!-- focus e focus-visible -->
<input class="border border-slate-300 rounded-md px-3 py-2
focus:outline-none focus:border-blue-500
focus:ring-2 focus:ring-blue-500/20" />
<button class="focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-blue-600">
Foco visível apenas via teclado
</button>
<!-- active -->
<button class="active:scale-95 active:bg-blue-900 transition-transform">
Pressionar reduz levemente
</button>
<!-- disabled -->
<button class="disabled:opacity-50 disabled:cursor-not-allowed
disabled:pointer-events-none" disabled>
Desabilitado
</button>
<!-- group: aplica estilos a filhos quando o pai recebe hover *)
<div class="group flex items-center gap-3 p-4 rounded-lg
hover:bg-slate-100 cursor-pointer">
<div class="w-10 h-10 rounded-full bg-blue-100 group-hover:bg-blue-200
transition-colors">
</div>
<p class="font-medium group-hover:text-blue-700 transition-colors">
Texto que muda quando o card recebe hover
</p>
</div>
12.6.2 — Pseudo-elementos: before:, after:, placeholder:¶
<!-- placeholder -->
<input class="placeholder:text-slate-400 placeholder:italic
border rounded-md px-3 py-2"
placeholder="Digite aqui..." />
<!-- before e after *)
<div class="relative before:absolute before:inset-0
before:bg-black/40 before:rounded-lg">
<img src="foto.jpg" alt="Com overlay" class="rounded-lg" />
</div>
<!-- first: e last: — primeiro e último filho *)
<ul>
<li class="py-3 border-b border-slate-100 first:pt-0 last:border-0">
Item da lista
</li>
</ul>
<!-- odd: e even: — alternância de linhas *)
<tr class="odd:bg-white even:bg-slate-50">
<td class="px-4 py-3">...</td>
</tr>
12.6.3 — Estados de formulário¶
<!-- required, invalid, valid *)
<input class="border rounded-md px-3 py-2
required:border-slate-300
invalid:border-red-500 invalid:ring-2 invalid:ring-red-500/20
valid:border-green-500"
type="email" required />
<!-- checked (checkbox/radio) *)
<input class="accent-blue-700 w-4 h-4 cursor-pointer"
type="checkbox" />
<!-- peer: aplica estilo a irmão com base no estado do elemento *)
<div class="flex flex-col gap-1">
<input class="peer border rounded-md px-3 py-2
focus:outline-none focus:border-blue-500"
type="email" required />
<p class="hidden peer-invalid:block text-sm text-red-600">
E-mail inválido
</p>
</div>
12.6.4 — Dark mode: dark:¶
<!-- dark: aplica estilos quando o tema escuro está ativo *)
<div class="bg-white dark:bg-slate-900
text-slate-900 dark:text-slate-100
border border-slate-200 dark:border-slate-700
p-6 rounded-xl">
<h2 class="text-xl font-bold text-slate-900 dark:text-white">
Título adaptativo
</h2>
<p class="text-slate-600 dark:text-slate-400 mt-2">
Conteúdo que se adapta ao tema do sistema.
</p>
</div>
Por padrão, o modo escuro do Tailwind usa prefers-color-scheme: dark. Para controle manual via classe .dark no <html>, configure darkMode: 'class' no tailwind.config.js.
12.6.5 — Combinando variantes¶
Variantes podem ser combinadas livremente — a ordem de escrita segue a lógica {breakpoint}:{estado}:{utility}:
<!-- Responsivo + estado *)
<button class="
bg-blue-700 text-white
hover:bg-blue-800
md:text-lg md:px-8 md:py-4
md:hover:bg-blue-900
dark:bg-blue-600 dark:hover:bg-blue-500
disabled:opacity-50 disabled:cursor-not-allowed
transition-all duration-200
focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-300
">
Botão com múltiplas variantes
</button>
12.7 — Componentes com Tailwind¶
Vídeo curto explicativo (link será adicionado posteriormente)
12.7.1 — Construindo um botão com variantes¶
<!-- Botão primário *)
<button class="inline-flex items-center justify-center gap-2
px-4 py-2 rounded-lg font-semibold text-sm
bg-blue-700 text-white
hover:bg-blue-800 active:bg-blue-900
focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-blue-600
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors duration-200"
type="button">
Salvar
</button>
<!-- Botão secundário (outline) *)
<button class="inline-flex items-center justify-center gap-2
px-4 py-2 rounded-lg font-semibold text-sm
border-2 border-blue-700 text-blue-700 bg-transparent
hover:bg-blue-50 active:bg-blue-100
focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-blue-600
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors duration-200"
type="button">
Cancelar
</button>
<!-- Botão de perigo *)
<button class="inline-flex items-center justify-center gap-2
px-4 py-2 rounded-lg font-semibold text-sm
bg-red-600 text-white
hover:bg-red-700 active:bg-red-800
focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-red-600
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors duration-200"
type="button">
Excluir
</button>
12.7.2 — Card responsivo¶
<article class="bg-white rounded-xl shadow-md overflow-hidden
flex flex-col
hover:shadow-lg hover:-translate-y-1 transition-all duration-300
focus-within:outline focus-within:outline-2
focus-within:outline-blue-500">
<img src="imagem.jpg" alt="Descrição"
class="w-full h-48 object-cover" />
<div class="flex-1 p-6 flex flex-col gap-3">
<div class="flex items-center gap-2">
<span class="text-xs font-semibold text-blue-700 bg-blue-50
px-2.5 py-0.5 rounded-full uppercase tracking-wide">
Categoria
</span>
</div>
<h2 class="text-xl font-semibold text-slate-900 leading-snug">
Título do card
</h2>
<p class="text-slate-600 leading-relaxed text-sm flex-1">
Descrição do conteúdo do card que pode ser mais ou menos longa.
</p>
</div>
<div class="px-6 pb-6 flex items-center justify-between">
<span class="text-xs text-slate-400">15 mar. 2026</span>
<a href="#"
class="text-sm font-semibold text-blue-700 hover:text-blue-800
hover:underline focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-blue-600
rounded">
Leia mais →
</a>
</div>
</article>
12.7.3 — Navbar responsiva¶
<header class="bg-slate-900 text-white shadow-lg sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<!-- Logo *)
<a href="/" class="flex items-center gap-2 font-bold text-lg
hover:text-blue-400 transition-colors">
<img src="logo.svg" alt="IFAL" class="h-8 w-auto" />
<span>PWEB1</span>
</a>
<!-- Links desktop: ocultos em mobile *)
<nav class="hidden md:flex items-center gap-1"
aria-label="Navegação principal">
<a href="/"
class="px-3 py-2 rounded-md text-sm font-medium
text-white hover:bg-slate-700 transition-colors
aria-[current=page]:bg-slate-700"
aria-current="page">
Início
</a>
<a href="/capitulos"
class="px-3 py-2 rounded-md text-sm font-medium
text-slate-300 hover:text-white hover:bg-slate-700
transition-colors">
Capítulos
</a>
<a href="/atividades"
class="px-3 py-2 rounded-md text-sm font-medium
text-slate-300 hover:text-white hover:bg-slate-700
transition-colors">
Atividades
</a>
</nav>
<!-- Botão menu mobile *)
<button class="md:hidden p-2 rounded-md text-slate-400
hover:text-white hover:bg-slate-700 transition-colors
focus-visible:outline focus-visible:outline-2
focus-visible:outline-white"
aria-controls="menu-mobile"
aria-expanded="false"
aria-label="Abrir menu"
type="button">
<svg class="w-6 h-6" fill="none" stroke="currentColor"
viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
<!-- Menu mobile (toggle via JS) *)
<div class="md:hidden hidden" id="menu-mobile">
<nav class="px-2 pt-2 pb-3 space-y-1 border-t border-slate-700"
aria-label="Menu mobile">
<a href="/"
class="block px-3 py-2 rounded-md text-base font-medium
bg-slate-700 text-white">
Início
</a>
<a href="/capitulos"
class="block px-3 py-2 rounded-md text-base font-medium
text-slate-300 hover:text-white hover:bg-slate-700
transition-colors">
Capítulos
</a>
</nav>
</div>
</header>
12.7.4 — Formulário de contato estilizado¶
<form class="max-w-lg mx-auto bg-white rounded-2xl shadow-lg p-8 space-y-6"
action="/contato" method="post">
<div class="space-y-1">
<label class="block text-sm font-medium text-slate-700" for="nome">
Nome completo <span class="text-red-500" aria-hidden="true">*</span>
</label>
<input class="w-full px-3 py-2 border border-slate-300 rounded-lg
text-slate-900 placeholder:text-slate-400
focus:outline-none focus:border-blue-500
focus:ring-2 focus:ring-blue-500/20
transition-shadow duration-200"
type="text" id="nome" name="nome" required
placeholder="Maria Silva" />
</div>
<div class="space-y-1">
<label class="block text-sm font-medium text-slate-700" for="email">
E-mail <span class="text-red-500" aria-hidden="true">*</span>
</label>
<input class="w-full px-3 py-2 border border-slate-300 rounded-lg
text-slate-900 placeholder:text-slate-400
focus:outline-none focus:border-blue-500
focus:ring-2 focus:ring-blue-500/20
transition-shadow duration-200"
type="email" id="email" name="email" required
placeholder="maria@exemplo.com" />
</div>
<div class="space-y-1">
<label class="block text-sm font-medium text-slate-700" for="mensagem">
Mensagem <span class="text-red-500" aria-hidden="true">*</span>
</label>
<textarea class="w-full px-3 py-2 border border-slate-300 rounded-lg
text-slate-900 placeholder:text-slate-400
focus:outline-none focus:border-blue-500
focus:ring-2 focus:ring-blue-500/20
transition-shadow duration-200 resize-y min-h-[120px]"
id="mensagem" name="mensagem" rows="5" required
placeholder="Sua mensagem..."></textarea>
</div>
<button class="w-full px-6 py-3 bg-blue-700 text-white font-semibold
rounded-lg hover:bg-blue-800 active:bg-blue-900
focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-blue-600
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors duration-200"
type="submit">
Enviar mensagem
</button>
</form>
12.7.5 — A diretiva @apply: extraindo componentes reutilizáveis¶
Quando um mesmo conjunto de classes é repetido em muitos lugares, a diretiva @apply permite extrair essas classes para uma classe CSS reutilizável — mantendo os benefícios do utility-first sem duplicação:
/* src/input.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
/* Componente btn extraído com @apply *)
.btn {
@apply inline-flex items-center justify-center gap-2
px-4 py-2 rounded-lg font-semibold text-sm
transition-colors duration-200 cursor-pointer
focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2
disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-primario {
@apply bg-blue-700 text-white
hover:bg-blue-800 active:bg-blue-900
focus-visible:outline-blue-600;
}
.btn-secundario {
@apply border-2 border-blue-700 text-blue-700 bg-transparent
hover:bg-blue-50 active:bg-blue-100
focus-visible:outline-blue-600;
}
.btn-perigo {
@apply bg-red-600 text-white
hover:bg-red-700 active:bg-red-800
focus-visible:outline-red-600;
}
/* Componente campo extraído *)
.campo {
@apply flex flex-col gap-1;
}
.campo-label {
@apply block text-sm font-medium text-slate-700;
}
.campo-input {
@apply w-full px-3 py-2 border border-slate-300 rounded-lg
text-slate-900 placeholder:text-slate-400
focus:outline-none focus:border-blue-500
focus:ring-2 focus:ring-blue-500/20
transition-shadow duration-200;
}
}
Uso em HTML após extração:
<!-- Antes: classes repetidas em cada botão *)
<button class="inline-flex items-center px-4 py-2 rounded-lg font-semibold
text-sm bg-blue-700 text-white hover:bg-blue-800 ...">
Salvar
</button>
<!-- Depois: classes extraídas e reutilizáveis *)
<button class="btn btn-primario" type="button">Salvar</button>
<button class="btn btn-secundario" type="button">Cancelar</button>
<button class="btn btn-perigo" type="button">Excluir</button>
Quando usar
@apply: reserve@applypara componentes genuinamente reutilizados em múltiplos lugares — botões, campos de formulário, badges. Para elementos únicos, mantenha as classes diretamente no HTML. O uso excessivo de@applyreconstrói o CSS semântico que o utility-first foi criado para evitar.
12.8 — Customização do Tailwind¶
Vídeo curto explicativo (link será adicionado posteriormente)
12.8.1 — Estendendo o tema: cores, fontes e espaçamentos personalizados¶
theme.extend adiciona valores ao tema padrão sem substituí-lo:
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{html,js}'],
theme: {
extend: {
// Cores customizadas — adicionadas à paleta existente
colors: {
'ifal': {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
700: '#1d4ed8',
900: '#1e3a8a',
},
'destaque': '#E8632A',
},
// Fontes customizadas
fontFamily: {
'sans': ['Inter', 'system-ui', 'sans-serif'],
'serif': ['Merriweather', 'Georgia', 'serif'],
'mono': ['Fira Code', 'Consolas', 'monospace'],
},
// Espaçamentos adicionais à escala existente
spacing: {
'18': '4.5rem',
'22': '5.5rem',
'88': '22rem',
'112': '28rem',
'128': '32rem',
},
// Border radius customizado
borderRadius: {
'4xl': '2rem',
'5xl': '2.5rem',
},
// Breakpoints adicionais
screens: {
'xs': '475px',
'3xl': '1920px',
},
// max-width customizado
maxWidth: {
'8xl': '88rem',
'9xl': '96rem',
},
},
},
}
12.8.2 — Sobrescrevendo valores padrão¶
theme.XXX (sem extend) substitui completamente os valores padrão daquela categoria:
module.exports = {
theme: {
// SUBSTITUI todos os breakpoints — use com cuidado
screens: {
'mobile': '375px',
'tablet': '768px',
'desktop': '1024px',
'wide': '1440px',
},
// SUBSTITUI todas as cores — o projeto não terá blue, red, etc.
// Use extend para ADICIONAR; use theme para SUBSTITUIR
colors: {
transparent: 'transparent',
current: 'currentColor',
white: '#ffffff',
black: '#000000',
primaria: { /* escala completa */ },
},
extend: {
// Adições aqui não afetam as substituições acima
},
},
}
12.8.3 — Criando utilitários customizados com @layer¶
Para propriedades CSS não cobertas pelo Tailwind, @layer utilities adiciona utilities customizadas que se comportam como as nativas — incluindo suporte a variantes responsivas e de estado:
@layer utilities {
/* Utility customizada: scrollbar oculta *)
.scrollbar-oculto {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-oculto::-webkit-scrollbar {
display: none;
}
/* Texto com gradiente *)
.texto-gradiente {
background-image: linear-gradient(135deg, #1d4ed8, #E8632A);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Fundo com padrão *)
.bg-grade {
background-image:
linear-gradient(rgba(0,0,0,0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,0,0,0.05) 1px, transparent 1px);
background-size: 20px 20px;
}
}
12.8.4 — Relação entre tokens do Design System e o tema Tailwind¶
O Capítulo 11 construiu um Design System baseado em variáveis CSS com hierarquia de tokens. O Tailwind pode ser configurado para consumir esses tokens — mantendo consistência entre o sistema de design e o framework:
// tailwind.config.js — consumindo tokens CSS como variáveis
module.exports = {
theme: {
extend: {
colors: {
// Referencia as variáveis CSS dos tokens semânticos
'primaria': 'var(--cor-primaria)',
'secundaria': 'var(--cor-secundaria)',
'destaque': 'var(--cor-destaque)',
'fundo': 'var(--cor-fundo-pagina)',
'texto': 'var(--cor-texto-padrao)',
'texto-2': 'var(--cor-texto-secundario)',
'borda': 'var(--cor-borda-padrao)',
'sucesso': 'var(--cor-sucesso)',
'erro': 'var(--cor-erro)',
'aviso': 'var(--cor-aviso)',
},
fontFamily: {
'sem-serifa': 'var(--fonte-sem-serifa)',
'serifa': 'var(--fonte-serifa)',
'codigo': 'var(--fonte-codigo)',
},
borderRadius: {
'sm': 'var(--raio-sm)',
'md': 'var(--raio-md)',
'lg': 'var(--raio-lg)',
'xl': 'var(--raio-xl)',
'full': 'var(--raio-circulo)',
},
},
},
}
<!-- Usando tokens do Design System via Tailwind *)
<button class="bg-primaria text-white font-semibold px-4 py-2
rounded-md hover:bg-blue-800 transition-colors">
Usa var(--cor-primaria)
</button>
<!-- Tema escuro funciona automaticamente porque --cor-primaria
é redefinida na media query prefers-color-scheme: dark *)
Esta integração fecha o arco entre os Capítulos 11 e 12: o Design System define os tokens e as decisões de design; o Tailwind consome esses tokens como classes utilitárias. Mudanças nos tokens se propagam tanto para o CSS customizado quanto para o Tailwind — mantendo consistência em todo o projeto.
Referências: - Tailwind CSS — Documentação oficial - Tailwind CSS — Customizing the theme - Tailwind CSS — Using CSS variables - Adam Wathan — CSS Utility Classes and "Separation of Concerns" - State of CSS 2024
Atividades — Capítulo 12¶
1. Qual é a diferença fundamental entre um framework utility-first como Tailwind e um framework component-based como Bootstrap?
2. Um elemento tem as classes text-base md:text-lg lg:text-2xl. Em um viewport de 900px, qual tamanho de fonte é aplicado?
3. Quando é apropriado usar a diretiva @apply no Tailwind?
- GitHub Classroom: Reestilizar a landing page do 2º Bimestre usando Tailwind CSS: instalar via npm, configurar o tema com as cores e fontes do Design System do Capítulo 11, implementar a navbar responsiva com menu mobile, a seção de cards com hover states e o formulário de contato — tudo sem CSS customizado, usando apenas Tailwind. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 11 — Variáveis CSS e Design System :material-arrow-right: Ir ao Capítulo 13 — JavaScript Essencial
Capítulo 13 — JavaScript Essencial¶
Vídeo curto explicativo (link será adicionado posteriormente)
13.1 — JavaScript no navegador: contexto e papel¶
Vídeo curto explicativo (link será adicionado posteriormente)
O JavaScript é a linguagem de programação nativa do navegador web — o único ambiente de execução de código que opera diretamente no cliente, sem necessidade de instalação ou plugin. Criado em 1995 por Brendan Eich na Netscape Communications em apenas dez dias, o JavaScript foi concebido originalmente como uma linguagem de scripting para adicionar comportamento interativo simples a páginas HTML. Nas três décadas seguintes, evoluiu para uma das linguagens de programação mais amplamente utilizadas do mundo, presente em navegadores, servidores, dispositivos embarcados e aplicações de desktop.
13.1.1 — O que é JavaScript e sua história¶
JavaScript é uma linguagem interpretada, dinamicamente tipada, multiparadigma — suportando programação imperativa, orientada a objetos (com protótipos) e funcional — e single-threaded (executada em uma única thread). Sua especificação formal é mantida pela ECMA International sob o nome ECMAScript (ES). A versão ES5 (2009) modernizou a linguagem; o ES6/ES2015 representou a maior atualização da história, introduzindo let, const, arrow functions, classes, Promises, módulos e dezenas de outras funcionalidades. Desde então, novas versões são publicadas anualmente.
Nota terminológica: JavaScript e ECMAScript são frequentemente usados como sinônimos. Tecnicamente, ECMAScript é a especificação; JavaScript é a implementação da especificação pelo navegador (e pelo Node.js). Ao longo deste capítulo, usaremos JavaScript como termo geral.
A padronização da linguagem foi motivada por uma guerra de incompatibilidades nos anos 1990, quando diferentes navegadores implementavam versões conflitantes. A criação do padrão ECMAScript pelo consórcio ECMA em 1997 iniciou a convergência que tornaria o JavaScript universal.
13.1.2 — JavaScript no navegador vs Node.js: o mesmo idioma, contextos diferentes¶
Na disciplina de Introdução à Programação, você provavelmente trabalhou com JavaScript no contexto do Node.js — um ambiente de execução JavaScript no servidor, criado em 2009, que permitiu usar a linguagem fora do navegador. É importante compreender que Node.js e o navegador compartilham o mesmo núcleo da linguagem (ECMAScript), mas operam em contextos com APIs completamente diferentes:
| JavaScript no Navegador | JavaScript no Node.js | |
|---|---|---|
| Ambiente | Navegador (Chrome, Firefox...) | Servidor / terminal |
| Objeto global | window |
global / globalThis |
| APIs exclusivas | DOM, BOM, Fetch, Web APIs | fs, http, path, process |
| Acesso ao sistema | Restrito (sandbox) | Amplo (arquivos, rede, SO) |
| Entrada do usuário | Eventos de mouse, teclado, formulários | process.stdin, argumentos |
| Saída visual | DOM — manipula HTML/CSS | Terminal / arquivos |
O núcleo da linguagem — tipos de dados, funções, arrays, objetos, loops, condicionais, Promises, async/await — é idêntico em ambos os contextos. O que muda são as APIs disponíveis para interagir com o ambiente.
Neste capítulo e nos seguintes, o foco é o JavaScript no navegador: como ele interage com o HTML via DOM, como responde a eventos do usuário e como busca dados de servidores via Fetch API.
13.1.3 — Como o navegador executa JavaScript: o event loop e a thread única¶
Compreender o modelo de execução do JavaScript no navegador é fundamental para evitar erros comuns e escrever código assíncrono corretamente.
O JavaScript é single-threaded — executa em uma única thread, processando uma operação por vez. Isso significa que não há paralelismo nativo: enquanto um bloco de código está em execução, nenhum outro código JavaScript pode ser executado simultaneamente.
O mecanismo que permite ao JavaScript lidar com operações demoradas (requisições de rede, timers, eventos do usuário) sem bloquear a interface é o event loop:
┌─────────────────────────────────┐
│ Call Stack │ ← onde o código é executado
│ (pilha de chamadas de função) │
└────────────────┬────────────────┘
│
┌────────────────▼────────────────┐
│ Event Loop │ ← monitora stack + queue
└────────────────┬────────────────┘
│
┌────────────────▼────────────────┐
│ Callback Queue │ ← callbacks aguardando execução
│ (eventos, timers, fetch...) │
└─────────────────────────────────┘
O funcionamento é: quando uma operação assíncrona (como fetch() ou setTimeout()) é iniciada, ela é delegada ao navegador (Web APIs). Quando completa, seu callback é colocado na fila (queue). O event loop verifica continuamente: se a call stack estiver vazia, pega o próximo callback da fila e o executa.
Consequência prática: código JavaScript que bloqueia a call stack por muito tempo (loops longos, computação pesada) trava a interface do usuário — o navegador não consegue processar eventos nem re-renderizar a página enquanto a stack não esvazia. Por isso, operações demoradas devem ser assíncronas.
13.1.4 — Como incluir JavaScript no HTML: <script>, defer e async¶
Existem três formas de incluir JavaScript em um documento HTML, cada uma com comportamento de carregamento distinto:
Inline (no corpo do HTML):
<script>
console.log('Executado imediatamente ao ser encontrado pelo parser');
</script>
Arquivo externo — sem atributos (bloqueante):
<!-- O parser HTML pausa, baixa e executa o script, depois continua -->
<script src="js/script.js"></script>
Arquivo externo — com defer (recomendado):
<!-- Download paralelo; executa apenas após o HTML ser completamente parseado -->
<script src="js/script.js" defer></script>
Arquivo externo — com async:
<!-- Download paralelo; executa imediatamente quando o download termina
(pode interromper o parsing do HTML) -->
<script src="js/script.js" async></script>
Imagem sugerida: diagrama comparativo mostrando a linha do tempo de parsing HTML, download e execução do script para os três comportamentos (sem atributo,
defer,async) — ilustrando visualmente por quedeferé a escolha mais segura para scripts que dependem do DOM.(imagem será adicionada posteriormente)
Regra prática: use sempre defer para scripts que interagem com o DOM. Use async apenas para scripts completamente independentes (como analytics). Nunca coloque scripts sem atributos no <head> — coloque-os antes de </body> ou use defer.
<!-- Padrão recomendado -->
<head>
<meta charset="UTF-8" />
<title>Página</title>
<link rel="stylesheet" href="css/style.css" />
<!-- defer: baixa em paralelo, executa após o DOM estar pronto -->
<script src="js/app.js" defer></script>
</head>
13.1.5 — O console do navegador como ambiente de aprendizado¶
O Console do DevTools (F12 → aba Console) é o ambiente interativo mais imediato para experimentar JavaScript. Ele funciona como um REPL (Read-Eval-Print Loop) — você digita uma expressão, pressiona Enter, e o resultado é exibido imediatamente.
// Exemplos para experimentar no console do navegador
// Saída no console
console.log('Olá, navegador!');
console.warn('Aviso');
console.error('Erro');
console.table([{ nome: 'Ana', nota: 9 }, { nome: 'Bruno', nota: 7 }]);
// Expressões são avaliadas imediatamente
2 + 2 // → 4
'olá' + ' mundo' // → "olá mundo"
typeof 42 // → "number"
// Variáveis persistem durante a sessão
let x = 10;
x * 3; // → 30
13.2 — Variáveis, tipos e operadores¶
Vídeo curto explicativo (link será adicionado posteriormente)
13.2.1 — var, let e const: diferenças e boas práticas¶
JavaScript possui três palavras-chave para declaração de variáveis, com comportamentos distintos em relação a escopo, reatribuição e hoisting:
// var — escopo de função, sofre hoisting, evitar em código moderno
var nome = 'Ana';
var nome = 'Bruno'; // redeclaração permitida — fonte de bugs
// let — escopo de bloco, pode ser reatribuída, não pode ser redeclarada
let contador = 0;
contador = 1; // reatribuição permitida
// let contador = 2; // erro: já foi declarada neste escopo
// const — escopo de bloco, não pode ser reatribuída após a declaração
const PI = 3.14159;
// PI = 3; // erro: assignment to constant variable
// IMPORTANTE: const não significa "imutável" — significa "não reatribuível"
// Objetos e arrays declarados com const podem ter seu conteúdo alterado
const usuario = { nome: 'Ana', idade: 20 };
usuario.idade = 21; // permitido: alterando propriedade
// usuario = {}; // erro: reatribuindo a variável
Regra prática moderna:
- Use const por padrão para tudo
- Use let apenas quando a variável precisar ser reatribuída (contadores, acumuladores)
- Nunca use var em código novo — seu comportamento de escopo e hoisting é fonte de bugs
var |
let |
const |
|
|---|---|---|---|
| Escopo | Função | Bloco | Bloco |
| Hoisting | Sim (com undefined) |
Sim (TDZ) | Sim (TDZ) |
| Reatribuição | Sim | Sim | Não |
| Redeclaração | Sim | Não | Não |
13.2.2 — Tipos primitivos¶
JavaScript possui seis tipos primitivos e um tipo de objeto:
// string — sequência de caracteres
const nome = 'Maria';
const mensagem = "Olá, mundo!";
const template = `Olá, ${nome}!`; // template literal
// number — todos os números (inteiros e decimais)
const inteiro = 42;
const decimal = 3.14;
const negativo = -7;
const infinito = Infinity;
const naoNumero = NaN; // Not a Number — resultado de operação inválida
// boolean
const ativo = true;
const arquivado = false;
// null — ausência intencional de valor
const semValor = null;
// undefined — variável declarada mas não inicializada
let naoInicializada;
console.log(naoInicializada); // → undefined
// symbol — identificador único (uso avançado)
const id = Symbol('id');
// bigint — inteiros arbitrariamente grandes
const grandeNumero = 9007199254740991n;
// typeof: verifica o tipo de um valor
typeof 'texto' // → "string"
typeof 42 // → "number"
typeof true // → "boolean"
typeof undefined // → "undefined"
typeof null // → "object" ← bug histórico da linguagem
typeof {} // → "object"
typeof [] // → "object"
typeof function(){} // → "function"
13.2.3 — Tipagem dinâmica e coerção de tipos¶
JavaScript é dinamicamente tipado — o tipo de uma variável é determinado pelo valor que ela contém em determinado momento, não por uma declaração explícita. Uma mesma variável pode conter diferentes tipos ao longo da execução.
A coerção de tipos (type coercion) é a conversão automática entre tipos realizada pelo JavaScript em determinadas operações — um comportamento que frequentemente surpreende desenvolvedores iniciantes:
// Coerção implícita — acontece automaticamente
'5' + 3 // → "53" (number coercido para string)
'5' - 3 // → 2 (string coercida para number)
'5' * '3' // → 15 (ambas coercidas para number)
true + 1 // → 2 (true vira 1)
false + 1 // → 1 (false vira 0)
null + 1 // → 1 (null vira 0)
undefined + 1 // → NaN
// Comparação com coerção (==) vs sem coerção (===)
5 == '5' // → true (coerção: '5' vira 5)
5 === '5' // → false (sem coerção: tipos diferentes)
null == undefined // → true
null === undefined // → false
// Conversão explícita — sempre preferível à implícita
Number('42') // → 42
Number('') // → 0
Number('abc') // → NaN
String(42) // → "42"
Boolean(0) // → false
Boolean('') // → false
Boolean(null) // → false
Boolean(undefined) // → false
Boolean(NaN) // → false
Boolean([]) // → true (array vazio é truthy!)
Boolean({}) // → true (objeto vazio é truthy!)
parseInt('42px') // → 42
parseFloat('3.14m') // → 3.14
Boa prática: sempre use
===(igualdade estrita) e!==em vez de==e!=. A igualdade estrita não realiza coerção de tipos, produzindo resultados mais previsíveis.
13.2.4 — Operadores aritméticos, de comparação e lógicos¶
// ── Aritméticos ──
10 + 3 // → 13
10 - 3 // → 7
10 * 3 // → 30
10 / 3 // → 3.3333...
10 % 3 // → 1 (resto da divisão)
10 ** 3 // → 1000 (exponenciação)
// Incremento e decremento
let x = 5;
x++; // x = 6 (pós-incremento)
++x; // x = 7 (pré-incremento)
x--; // x = 6
--x; // x = 5
// Atribuição composta
x += 10; // x = x + 10
x -= 5; // x = x - 5
x *= 2; // x = x * 2
x /= 4; // x = x / 4
x **= 2; // x = x ** 2
x %= 3; // x = x % 3
// ── Comparação ──
5 > 3 // → true
5 < 3 // → false
5 >= 5 // → true
5 <= 4 // → false
5 === 5 // → true (igualdade estrita — recomendada)
5 !== 3 // → true (desigualdade estrita)
// ── Lógicos ──
true && true // → true (E lógico)
true && false // → false
false || true // → true (OU lógico)
false || false // → false
!true // → false (NÃO lógico)
!false // → true
// Short-circuit evaluation
false && expressaoCara() // expressaoCara() NÃO é chamada
true || expressaoCara() // expressaoCara() NÃO é chamada
// Uso prático do short-circuit
const nome = usuario.nome || 'Anônimo'; // fallback se nome for falsy
const exibir = estaLogado && renderizarPerfil(); // executa apenas se logado
13.2.5 — Template literals¶
Template literals são strings delimitadas por backticks (`) que permitem interpolação de expressões e strings multilinhas:
const nome = 'Ana';
const nota = 9.5;
// Interpolação de variáveis e expressões
const mensagem = `Olá, ${nome}! Sua nota foi ${nota}.`;
// → "Olá, Ana! Sua nota foi 9.5."
const calculo = `Resultado: ${10 * 3 + 5}`;
// → "Resultado: 35"
// Expressões complexas
const aprovado = `${nome} foi ${nota >= 7 ? 'aprovada' : 'reprovada'}.`;
// → "Ana foi aprovada."
// String multilinha — sem necessidade de \n
const html = `
<article class="card">
<h2>${nome}</h2>
<p>Nota: ${nota}</p>
</article>
`;
13.2.6 — Nullish coalescing (??) e optional chaining (?.)¶
Dois operadores modernos do ES2020 que simplificam o tratamento de valores nulos:
// Nullish coalescing (??) — retorna o lado direito apenas se o esquerdo
// for null ou undefined (diferente de ||, que reage a qualquer falsy)
const nome = null ?? 'Anônimo'; // → "Anônimo"
const porto = undefined ?? 'Maceió'; // → "Maceió"
const zero = 0 ?? 42; // → 0 (|| retornaria 42)
const vazio = '' ?? 'padrão'; // → '' (|| retornaria 'padrão')
// Optional chaining (?.) — acessa propriedades sem lançar erro se o objeto
// for null ou undefined
const usuario = null;
const cidade = usuario?.endereco?.cidade; // → undefined (sem erro)
// sem ?.: usuario.endereco lançaria TypeError
const usuarios = [{ nome: 'Ana' }, null];
const primeiroNome = usuarios[0]?.nome; // → "Ana"
const segundoNome = usuarios[1]?.nome; // → undefined (sem erro)
// Combinando ?? com ?.
const cidade = usuario?.endereco?.cidade ?? 'Cidade não informada';
13.3 — Funções e escopo¶
Vídeo curto explicativo (link será adicionado posteriormente)
13.3.1 — Declaração de função vs expressão de função¶
// Declaração de função (function declaration)
// Sofre hoisting completo — pode ser chamada antes de ser declarada
function saudacao(nome) {
return `Olá, ${nome}!`;
}
console.log(saudacao('Maria')); // → "Olá, Maria!"
// Expressão de função (function expression)
// NÃO sofre hoisting — deve ser declarada antes de ser chamada
const saudacao = function(nome) {
return `Olá, ${nome}!`;
};
// Função nomeada — útil para debug e recursão
const fatorial = function calcularFatorial(n) {
return n <= 1 ? 1 : n * calcularFatorial(n - 1);
};
13.3.2 — Arrow functions¶
Arrow functions são uma sintaxe concisa para funções introduzida no ES6. Além da brevidade, possuem uma diferença semântica importante em relação ao this — relevante no contexto do DOM (Capítulo 14):
// Sintaxe básica
const dobrar = (n) => n * 2;
const somar = (a, b) => a + b;
// Parênteses opcionais com um único parâmetro
const quadrado = n => n * n;
// Sem parâmetros: parênteses obrigatórios
const saudacao = () => 'Olá!';
// Corpo com múltiplas linhas: chaves e return explícito
const calcularImc = (peso, altura) => {
const imc = peso / (altura * altura);
return imc.toFixed(2);
};
// Retorno de objeto literal: envolva em parênteses
const criarUsuario = (nome, idade) => ({ nome, idade });
// sem parênteses, as chaves seriam interpretadas como corpo da função
// Arrow functions são ideais como callbacks
const numeros = [1, 2, 3, 4, 5];
const dobrados = numeros.map(n => n * 2); // → [2, 4, 6, 8, 10]
const pares = numeros.filter(n => n % 2 === 0); // → [2, 4]
13.3.3 — Parâmetros padrão e rest parameters¶
// Parâmetros padrão (default parameters)
function cumprimentar(nome, saudacao = 'Olá') {
return `${saudacao}, ${nome}!`;
}
cumprimentar('Ana'); // → "Olá, Ana!"
cumprimentar('Ana', 'Bem-vinda'); // → "Bem-vinda, Ana!"
// Rest parameters (...) — agrupa argumentos extras em um array
function somar(...numeros) {
return numeros.reduce((total, n) => total + n, 0);
}
somar(1, 2, 3) // → 6
somar(1, 2, 3, 4, 5) // → 15
// Combinando parâmetros normais com rest
function log(nivel, ...mensagens) {
console.log(`[${nivel}]`, ...mensagens);
}
log('INFO', 'Servidor iniciado', 'porta 3000');
// → [INFO] Servidor iniciado porta 3000
13.3.4 — Escopo: global, de função e de bloco¶
O escopo determina onde uma variável é acessível. JavaScript possui três níveis:
// Escopo global — acessível em qualquer lugar
const APP_NOME = 'PWEB1';
function demonstrar() {
// Escopo de função — acessível apenas dentro da função
const local = 'variável local';
console.log(APP_NOME); // → 'PWEB1' (acessa escopo global)
console.log(local); // → 'variável local'
if (true) {
// Escopo de bloco (let e const) — acessível apenas dentro do bloco
let bloco = 'variável de bloco';
var funcao = 'variável de função'; // var ignora o bloco!
console.log(bloco); // → 'variável de bloco'
}
// console.log(bloco); // ReferenceError: bloco is not defined
console.log(funcao); // → 'variável de função' (var ignora o bloco)
}
// console.log(local); // ReferenceError: local is not defined
13.3.5 — Hoisting¶
Hoisting é o comportamento pelo qual declarações de funções e variáveis são "elevadas" para o topo do seu escopo antes da execução:
// Declarações de função sofrem hoisting completo
console.log(somar(2, 3)); // → 5 — funciona antes da declaração!
function somar(a, b) {
return a + b;
}
// var sofre hoisting mas é inicializado como undefined
console.log(x); // → undefined (não lança erro)
var x = 10;
console.log(x); // → 10
// let e const sofrem hoisting mas ficam na Temporal Dead Zone (TDZ)
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 20;
// Expressões de função NÃO sofrem hoisting
// console.log(multiplicar(2, 3)); // TypeError: multiplicar is not a function
const multiplicar = (a, b) => a * b;
13.3.6 — Closures: conceito e casos de uso práticos¶
Uma closure é a capacidade de uma função de "lembrar" e acessar variáveis do escopo onde foi criada, mesmo após esse escopo ter encerrado sua execução. É um dos conceitos mais poderosos e fundamentais do JavaScript:
// Exemplo fundamental de closure
function criarContador() {
let contagem = 0; // variável do escopo externo
return function() {
contagem++;
return contagem;
};
}
const contador = criarContador();
console.log(contador()); // → 1
console.log(contador()); // → 2
console.log(contador()); // → 3
// 'contagem' é inacessível diretamente, mas a função interna a "lembra"
// Closure com parâmetro — fábrica de funções
function multiplicadorDe(fator) {
return (numero) => numero * fator;
}
const dobrar = multiplicadorDe(2);
const triplicar = multiplicadorDe(3);
dobrar(5) // → 10
triplicar(5) // → 15
// Caso de uso prático: encapsulamento de estado
function criarCarrinho() {
const itens = []; // privado — inacessível externamente
return {
adicionar(produto) { itens.push(produto); },
remover(nome) {
const idx = itens.findIndex(i => i.nome === nome);
if (idx !== -1) itens.splice(idx, 1);
},
total() { return itens.reduce((s, i) => s + i.preco, 0); },
listar() { return [...itens]; }
};
}
const carrinho = criarCarrinho();
carrinho.adicionar({ nome: 'Curso JS', preco: 99 });
carrinho.adicionar({ nome: 'Livro CSS', preco: 49 });
console.log(carrinho.total()); // → 148
13.4 — Arrays e objetos¶
Vídeo curto explicativo (link será adicionado posteriormente)
13.4.1 — Arrays: criação, acesso e métodos essenciais¶
// Criação
const frutas = ['maçã', 'banana', 'laranja'];
const numeros = [1, 2, 3, 4, 5];
const misto = [1, 'texto', true, null, { chave: 'valor' }];
const vazio = [];
const tamanho5 = new Array(5).fill(0); // → [0, 0, 0, 0, 0]
// Acesso por índice (começa em 0)
frutas[0] // → 'maçã'
frutas[2] // → 'laranja'
frutas.at(-1) // → 'laranja' (último elemento — ES2022)
// Propriedade length
frutas.length // → 3
// Adição e remoção
frutas.push('uva'); // adiciona ao final → ['maçã','banana','laranja','uva']
frutas.pop(); // remove do final → ['maçã','banana','laranja']
frutas.unshift('morango');// adiciona ao início → ['morango','maçã','banana','laranja']
frutas.shift(); // remove do início → ['maçã','banana','laranja']
// splice: remove/insere em posição específica
frutas.splice(1, 1); // remove 1 elemento a partir do índice 1
frutas.splice(1, 0, 'pêra', 'kiwi'); // insere sem remover
// slice: extrai sem modificar o original
const primeiras = frutas.slice(0, 2); // cópia dos 2 primeiros
const ultimas = frutas.slice(-2); // cópia dos 2 últimos
// indexOf e includes
frutas.indexOf('banana') // → 1 (ou -1 se não encontrar)
frutas.includes('laranja') // → true
// join e split
frutas.join(', ') // → "maçã, banana, laranja"
'a,b,c'.split(',') // → ['a', 'b', 'c']
// sort e reverse
[3,1,4,1,5].sort((a, b) => a - b) // → [1, 1, 3, 4, 5]
frutas.reverse() // inverte no lugar
13.4.2 — Métodos funcionais: map, filter, reduce, find, some, every¶
Estes métodos são fundamentais no JavaScript moderno — especialmente para manipular dados vindos de APIs:
const alunos = [
{ nome: 'Ana', nota: 9.5, aprovado: true },
{ nome: 'Bruno', nota: 5.0, aprovado: false },
{ nome: 'Carla', nota: 8.2, aprovado: true },
{ nome: 'Diego', nota: 6.8, aprovado: true },
];
// map — transforma cada elemento, retorna novo array de mesmo tamanho
const nomes = alunos.map(a => a.nome);
// → ['Ana', 'Bruno', 'Carla', 'Diego']
const notas = alunos.map(a => a.nota);
// → [9.5, 5.0, 8.2, 6.8]
const resumos = alunos.map(a =>
`${a.nome}: ${a.aprovado ? '✓' : '✗'}`
);
// → ['Ana: ✓', 'Bruno: ✗', 'Carla: ✓', 'Diego: ✓']
// filter — retorna novo array com elementos que satisfazem a condição
const aprovados = alunos.filter(a => a.aprovado);
// → [{Ana}, {Carla}, {Diego}]
const notaAlta = alunos.filter(a => a.nota >= 8);
// → [{Ana}, {Carla}]
// reduce — acumula todos os elementos em um único valor
const somaNotas = alunos.reduce((soma, a) => soma + a.nota, 0);
// → 29.5
const mediaNotas = somaNotas / alunos.length;
// → 7.375
// Reduce para construir objeto
const porNome = alunos.reduce((acc, a) => {
acc[a.nome] = a.nota;
return acc;
}, {});
// → { Ana: 9.5, Bruno: 5.0, Carla: 8.2, Diego: 6.8 }
// find — retorna o PRIMEIRO elemento que satisfaz a condição (ou undefined)
const primeiroAprovado = alunos.find(a => a.aprovado);
// → { nome: 'Ana', nota: 9.5, aprovado: true }
const alunoInexistente = alunos.find(a => a.nome === 'Eva');
// → undefined
// findIndex — como find, mas retorna o índice
const idxBruno = alunos.findIndex(a => a.nome === 'Bruno'); // → 1
// some — verdadeiro se PELO MENOS UM elemento satisfaz a condição
const algumReprovado = alunos.some(a => !a.aprovado); // → true
// every — verdadeiro se TODOS os elementos satisfazem a condição
const todosAprovados = alunos.every(a => a.aprovado); // → false
// flat e flatMap
const matriz = [[1, 2], [3, 4], [5]];
matriz.flat() // → [1, 2, 3, 4, 5]
const frasesComPalavras = ['olá mundo', 'bom dia'];
frasesComPalavras.flatMap(f => f.split(' '));
// → ['olá', 'mundo', 'bom', 'dia']
// Encadeamento de métodos — muito comum na prática
const mediasAprovados = alunos
.filter(a => a.aprovado)
.map(a => a.nota)
.reduce((soma, nota, _, arr) => soma + nota / arr.length, 0);
// média das notas apenas dos aprovados
13.4.3 — Objetos: criação, acesso e manipulação¶
// Criação de objeto literal
const usuario = {
nome: 'Maria Silva',
idade: 22,
email: 'maria@exemplo.com',
ativo: true,
endereco: { // objeto aninhado
cidade: 'Maceió',
estado: 'AL'
},
hobbies: ['leitura', 'programação'], // array como propriedade
saudacao() { // método
return `Olá, sou ${this.nome}`;
}
};
// Acesso a propriedades
usuario.nome // → 'Maria Silva' (notação de ponto)
usuario['email'] // → 'maria@exemplo.com' (notação de colchetes)
usuario.endereco.cidade // → 'Maceió'
usuario.hobbies[0] // → 'leitura'
usuario.saudacao() // → 'Olá, sou Maria Silva'
// Propriedade dinâmica (nome da propriedade em variável)
const campo = 'nome';
usuario[campo] // → 'Maria Silva'
// Adição e modificação de propriedades
usuario.curso = 'Sistemas de Informação'; // adiciona
usuario.idade = 23; // modifica
// Remoção
delete usuario.ativo;
// Verificação de existência
'nome' in usuario // → true
usuario.hasOwnProperty('curso') // → true
// Shorthand property — quando variável e chave têm o mesmo nome
const nome = 'João';
const idade = 25;
const pessoa = { nome, idade }; // equivale a { nome: nome, idade: idade }
// Computed property names — nome de chave dinâmico
const chave = 'cor';
const objeto = { [chave]: 'azul' }; // → { cor: 'azul' }
13.4.4 — Desestruturação de arrays e objetos¶
A desestruturação (destructuring) permite extrair valores de arrays e objetos de forma concisa:
// Desestruturação de array
const coordenadas = [10, 20, 30];
const [x, y, z] = coordenadas;
// x → 10, y → 20, z → 30
// Ignorando elementos
const [primeiro, , terceiro] = coordenadas;
// primeiro → 10, terceiro → 30
// Com valor padrão
const [a = 0, b = 0, c = 0, d = 0] = [1, 2];
// a→1, b→2, c→0, d→0
// Troca de variáveis sem temporária
let p = 1, q = 2;
[p, q] = [q, p]; // p→2, q→1
// Desestruturação de objeto
const usuario = { nome: 'Ana', idade: 22, cidade: 'Maceió' };
const { nome, idade } = usuario;
// nome → 'Ana', idade → 22
// Com renomeação
const { nome: nomeUsuario, cidade: localidade } = usuario;
// nomeUsuario → 'Ana', localidade → 'Maceió'
// Com valor padrão
const { nome: n, curso = 'Não informado' } = usuario;
// n → 'Ana', curso → 'Não informado'
// Desestruturação aninhada
const { endereco: { cidade, estado } } = {
endereco: { cidade: 'Maceió', estado: 'AL' }
};
// Desestruturação em parâmetros de função
function exibirUsuario({ nome, idade, curso = 'SI' }) {
return `${nome}, ${idade} anos — ${curso}`;
}
exibirUsuario({ nome: 'Ana', idade: 22 });
// → "Ana, 22 anos — SI"
13.4.5 — Spread operator e rest em objetos¶
// Spread em arrays — expande elementos
const a = [1, 2, 3];
const b = [4, 5, 6];
const combinado = [...a, ...b]; // → [1, 2, 3, 4, 5, 6]
const copia = [...a]; // cópia superficial
// Spread em objetos — copia e/ou mescla propriedades
const base = { tema: 'claro', idioma: 'pt-BR' };
const extensao = { idioma: 'en-US', fonte: 'Inter' };
const configuracao = { ...base, ...extensao };
// → { tema: 'claro', idioma: 'en-US', fonte: 'Inter' }
// 'idioma' de extensao sobrescreve o de base
// Cópia com modificação (padrão imutável — muito usado com estado)
const usuarioAtualizado = { ...usuario, idade: 23 };
// cria novo objeto com todos os campos, mas idade = 23
// Rest em objetos — captura o restante
const { nome, ...restante } = usuario;
// nome → 'Ana'
// restante → { idade: 22, cidade: 'Maceió' }
13.4.6 — Métodos estáticos: Object.keys(), Object.values(), Object.entries()¶
const produto = {
nome: 'Notebook',
preco: 3500,
estoque: 12
};
// Object.keys — array de chaves
Object.keys(produto) // → ['nome', 'preco', 'estoque']
// Object.values — array de valores
Object.values(produto) // → ['Notebook', 3500, 12]
// Object.entries — array de pares [chave, valor]
Object.entries(produto)
// → [['nome','Notebook'], ['preco',3500], ['estoque',12]]
// Iterando sobre um objeto com for...of + entries
for (const [chave, valor] of Object.entries(produto)) {
console.log(`${chave}: ${valor}`);
}
// Object.assign — copia propriedades
const destino = {};
Object.assign(destino, produto, { desconto: 10 });
// equivale a spread: { ...produto, desconto: 10 }
// Object.freeze — torna objeto imutável
const CONFIG = Object.freeze({ API_URL: 'https://api.exemplo.com' });
// CONFIG.API_URL = 'outra'; — silenciosamente ignorado (ou erro em strict mode)
// Object.fromEntries — o inverso de entries
const mapa = [['a', 1], ['b', 2], ['c', 3]];
Object.fromEntries(mapa) // → { a: 1, b: 2, c: 3 }
// Transformando objeto via entries + map
const precosComDesconto = Object.fromEntries(
Object.entries(produto)
.filter(([k]) => k === 'preco')
.map(([k, v]) => [k, v * 0.9])
);
13.5 — Condicionais e loops¶
Vídeo curto explicativo (link será adicionado posteriormente)
13.5.1 — if, else if, else e operador ternário¶
const nota = 7.5;
// if/else if/else
if (nota >= 9) {
console.log('Excelente');
} else if (nota >= 7) {
console.log('Aprovado');
} else if (nota >= 5) {
console.log('Recuperação');
} else {
console.log('Reprovado');
}
// Operador ternário — para expressões simples
const resultado = nota >= 7 ? 'Aprovado' : 'Reprovado';
// Ternário aninhado (use com moderação — prejudica legibilidade)
const conceito = nota >= 9 ? 'A' : nota >= 7 ? 'B' : nota >= 5 ? 'C' : 'D';
// Condicional com truthy/falsy
const nome = '';
const exibicao = nome || 'Anônimo'; // → 'Anônimo' (nome é falsy)
// Guardas (early return) — evitam aninhamento excessivo
function processar(valor) {
if (!valor) return null; // guarda: retorna cedo se inválido
if (valor < 0) return 0; // guarda: retorna cedo se negativo
return valor * 2; // lógica principal sem aninhamento
}
13.5.2 — switch¶
const diaSemana = 3;
switch (diaSemana) {
case 1:
console.log('Segunda-feira');
break;
case 2:
console.log('Terça-feira');
break;
case 3:
case 4:
console.log('Quarta ou Quinta-feira'); // fallthrough intencional
break;
case 5:
console.log('Sexta-feira');
break;
default:
console.log('Fim de semana');
}
// switch com strings
const comando = 'iniciar';
switch (comando) {
case 'iniciar':
iniciar();
break;
case 'parar':
parar();
break;
default:
console.warn(`Comando desconhecido: ${comando}`);
}
13.5.3 — for, while e do...while¶
// for — quando o número de iterações é conhecido
for (let i = 0; i < 5; i++) {
console.log(i); // → 0, 1, 2, 3, 4
}
// Iterando sobre array com índice
const frutas = ['maçã', 'banana', 'laranja'];
for (let i = 0; i < frutas.length; i++) {
console.log(`${i}: ${frutas[i]}`);
}
// while — quando a condição de parada não é conhecida previamente
let tentativas = 0;
let acertou = false;
while (!acertou && tentativas < 3) {
tentativas++;
const resposta = obterResposta();
if (resposta === 'correta') acertou = true;
}
// do...while — executa pelo menos uma vez antes de verificar a condição
let entrada;
do {
entrada = solicitarEntrada();
} while (!entradaValida(entrada));
13.5.4 — for...of e for...in¶
// for...of — itera sobre valores de iteráveis (arrays, strings, Maps, Sets)
const numeros = [10, 20, 30];
for (const numero of numeros) {
console.log(numero); // → 10, 20, 30
}
// Com string
for (const letra of 'IFAL') {
console.log(letra); // → I, F, A, L
}
// Com desestruturação e entries
for (const [indice, valor] of numeros.entries()) {
console.log(`${indice}: ${valor}`);
}
// for...in — itera sobre chaves enumeráveis de objetos
// (use com cautela em arrays)
const configuracao = { tema: 'escuro', idioma: 'pt', fonte: 16 };
for (const chave in configuracao) {
console.log(`${chave}: ${configuracao[chave]}`);
}
// → tema: escuro | idioma: pt | fonte: 16
// for...of vs for...in em arrays
const arr = ['a', 'b', 'c'];
for (const v of arr) console.log(v); // → a, b, c (valores)
for (const k in arr) console.log(k); // → 0, 1, 2 (índices como strings)
// Prefira for...of para arrays
13.5.5 — break e continue¶
// break — interrompe o loop imediatamente
const numeros = [1, 5, 3, 8, 2, 9, 4];
let primeiraMaiorQue6;
for (const n of numeros) {
if (n > 6) {
primeiraMaiorQue6 = n;
break; // para o loop ao encontrar o primeiro
}
}
// → primeiraMaiorQue6 = 8
// continue — pula para a próxima iteração
const resultado = [];
for (let i = 0; i <= 10; i++) {
if (i % 2 !== 0) continue; // pula ímpares
resultado.push(i);
}
// → [0, 2, 4, 6, 8, 10]
// Labels — para break/continue em loops aninhados
externo: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) continue externo; // continua o loop externo
console.log(i, j);
}
}
13.6 — Assincronia: introdução¶
Vídeo curto explicativo (link será adicionado posteriormente)
13.6.1 — Por que JavaScript é assíncrono¶
Como apresentado na seção 13.1.3, o JavaScript é single-threaded — executa em uma única thread. Operações que demoram para completar — requisições de rede, leitura de arquivos, timers — não podem simplesmente bloquear essa thread aguardando o resultado, pois isso congelaria a interface do usuário.
A solução é o modelo assíncrono: em vez de esperar, o JavaScript inicia a operação, registra o que deve ser feito quando ela completar (um callback), e continua executando o restante do código. Quando a operação completa, o callback é chamado.
// Exemplo de assincronia: setTimeout
console.log('1 — antes');
setTimeout(() => {
console.log('3 — dentro do timeout (após 1 segundo)');
}, 1000);
console.log('2 — depois');
// Saída:
// 1 — antes
// 2 — depois
// 3 — dentro do timeout (após 1 segundo)
O código após o setTimeout executa imediatamente, sem esperar o timer. Quando o segundo passa, o callback é colocado na fila e executado após a call stack esvaziar.
13.6.2 — Callbacks: o padrão original¶
O callback é o padrão mais básico de assincronia: uma função passada como argumento para ser chamada quando uma operação assíncrona completa:
// Callback simples
function buscarDados(id, callback) {
setTimeout(() => { // simula requisição de rede
const dados = { id, nome: 'Produto ' + id, preco: 99.90 };
callback(null, dados); // convenção: (erro, resultado)
}, 500);
}
buscarDados(1, (erro, produto) => {
if (erro) {
console.error('Erro:', erro);
return;
}
console.log(produto);
});
// Callback hell — problema clássico com callbacks aninhados
buscarUsuario(id, (erro, usuario) => {
if (erro) return tratarErro(erro);
buscarPedidos(usuario.id, (erro, pedidos) => {
if (erro) return tratarErro(erro);
buscarProdutos(pedidos[0].id, (erro, produto) => {
if (erro) return tratarErro(erro);
// aninhamento torna o código ilegível e difícil de manter
});
});
});
O "callback hell" — aninhamento profundo de callbacks — foi o principal motivador para a criação das Promises.
13.6.3 — Promises: conceito, estados e encadeamento¶
Uma Promise representa o resultado eventual de uma operação assíncrona. Em vez de passar um callback, a função assíncrona retorna uma Promise que pode estar em um de três estados:
- Pending: operação em andamento
- Fulfilled: operação completou com sucesso (tem um valor)
- Rejected: operação falhou (tem um motivo de erro)
// Criando uma Promise
function buscarDados(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, nome: 'Produto ' + id }); // sucesso
} else {
reject(new Error('ID inválido')); // falha
}
}, 500);
});
}
// Consumindo com .then() e .catch()
buscarDados(1)
.then(produto => {
console.log('Sucesso:', produto);
return produto.nome; // retorno é passado para o próximo .then()
})
.then(nome => {
console.log('Nome:', nome);
})
.catch(erro => {
console.error('Erro:', erro.message);
})
.finally(() => {
console.log('Operação concluída'); // sempre executado
});
// Promise.all — aguarda múltiplas promises em paralelo
Promise.all([
buscarDados(1),
buscarDados(2),
buscarDados(3),
]).then(([p1, p2, p3]) => {
console.log(p1, p2, p3);
}).catch(erro => {
// falha se QUALQUER uma rejeitar
console.error(erro);
});
// Promise.allSettled — aguarda todas, independente de sucesso ou falha
Promise.allSettled([buscarDados(1), buscarDados(-1)])
.then(resultados => {
resultados.forEach(r => {
if (r.status === 'fulfilled') console.log('OK:', r.value);
else console.log('Erro:', r.reason.message);
});
});
13.6.4 — async/await: sintaxe síncrona para código assíncrono¶
async/await é uma sintaxe introduzida no ES2017 que permite escrever código assíncrono com aparência síncrona — tornando-o mais legível e próximo do fluxo de pensamento linear:
// async: declara que a função retorna uma Promise
// await: pausa a execução da função até a Promise resolver
async function carregarPerfil(userId) {
// await pausa aqui até buscarUsuario resolver
const usuario = await buscarUsuario(userId);
const pedidos = await buscarPedidos(usuario.id);
const ultimoPedido = await buscarPedido(pedidos[0].id);
return {
usuario,
totalPedidos: pedidos.length,
ultimoPedido
};
}
// Equivale ao encadeamento de .then() mas muito mais legível
// Compare com o callback hell da seção 13.6.2
// Consumindo uma função async
carregarPerfil(1)
.then(perfil => console.log(perfil))
.catch(erro => console.error(erro));
// await também funciona no nível do módulo (top-level await)
13.6.5 — Tratamento de erros com try/catch¶
async function carregarDados(id) {
try {
const usuario = await buscarUsuario(id);
const pedidos = await buscarPedidos(usuario.id);
return { usuario, pedidos };
} catch (erro) {
// captura qualquer erro nas operações await acima
console.error('Falha ao carregar dados:', erro.message);
return null; // ou relançar: throw erro;
} finally {
// executado sempre, independente de sucesso ou erro
esconderIndicadorDeCarregamento();
}
}
// Tratamento de erros específicos
async function salvar(dados) {
try {
const resposta = await fetch('/api/salvar', {
method: 'POST',
body: JSON.stringify(dados)
});
if (!resposta.ok) {
// fetch não rejeita em erros HTTP — é necessário verificar
throw new Error(`HTTP ${resposta.status}: ${resposta.statusText}`);
}
return await resposta.json();
} catch (erro) {
if (erro instanceof TypeError) {
// TypeError do fetch: sem conexão, URL inválida
console.error('Erro de rede:', erro.message);
} else {
console.error('Erro ao salvar:', erro.message);
}
throw erro; // relança para o chamador decidir
}
}
Referências: - MDN — JavaScript - MDN — JavaScript Guide - ECMAScript — Especificação oficial - javascript.info — O Tutorial Moderno de JavaScript - You Don't Know JS (livro gratuito)
Atividades — Capítulo 13¶
1. Por que é recomendado usar === em vez de == para comparações em JavaScript?
2. Qual é a diferença entre map() e filter()?
3. O que é uma closure em JavaScript?
4. Qual é a vantagem de async/await em relação ao encadeamento de .then()?
- GitHub Classroom: Implementar um módulo JavaScript que: (1) processe um array de alunos usando
filter,mapereducepara calcular médias e classificar aprovados; (2) implemente uma função com closure para criar um placar de pontuação; (3) useasync/awaitcomsetTimeoutpara simular uma operação assíncrona de busca de dados. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 12 — Framework CSS: Tailwind CSS :material-arrow-right: Ir ao Capítulo 14 — Manipulação do DOM
Capítulo 14 — Manipulação do DOM¶
Vídeo curto explicativo (link será adicionado posteriormente)
14.1 — O que é o DOM¶
Vídeo curto explicativo (link será adicionado posteriormente)
O DOM (Document Object Model) é a representação do documento HTML em memória, estruturada como uma árvore de objetos que o JavaScript pode inspecionar e modificar em tempo de execução. Quando o navegador carrega um documento HTML, ele não apenas renderiza o visual — ele constrói uma estrutura de dados hierárquica em memória que modela cada elemento, atributo e fragmento de texto do documento como um nó (node).
Esta estrutura é o que torna possível o desenvolvimento de interfaces dinâmicas: adicionar e remover elementos, alterar textos e estilos, responder a cliques e digitações — tudo via JavaScript operando sobre o DOM.
14.1.1 — A árvore DOM¶
Para o seguinte documento HTML:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<title>Exemplo</title>
</head>
<body>
<h1 id="titulo">Olá, DOM</h1>
<p class="descricao">Primeiro parágrafo.</p>
<p class="descricao">Segundo parágrafo.</p>
</body>
</html>
O DOM construído pelo navegador é:
document
└── html
├── head
│ └── title
│ └── "Exemplo" (nó de texto)
└── body
├── h1#titulo
│ └── "Olá, DOM"
├── p.descricao
│ └── "Primeiro parágrafo."
└── p.descricao
└── "Segundo parágrafo."
Cada caixa é um nó DOM. Os tipos mais relevantes são:
| Tipo | Descrição | Exemplo |
|---|---|---|
Document |
O documento inteiro | document |
Element |
Um elemento HTML | <h1>, <p>, <div> |
Text |
Conteúdo textual de um elemento | "Olá, DOM" |
Attr |
Um atributo de elemento | id="titulo" |
Comment |
Comentário HTML | <!-- comentário --> |
14.1.2 — O objeto document¶
O ponto de entrada para toda interação com o DOM é o objeto global document — disponível automaticamente em qualquer script que rode no navegador:
document.title // → "Exemplo" — título da página
document.URL // → URL atual
document.documentElement // → elemento <html>
document.head // → elemento <head>
document.body // → elemento <body>
document.characterSet // → "UTF-8"
document.readyState // → "complete" | "interactive" | "loading"
14.1.3 — DOM vs HTML: a distinção importante¶
O DOM não é uma cópia estática do HTML — é uma representação viva e dinâmica que pode divergir do HTML original. Quando JavaScript modifica o DOM (adiciona um elemento, altera um texto), o HTML original no servidor não muda — apenas a representação em memória no navegador. O HTML que você vê em "Visualizar Código-fonte" é sempre o original; o que o DevTools mostra na aba Elements é o DOM atual (possivelmente modificado por JavaScript).
No DevTools: abra a aba Elements enquanto inspeciona uma página. Você está vendo o DOM em tempo real — não o HTML original. Ao executar JavaScript que modifica o DOM, as mudanças aparecem imediatamente na aba Elements.
14.2 — Seleção de elementos¶
Vídeo curto explicativo (link será adicionado posteriormente)
Para manipular um elemento, é necessário primeiro selecioná-lo — obter uma referência ao nó DOM correspondente. O JavaScript oferece múltiplos métodos de seleção, cada um com sua semântica e caso de uso.
14.2.1 — Métodos modernos: querySelector e querySelectorAll¶
Os métodos mais versáteis e recomendados para seleção de elementos utilizam seletores CSS como argumento — tornando familiar a quem já domina CSS:
// querySelector — retorna O PRIMEIRO elemento que corresponde ao seletor
// (ou null se nenhum for encontrado)
const titulo = document.querySelector('h1');
const primeiroBotao = document.querySelector('button');
const entrada = document.querySelector('#email');
const destaque = document.querySelector('.destaque');
const inputEmail = document.querySelector('input[type="email"]');
const primeiroLi = document.querySelector('nav ul > li:first-child');
// querySelectorAll — retorna TODOS os elementos correspondentes
// Retorna NodeList (similar a array, mas não é array)
const paragrafos = document.querySelectorAll('p');
const botoes = document.querySelectorAll('.btn');
const campos = document.querySelectorAll('input, select, textarea');
// Iterando sobre NodeList
paragrafos.forEach(p => {
console.log(p.textContent);
});
// Convertendo NodeList para Array (para usar métodos como filter, map)
const arrayBotoes = Array.from(botoes);
// ou: const arrayBotoes = [...botoes];
// Seleção dentro de um elemento específico (não apenas no document)
const formulario = document.querySelector('#form-contato');
const camposDoForm = formulario.querySelectorAll('input, textarea');
14.2.2 — Métodos clássicos (ainda amplamente usados)¶
// getElementById — seleciona por ID (mais rápido para IDs únicos)
const header = document.getElementById('cabecalho');
// Não usa # — o ID é passado direto como string
// getElementsByClassName — retorna HTMLCollection (dinâmica)
const cards = document.getElementsByClassName('card');
// getElementsByTagName — retorna HTMLCollection por tag
const links = document.getElementsByTagName('a');
const inputs = document.getElementsByTagName('input');
// Diferença importante: HTMLCollection vs NodeList
// HTMLCollection: atualizada automaticamente quando o DOM muda
// NodeList (querySelectorAll): estática — representa o DOM no momento da chamada
14.2.3 — Navegação pela árvore DOM¶
Além de selecionar diretamente, é possível navegar pela árvore a partir de um nó já referenciado:
const lista = document.querySelector('ul');
// Filhos
lista.children // HTMLCollection de filhos Element
lista.childNodes // NodeList de todos os filhos (inclui Text nodes)
lista.firstElementChild // primeiro filho Element
lista.lastElementChild // último filho Element
// Pai
lista.parentElement // elemento pai
lista.parentNode // nó pai (pode ser Document)
lista.closest('.container') // ancestral mais próximo que corresponde ao seletor
// Irmãos
lista.nextElementSibling // próximo irmão Element
lista.previousElementSibling // irmão anterior Element
// Exemplo: percorrendo filhos
for (const item of lista.children) {
console.log(item.textContent);
}
14.3 — Manipulação de conteúdo e atributos¶
Vídeo curto explicativo (link será adicionado posteriormente)
14.3.1 — Lendo e modificando conteúdo¶
const titulo = document.querySelector('h1');
const paragrafo = document.querySelector('p');
// textContent — lê/modifica apenas o texto (sem HTML)
console.log(titulo.textContent); // → "Olá, DOM"
titulo.textContent = 'Novo título'; // altera o texto
// innerHTML — lê/modifica o HTML interno
paragrafo.innerHTML = '<strong>Texto em negrito</strong>';
// ⚠️ NUNCA use innerHTML com dados de usuário — risco de XSS
// innerText — similar ao textContent, mas considera CSS (mais lento)
// Não recomendado para escrita; use textContent
// Conteúdo de inputs
const input = document.querySelector('input');
input.value // lê o valor atual
input.value = 'novo'; // define o valor
// Conteúdo de selects
const select = document.querySelector('select');
select.value // opção selecionada
// Criando elementos com template literals (seguro)
function criarCard(produto) {
const card = document.createElement('article');
card.className = 'card';
// textContent é seguro para texto; innerHTML apenas para HTML controlado
card.innerHTML = `
<h2 class="card__titulo">${escapeHtml(produto.nome)}</h2>
<p class="card__preco">R$ ${produto.preco.toFixed(2)}</p>
`;
return card;
}
// Função auxiliar para escapar HTML (evita XSS)
function escapeHtml(texto) {
const div = document.createElement('div');
div.textContent = texto;
return div.innerHTML;
}
14.3.2 — Manipulando atributos¶
const link = document.querySelector('a');
const imagem = document.querySelector('img');
const input = document.querySelector('input[type="text"]');
// getAttribute / setAttribute
link.getAttribute('href') // → "/pagina"
link.setAttribute('href', '/nova') // modifica
link.setAttribute('target', '_blank') // adiciona
// Atributos como propriedades (para atributos padrão HTML)
link.href // → URL completa (diferente de getAttribute)
imagem.src // → URL completa da imagem
imagem.alt // → texto alternativo
input.placeholder // → placeholder
input.disabled // → boolean
input.required // → boolean
// removeAttribute
link.removeAttribute('target');
// hasAttribute
link.hasAttribute('download') // → false
// dataset — atributos data-*
// HTML: <div data-id="42" data-tipo="produto">
const div = document.querySelector('div');
div.dataset.id // → "42"
div.dataset.tipo // → "produto"
div.dataset.novoAtributo = 'valor'; // cria data-novo-atributo="valor"
14.3.3 — Manipulando classes CSS¶
const elemento = document.querySelector('.card');
// classList API — a forma recomendada
elemento.classList.add('ativo'); // adiciona classe
elemento.classList.remove('oculto'); // remove classe
elemento.classList.toggle('expandido'); // adiciona se não tem, remove se tem
elemento.classList.toggle('oculto', false); // force remove
elemento.classList.toggle('visivel', true); // force add
elemento.classList.contains('ativo'); // → boolean
elemento.classList.replace('antigo', 'novo'); // substitui
// Múltiplas classes de uma vez
elemento.classList.add('ativo', 'destacado', 'animado');
elemento.classList.remove('oculto', 'desabilitado');
// className — lê/define todas as classes como string (evitar para manipulação)
console.log(elemento.className); // → "card ativo"
14.3.4 — Manipulando estilos inline¶
const caixa = document.querySelector('.caixa');
// style — define estilos inline (camelCase para propriedades com hífen)
caixa.style.backgroundColor = '#E8632A';
caixa.style.fontSize = '1.5rem';
caixa.style.marginTop = '2rem';
caixa.style.display = 'none'; // oculta
// Removendo estilo inline
caixa.style.backgroundColor = ''; // string vazia remove a propriedade
// getComputedStyle — lê os estilos CALCULADOS (após cascata e herança)
const estilosCalculados = window.getComputedStyle(caixa);
estilosCalculados.backgroundColor // → "rgb(232, 99, 42)"
estilosCalculados.fontSize // → "24px"
// getComputedStyle é somente leitura
// Variáveis CSS via JavaScript
document.documentElement.style.setProperty('--cor-primaria', '#E8632A');
const corAtual = getComputedStyle(document.documentElement)
.getPropertyValue('--cor-primaria').trim();
14.3.5 — Criando, inserindo e removendo elementos¶
// Criando elementos
const novoTitulo = document.createElement('h2');
novoTitulo.textContent = 'Novo título';
novoTitulo.className = 'titulo-secao';
const novoLink = document.createElement('a');
novoLink.href = '/pagina';
novoLink.textContent = 'Ir para a página';
// Inserindo no DOM
const container = document.querySelector('.container');
container.appendChild(novoTitulo); // insere como último filho
container.prepend(novoLink); // insere como primeiro filho
container.append('texto no final'); // append aceita string e nodes
container.insertBefore(novoTitulo, container.firstElementChild);
// insertAdjacentElement — mais controle sobre a posição
const referencia = document.querySelector('#secao-principal');
referencia.insertAdjacentElement('beforebegin', novoTitulo); // antes do elemento
referencia.insertAdjacentElement('afterbegin', novoTitulo); // primeiro filho
referencia.insertAdjacentElement('beforeend', novoTitulo); // último filho
referencia.insertAdjacentElement('afterend', novoTitulo); // após o elemento
// insertAdjacentHTML — insere HTML como string
referencia.insertAdjacentHTML('beforeend', '<p>Novo parágrafo</p>');
// Removendo elementos
const elementoARemover = document.querySelector('.obsoleto');
elementoARemover.remove(); // remove a si mesmo
// Clonando
const copia = novoTitulo.cloneNode(true); // true = clona filhos também
container.appendChild(copia);
// Substituindo
const velho = document.querySelector('.velho');
const novo = document.createElement('span');
velho.replaceWith(novo);
// Fragment — para inserções eficientes de múltiplos elementos
const fragment = document.createDocumentFragment();
const produtos = ['Produto A', 'Produto B', 'Produto C'];
produtos.forEach(nome => {
const li = document.createElement('li');
li.textContent = nome;
fragment.appendChild(li); // manipulações no fragment não afetam o DOM
});
document.querySelector('ul').appendChild(fragment); // uma única operação no DOM
14.4 — Eventos e interatividade¶
Vídeo curto explicativo (link será adicionado posteriormente)
Os eventos são o mecanismo pelo qual JavaScript responde às ações do usuário e do navegador. Um evento é uma notificação de que algo aconteceu — um clique, uma digitação, o carregamento da página, o movimento do mouse. O JavaScript registra listeners (ouvintes) que são chamados quando determinados eventos ocorrem em determinados elementos.
14.4.1 — addEventListener¶
O método addEventListener é a forma recomendada de registrar event listeners:
const botao = document.querySelector('#meu-botao');
// Sintaxe: elemento.addEventListener(evento, callback, opcoes)
botao.addEventListener('click', function(evento) {
console.log('Clicado!', evento);
});
// Com arrow function
botao.addEventListener('click', (e) => {
console.log('Clicado!', e.target);
});
// Função nomeada — permite remover o listener depois
function aoClicar(e) {
console.log('Clicado!');
}
botao.addEventListener('click', aoClicar);
botao.removeEventListener('click', aoClicar); // remove o listener
// Opções
botao.addEventListener('click', aoClicar, {
once: true, // executa apenas uma vez e se remove
passive: true, // indica que não chamará preventDefault (melhora performance em scroll)
capture: true // captura durante a fase de captura (ver 14.4.4)
});
14.4.2 — O objeto Event¶
O callback recebe um objeto Event com informações sobre o evento:
document.querySelector('form').addEventListener('submit', (evento) => {
// Previne o comportamento padrão (envio do formulário)
evento.preventDefault();
// Propriedades comuns a todos os eventos
evento.type // → "submit"
evento.target // elemento que disparou o evento
evento.currentTarget // elemento onde o listener está registrado
evento.timeStamp // momento em que o evento ocorreu
// stopPropagation — impede o evento de subir na árvore DOM
evento.stopPropagation();
});
// Eventos de mouse
document.addEventListener('mousemove', (e) => {
e.clientX // posição X relativa à viewport
e.clientY // posição Y relativa à viewport
e.pageX // posição X relativa ao documento inteiro
e.pageY // posição Y
e.button // qual botão: 0=esquerdo, 1=meio, 2=direito
});
// Eventos de teclado
document.addEventListener('keydown', (e) => {
e.key // → "Enter", "Escape", "a", "ArrowLeft"...
e.code // → "KeyA", "Enter", "Space"... (independente do layout)
e.ctrlKey // → boolean
e.shiftKey // → boolean
e.altKey // → boolean
e.metaKey // → boolean (Cmd no Mac)
});
14.4.3 — Eventos mais comuns¶
// ── Eventos de mouse ──
elemento.addEventListener('click', handler); // clique único
elemento.addEventListener('dblclick', handler); // duplo clique
elemento.addEventListener('mouseenter', handler); // mouse entra (não borbulha)
elemento.addEventListener('mouseleave', handler); // mouse sai (não borbulha)
elemento.addEventListener('mouseover', handler); // mouse sobre (borbulha)
elemento.addEventListener('mouseout', handler); // mouse fora (borbulha)
elemento.addEventListener('mousedown', handler); // botão pressionado
elemento.addEventListener('mouseup', handler); // botão liberado
elemento.addEventListener('contextmenu', handler); // clique direito
// ── Eventos de teclado ──
elemento.addEventListener('keydown', handler); // tecla pressionada
elemento.addEventListener('keyup', handler); // tecla liberada
elemento.addEventListener('keypress', handler); // (depreciado — evitar)
// ── Eventos de formulário ──
input.addEventListener('input', handler); // valor mudou (tempo real)
input.addEventListener('change', handler); // valor confirmado (ao sair do campo)
input.addEventListener('focus', handler); // campo recebeu foco
input.addEventListener('blur', handler); // campo perdeu foco
form.addEventListener('submit', handler); // formulário submetido
form.addEventListener('reset', handler); // formulário reiniciado
// ── Eventos de documento/janela ──
document.addEventListener('DOMContentLoaded', handler); // DOM pronto
window.addEventListener('load', handler); // página inteira carregada
window.addEventListener('resize', handler); // janela redimensionada
window.addEventListener('scroll', handler); // página rolada
// ── Eventos de toque (mobile) ──
elemento.addEventListener('touchstart', handler);
elemento.addEventListener('touchend', handler);
elemento.addEventListener('touchmove', handler);
14.4.4 — Event bubbling e delegação de eventos¶
Event bubbling (borbulhamento): quando um evento ocorre em um elemento, ele sobe pela árvore DOM — disparando nos ancestrais até alcançar o document. Isso permite uma técnica poderosa chamada delegação de eventos:
// Problema: adicionar listener a cada item de uma lista dinâmica
// (ineficiente — e não funciona para itens adicionados depois)
document.querySelectorAll('.btn-remover').forEach(btn => {
btn.addEventListener('click', removerItem);
});
// Solução: delegação de eventos — listener único no pai
const lista = document.querySelector('#lista-tarefas');
lista.addEventListener('click', (evento) => {
// verifica se o clique foi num botão de remover
if (evento.target.matches('.btn-remover')) {
const item = evento.target.closest('li');
item.remove();
}
// ou usando dataset para identificar a ação
if (evento.target.dataset.acao === 'editar') {
const id = evento.target.dataset.id;
editarItem(id);
}
});
// Vantagens da delegação:
// 1. Apenas um listener para N itens
// 2. Funciona automaticamente para elementos adicionados dinamicamente
// 3. Menos uso de memória
14.4.5 — Exemplo integrado: to-do list¶
// HTML esperado:
// <form id="form-tarefa">
// <input type="text" id="input-tarefa" placeholder="Nova tarefa..." required />
// <button type="submit">Adicionar</button>
// </form>
// <ul id="lista-tarefas"></ul>
const form = document.querySelector('#form-tarefa');
const input = document.querySelector('#input-tarefa');
const lista = document.querySelector('#lista-tarefas');
let tarefas = [];
// Adicionar tarefa
form.addEventListener('submit', (e) => {
e.preventDefault();
const texto = input.value.trim();
if (!texto) return;
const tarefa = {
id: Date.now(),
texto,
concluida: false
};
tarefas.push(tarefa);
input.value = '';
renderizar();
input.focus();
});
// Delegação para ações na lista
lista.addEventListener('click', (e) => {
const id = Number(e.target.closest('[data-id]')?.dataset.id);
if (!id) return;
if (e.target.matches('.btn-concluir')) {
tarefas = tarefas.map(t =>
t.id === id ? { ...t, concluida: !t.concluida } : t
);
}
if (e.target.matches('.btn-remover')) {
tarefas = tarefas.filter(t => t.id !== id);
}
renderizar();
});
function renderizar() {
lista.innerHTML = '';
const fragment = document.createDocumentFragment();
tarefas.forEach(tarefa => {
const li = document.createElement('li');
li.dataset.id = tarefa.id;
li.className = `tarefa ${tarefa.concluida ? 'tarefa--concluida' : ''}`;
li.innerHTML = `
<span class="tarefa__texto">${escapeHtml(tarefa.texto)}</span>
<div class="tarefa__acoes">
<button class="btn-concluir" type="button"
aria-label="${tarefa.concluida ? 'Reabrir' : 'Concluir'}">
${tarefa.concluida ? '↩' : '✓'}
</button>
<button class="btn-remover" type="button" aria-label="Remover">
✕
</button>
</div>
`;
fragment.appendChild(li);
});
lista.appendChild(fragment);
}
function escapeHtml(texto) {
const div = document.createElement('div');
div.textContent = texto;
return div.innerHTML;
}
14.5 — Jogos no navegador: HTML5, CSS e JavaScript¶
Vídeo curto explicativo (link será adicionado posteriormente)
Com o DOM, eventos e JavaScript essencial dominados, é possível construir experiências interativas completas diretamente no navegador — incluindo jogos. Esta seção apresenta três jogos em complexidade crescente, cada um introduzindo conceitos progressivos: lógica pura, manipulação de DOM com estado, e renderização com Canvas.
14.5.1 — O navegador como plataforma de jogos¶
O navegador moderno é uma plataforma de jogos completa. As tecnologias disponíveis incluem:
- DOM + CSS: suficiente para jogos baseados em elementos HTML (puzzles, jogos de cartas, quizzes, jogos de texto)
- Canvas 2D: API de desenho raster para jogos com gráficos personalizados (plataformers, shooters, Snake, Tetris)
- WebGL: renderização 3D com aceleração GPU
- Web Audio API: síntese e reprodução de áudio
- Gamepad API: suporte a controles
Para os exemplos deste capítulo, usaremos DOM e Canvas 2D — o suficiente para construir jogos funcionais e divertidos.
14.5.2 — O game loop: requestAnimationFrame¶
O game loop é o coração de qualquer jogo: um ciclo que, repetidamente, atualiza o estado do jogo e renderiza o resultado na tela. No navegador, o mecanismo correto para implementá-lo é requestAnimationFrame:
// requestAnimationFrame chama o callback antes do próximo repaint do navegador
// Isso sincroniza o loop com a taxa de atualização da tela (geralmente 60fps)
function gameLoop(timestamp) {
// timestamp: tempo em ms desde o início da página
atualizar(timestamp); // atualiza posições, colisões, pontuação
renderizar(); // redesenha a tela
// agenda a próxima iteração
requestAnimationFrame(gameLoop);
}
// Inicia o loop
requestAnimationFrame(gameLoop);
// Para pausar: guarde o ID retornado e cancele
let loopId;
loopId = requestAnimationFrame(gameLoop);
cancelAnimationFrame(loopId); // pausa o loop
// Calculando delta time — tempo entre frames (para movimento consistente)
let ultimoTimestamp = 0;
function gameLoop(timestamp) {
const deltaTime = timestamp - ultimoTimestamp; // ms desde o último frame
ultimoTimestamp = timestamp;
// Mover 200 pixels por segundo, independente do fps
posicaoX += velocidade * (deltaTime / 1000);
requestAnimationFrame(gameLoop);
}
Por que requestAnimationFrame e não setInterval?
- Sincronizado com o ciclo de refresh da tela (60fps, 120fps, etc.)
- Pausa automaticamente quando a aba fica inativa (economiza bateria e CPU)
- Mais preciso que
setIntervalpara animações
14.5.3 — Capturando entrada do usuário: teclado e mouse¶
// Estado das teclas — melhor abordagem para jogos (permite múltiplas teclas)
const teclasPressionadas = new Set();
document.addEventListener('keydown', (e) => {
teclasPressionadas.add(e.code);
e.preventDefault(); // evita scroll da página com setas
});
document.addEventListener('keyup', (e) => {
teclasPressionadas.delete(e.code);
});
// No game loop: verificar estado das teclas
function atualizar() {
if (teclasPressionadas.has('ArrowLeft')) jogador.x -= velocidade;
if (teclasPressionadas.has('ArrowRight')) jogador.x += velocidade;
if (teclasPressionadas.has('ArrowUp')) jogador.y -= velocidade;
if (teclasPressionadas.has('ArrowDown')) jogador.y += velocidade;
if (teclasPressionadas.has('Space')) atirar();
}
// Mouse / touch no Canvas
const canvas = document.querySelector('canvas');
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left; // posição relativa ao canvas
const y = e.clientY - rect.top;
aoClicarEm(x, y);
});
14.5.4 — Jogo 1: Adivinhe o número¶
O primeiro jogo usa apenas lógica JavaScript e manipulação básica de DOM — sem canvas. O objetivo: adivinhar um número aleatório entre 1 e 100 com dicas de "maior" ou "menor".
Imagem sugerida: captura do jogo em execução mostrando o campo de entrada, o histórico de tentativas e a mensagem de vitória com o número de tentativas.
(imagem será adicionada posteriormente)
<!-- HTML do jogo -->
<div class="jogo" id="jogo-adivinha">
<h1>🎯 Adivinhe o número</h1>
<p class="instrucao">Pensei em um número entre <strong>1</strong> e <strong>100</strong>.</p>
<div class="entrada-grupo">
<label for="palpite" class="sr-only">Seu palpite</label>
<input type="number" id="palpite" min="1" max="100"
placeholder="Digite seu palpite..." />
<button type="button" id="btn-tentar">Tentar</button>
</div>
<p class="feedback" id="feedback" aria-live="polite"></p>
<p class="tentativas" id="tentativas"></p>
<ul class="historico" id="historico" aria-label="Histórico de tentativas"></ul>
<button type="button" id="btn-reiniciar" class="btn-reiniciar hidden">
Jogar novamente
</button>
</div>
// Lógica do jogo
const MINIMO = 1;
const MAXIMO = 100;
let numeroSecreto;
let totalTentativas;
let jogoAtivo;
function inicializar() {
numeroSecreto = Math.floor(Math.random() * (MAXIMO - MINIMO + 1)) + MINIMO;
totalTentativas = 0;
jogoAtivo = true;
// Resetar interface
document.querySelector('#feedback').textContent = '';
document.querySelector('#tentativas').textContent = '';
document.querySelector('#historico').innerHTML = '';
document.querySelector('#palpite').value = '';
document.querySelector('#palpite').disabled = false;
document.querySelector('#btn-tentar').disabled = false;
document.querySelector('#btn-reiniciar').classList.add('hidden');
document.querySelector('#palpite').focus();
}
function processarPalpite() {
if (!jogoAtivo) return;
const input = document.querySelector('#palpite');
const palpite = parseInt(input.value);
// Validação
if (isNaN(palpite) || palpite < MINIMO || palpite > MAXIMO) {
exibirFeedback(`Digite um número entre ${MINIMO} e ${MAXIMO}.`, 'aviso');
return;
}
totalTentativas++;
adicionarAoHistorico(palpite);
input.value = '';
input.focus();
// Verificar resultado
if (palpite === numeroSecreto) {
exibirFeedback(
`🎉 Parabéns! Você acertou em ${totalTentativas} tentativa${totalTentativas > 1 ? 's' : ''}!`,
'sucesso'
);
encerrarJogo();
return;
}
const dica = palpite < numeroSecreto ? '📈 Maior!' : '📉 Menor!';
exibirFeedback(`${dica} Tentativa ${totalTentativas}.`, 'dica');
}
function adicionarAoHistorico(palpite) {
const historico = document.querySelector('#historico');
const li = document.createElement('li');
li.textContent = `Tentativa ${totalTentativas}: ${palpite}`;
const icone = palpite < numeroSecreto ? '↑' : '↓';
li.insertAdjacentHTML('beforeend', ` <span class="dica-icone">${icone}</span>`);
historico.prepend(li); // mais recente primeiro
}
function exibirFeedback(mensagem, tipo) {
const el = document.querySelector('#feedback');
el.textContent = mensagem;
el.className = `feedback feedback--${tipo}`;
}
function encerrarJogo() {
jogoAtivo = false;
document.querySelector('#palpite').disabled = true;
document.querySelector('#btn-tentar').disabled = true;
document.querySelector('#btn-reiniciar').classList.remove('hidden');
}
// Event listeners
document.querySelector('#btn-tentar').addEventListener('click', processarPalpite);
document.querySelector('#palpite').addEventListener('keydown', (e) => {
if (e.key === 'Enter') processarPalpite();
});
document.querySelector('#btn-reiniciar').addEventListener('click', inicializar);
// Iniciar
inicializar();
14.5.5 — Jogo 2: Clique no alvo¶
O segundo jogo introduz: geração dinâmica de elementos, posicionamento aleatório, timer com setInterval, e gerenciamento de estado mais complexo (pontuação, tempo restante, nível de dificuldade).
Imagem sugerida: captura do jogo em andamento mostrando o alvo colorido em posição aleatória, o placar de pontos e o timer regressivo.
(imagem será adicionada posteriormente)
<!-- HTML do jogo -->
<div class="jogo-alvo" id="jogo-alvo">
<header class="hud">
<div class="hud__item">
<span class="hud__label">Pontos</span>
<span class="hud__valor" id="placar">0</span>
</div>
<div class="hud__item">
<span class="hud__label">Tempo</span>
<span class="hud__valor" id="timer">30</span>
</div>
<div class="hud__item">
<span class="hud__label">Nível</span>
<span class="hud__valor" id="nivel">1</span>
</div>
</header>
<div class="arena" id="arena" aria-label="Arena do jogo">
<!-- alvos são inseridos aqui via JavaScript -->
</div>
<div class="tela-inicio" id="tela-inicio">
<h2>🎯 Clique no Alvo!</h2>
<p>Clique nos alvos o mais rápido que puder.<br>
Alvos maiores = menos pontos. Alvos menores = mais pontos!</p>
<button type="button" id="btn-iniciar-alvo">Iniciar</button>
</div>
<div class="tela-fim hidden" id="tela-fim">
<h2>Fim de jogo!</h2>
<p>Pontuação final: <strong id="pontuacao-final">0</strong></p>
<button type="button" id="btn-reiniciar-alvo">Jogar novamente</button>
</div>
</div>
// Estado do jogo
const estado = {
pontos: 0,
tempoRestante: 30,
nivel: 1,
ativo: false,
intervalTimer: null,
intervalAlvo: null
};
const arena = document.querySelector('#arena');
// Configurações por nível
const NIVEIS = {
1: { intervalo: 1200, tamanhoMin: 60, tamanhoMax: 90, pontos: 10 },
2: { intervalo: 900, tamanhoMin: 45, tamanhoMax: 70, pontos: 15 },
3: { intervalo: 650, tamanhoMin: 30, tamanhoMax: 55, pontos: 25 },
4: { intervalo: 450, tamanhoMin: 20, tamanhoMax: 40, pontos: 40 },
};
function iniciarJogo() {
// Reset estado
estado.pontos = 0;
estado.tempoRestante = 30;
estado.nivel = 1;
estado.ativo = true;
// Atualizar HUD
atualizarHUD();
// Esconder telas de início/fim
document.querySelector('#tela-inicio').classList.add('hidden');
document.querySelector('#tela-fim').classList.add('hidden');
// Timer regressivo
estado.intervalTimer = setInterval(() => {
estado.tempoRestante--;
document.querySelector('#timer').textContent = estado.tempoRestante;
// Aumentar nível a cada 10 segundos
if (estado.tempoRestante === 20) subirNivel(2);
if (estado.tempoRestante === 10) subirNivel(3);
if (estado.tempoRestante === 5) subirNivel(4);
if (estado.tempoRestante <= 0) encerrarJogo();
}, 1000);
// Spawnar alvos
spawnarAlvo();
estado.intervalAlvo = setInterval(
spawnarAlvo,
NIVEIS[estado.nivel].intervalo
);
}
function spawnarAlvo() {
if (!estado.ativo) return;
const cfg = NIVEIS[estado.nivel];
const tamanho = aleatorio(cfg.tamanhoMin, cfg.tamanhoMax);
const arenaRect = arena.getBoundingClientRect();
// Posição aleatória dentro da arena
const maxX = arenaRect.width - tamanho;
const maxY = arenaRect.height - tamanho;
const x = aleatorio(0, maxX);
const y = aleatorio(0, maxY);
const alvo = document.createElement('button');
alvo.type = 'button';
alvo.className = 'alvo';
alvo.style.cssText = `
width: ${tamanho}px;
height: ${tamanho}px;
left: ${x}px;
top: ${y}px;
background-color: hsl(${aleatorio(0, 360)}, 80%, 55%);
`;
alvo.setAttribute('aria-label', `Alvo: ${cfg.pontos} pontos`);
// Remover alvo após 1.5x o intervalo se não for clicado
const timeout = setTimeout(() => alvo.remove(), cfg.intervalo * 1.5);
alvo.addEventListener('click', () => {
clearTimeout(timeout);
marcarPonto(alvo, cfg.pontos, x + tamanho / 2, y);
alvo.remove();
});
arena.appendChild(alvo);
}
function marcarPonto(alvo, pontos, x, y) {
estado.pontos += pontos;
atualizarHUD();
// Feedback visual: mostrar pontos ganhos
const feedback = document.createElement('span');
feedback.className = 'ponto-feedback';
feedback.textContent = `+${pontos}`;
feedback.style.cssText = `left: ${x}px; top: ${y}px;`;
arena.appendChild(feedback);
setTimeout(() => feedback.remove(), 600);
}
function subirNivel(novoNivel) {
if (estado.nivel >= novoNivel) return;
estado.nivel = novoNivel;
document.querySelector('#nivel').textContent = novoNivel;
clearInterval(estado.intervalAlvo);
estado.intervalAlvo = setInterval(spawnarAlvo, NIVEIS[novoNivel].intervalo);
}
function atualizarHUD() {
document.querySelector('#placar').textContent = estado.pontos;
}
function encerrarJogo() {
estado.ativo = false;
clearInterval(estado.intervalTimer);
clearInterval(estado.intervalAlvo);
arena.innerHTML = ''; // remove alvos restantes
document.querySelector('#pontuacao-final').textContent = estado.pontos;
document.querySelector('#tela-fim').classList.remove('hidden');
}
function aleatorio(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Event listeners
document.querySelector('#btn-iniciar-alvo').addEventListener('click', iniciarJogo);
document.querySelector('#btn-reiniciar-alvo').addEventListener('click', iniciarJogo);
14.5.6 — Jogo 3: Snake com Canvas¶
O terceiro jogo introduz o elemento <canvas> e a API de desenho 2D — a base para jogos com gráficos personalizados. O Snake é um clássico que demonstra: game loop com requestAnimationFrame, detecção de colisão, gerenciamento de estado complexo e renderização frame a frame.
Imagem sugerida: captura do jogo Snake em andamento, mostrando a cobra de múltiplos segmentos, a maçã como alimento, o placar e o canvas com fundo escuro.
(imagem será adicionada posteriormente)
<!-- HTML do jogo -->
<div class="jogo-snake">
<div class="snake-hud">
<span>Pontos: <strong id="snake-placar">0</strong></span>
<span>Recorde: <strong id="snake-recorde">0</strong></span>
</div>
<canvas id="canvas-snake" width="400" height="400"
tabindex="0"
aria-label="Jogo Snake — use as teclas de seta para mover">
</canvas>
<p class="snake-instrucao" id="snake-instrucao">
Pressione qualquer seta para começar
</p>
</div>
// ── Configuração ──────────────────────────────────────────
const TAMANHO_CELULA = 20; // px por célula da grade
const COLUNAS = 20; // 400px / 20px = 20 células
const LINHAS = 20;
const VELOCIDADE_INICIAL = 150; // ms entre frames lógicos
// ── Referências ───────────────────────────────────────────
const canvas = document.querySelector('#canvas-snake');
const ctx = canvas.getContext('2d');
const placarEl = document.querySelector('#snake-placar');
const recordeEl= document.querySelector('#snake-recorde');
// ── Estado do jogo ────────────────────────────────────────
let cobra, direcao, proximaDirecao, comida, pontos, recorde, ativo, loopId;
let ultimoFrame = 0;
function inicializar() {
cobra = [
{ x: 10, y: 10 }, // cabeça
{ x: 9, y: 10 },
{ x: 8, y: 10 },
];
direcao = { x: 1, y: 0 }; // movendo para direita
proximaDirecao = { x: 1, y: 0 };
pontos = 0;
recorde = parseInt(localStorage.getItem('snake-recorde') || '0');
ativo = false;
gerarComida();
atualizarPlacar();
desenhar();
canvas.focus();
}
// ── Controles ─────────────────────────────────────────────
document.addEventListener('keydown', (e) => {
const mapa = {
'ArrowUp': { x: 0, y: -1 },
'ArrowDown': { x: 0, y: 1 },
'ArrowLeft': { x: -1, y: 0 },
'ArrowRight': { x: 1, y: 0 },
};
if (!mapa[e.code]) return;
e.preventDefault();
const nova = mapa[e.code];
// Impede reversão de direção (não pode ir para trás)
if (nova.x === -direcao.x && nova.y === -direcao.y) return;
proximaDirecao = nova;
// Iniciar jogo na primeira tecla pressionada
if (!ativo) {
ativo = true;
document.querySelector('#snake-instrucao').style.display = 'none';
requestAnimationFrame(gameLoop);
}
});
// ── Game Loop ──────────────────────────────────────────────
function gameLoop(timestamp) {
if (!ativo) return;
const velocidade = Math.max(80, VELOCIDADE_INICIAL - pontos * 2);
if (timestamp - ultimoFrame >= velocidade) {
ultimoFrame = timestamp;
atualizar();
}
desenhar();
loopId = requestAnimationFrame(gameLoop);
}
// ── Lógica ────────────────────────────────────────────────
function atualizar() {
direcao = { ...proximaDirecao };
// Nova posição da cabeça
const cabeca = {
x: cobra[0].x + direcao.x,
y: cobra[0].y + direcao.y
};
// Colisão com paredes
if (
cabeca.x < 0 || cabeca.x >= COLUNAS ||
cabeca.y < 0 || cabeca.y >= LINHAS
) {
gameOver();
return;
}
// Colisão com o próprio corpo
if (cobra.some(seg => seg.x === cabeca.x && seg.y === cabeca.y)) {
gameOver();
return;
}
cobra.unshift(cabeca); // adiciona nova cabeça
// Verificar se comeu
if (cabeca.x === comida.x && cabeca.y === comida.y) {
pontos += 10;
atualizarPlacar();
gerarComida();
// NÃO remove a cauda — cobra cresce
} else {
cobra.pop(); // remove a cauda
}
}
function gerarComida() {
// Posição aleatória que não seja ocupada pela cobra
do {
comida = {
x: Math.floor(Math.random() * COLUNAS),
y: Math.floor(Math.random() * LINHAS)
};
} while (cobra.some(seg => seg.x === comida.x && seg.y === comida.y));
}
function gameOver() {
ativo = false;
cancelAnimationFrame(loopId);
// Salvar recorde
if (pontos > recorde) {
recorde = pontos;
localStorage.setItem('snake-recorde', recorde);
}
// Desenhar tela de fim
desenhar();
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.font = 'bold 28px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Game Over!', canvas.width / 2, canvas.height / 2 - 20);
ctx.font = '18px sans-serif';
ctx.fillText(`Pontos: ${pontos}`, canvas.width / 2, canvas.height / 2 + 15);
ctx.fillText('Pressione qualquer seta para reiniciar',
canvas.width / 2, canvas.height / 2 + 45);
atualizarPlacar();
// Reiniciar ao pressionar tecla
const reiniciar = (e) => {
if (!['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) return;
e.preventDefault();
document.removeEventListener('keydown', reiniciar);
inicializar();
};
document.addEventListener('keydown', reiniciar);
}
function atualizarPlacar() {
placarEl.textContent = pontos;
recordeEl.textContent = Math.max(recorde, pontos);
}
// ── Renderização ──────────────────────────────────────────
function desenhar() {
// Fundo
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Grade sutil
ctx.strokeStyle = 'rgba(255,255,255,0.03)';
ctx.lineWidth = 0.5;
for (let i = 0; i <= COLUNAS; i++) {
ctx.beginPath();
ctx.moveTo(i * TAMANHO_CELULA, 0);
ctx.lineTo(i * TAMANHO_CELULA, canvas.height);
ctx.stroke();
}
for (let i = 0; i <= LINHAS; i++) {
ctx.beginPath();
ctx.moveTo(0, i * TAMANHO_CELULA);
ctx.lineTo(canvas.width, i * TAMANHO_CELULA);
ctx.stroke();
}
// Comida
const cx = comida.x * TAMANHO_CELULA + TAMANHO_CELULA / 2;
const cy = comida.y * TAMANHO_CELULA + TAMANHO_CELULA / 2;
ctx.fillStyle = '#E8632A';
ctx.beginPath();
ctx.arc(cx, cy, TAMANHO_CELULA / 2 - 2, 0, Math.PI * 2);
ctx.fill();
// Cobra
cobra.forEach((segmento, indice) => {
const ehCabeca = indice === 0;
const x = segmento.x * TAMANHO_CELULA;
const y = segmento.y * TAMANHO_CELULA;
const s = TAMANHO_CELULA;
const r = ehCabeca ? 6 : 3; // cantos mais arredondados na cabeça
// Gradiente de cor do corpo (cabeça mais clara)
const progresso = indice / cobra.length;
const verde = Math.floor(200 - progresso * 80);
ctx.fillStyle = ehCabeca
? `rgb(80, ${verde + 55}, 80)`
: `rgb(40, ${verde}, 40)`;
// Retângulo arredondado
ctx.beginPath();
ctx.roundRect(x + 1, y + 1, s - 2, s - 2, r);
ctx.fill();
// Olhos na cabeça
if (ehCabeca) {
ctx.fillStyle = 'white';
const olhoOffset = 4;
if (direcao.x === 1) { // direita
ctx.fillRect(x + s - 6, y + olhoOffset, 3, 3);
ctx.fillRect(x + s - 6, y + s - olhoOffset - 3, 3, 3);
} else if (direcao.x === -1) { // esquerda
ctx.fillRect(x + 3, y + olhoOffset, 3, 3);
ctx.fillRect(x + 3, y + s - olhoOffset - 3, 3, 3);
} else if (direcao.y === -1) { // cima
ctx.fillRect(x + olhoOffset, y + 3, 3, 3);
ctx.fillRect(x + s - olhoOffset - 3, y + 3, 3, 3);
} else { // baixo
ctx.fillRect(x + olhoOffset, y + s - 6, 3, 3);
ctx.fillRect(x + s - olhoOffset - 3, y + s - 6, 3, 3);
}
}
});
}
// Iniciar
inicializar();
14.5.7 — Próximos passos: onde ir depois¶
Os três jogos deste capítulo introduzem os fundamentos do desenvolvimento de jogos no navegador. Para aprofundamento:
Frameworks e bibliotecas de jogos 2D: - Phaser (phaser.io) — o framework mais popular para jogos 2D no navegador; inclui física, animações, tilemaps e muito mais - PixiJS (pixijs.com) — renderização 2D de alto desempenho com WebGL
Conceitos para explorar:
- Física básica: gravidade, velocidade, aceleração
- Detecção de colisão AABB (Axis-Aligned Bounding Box)
- Tilemaps: mapas baseados em grades
- Sprites e animações de quadros
- Som com a Web Audio API
- Persistência de dados com localStorage
Referências: - MDN — Canvas API - MDN — requestAnimationFrame - javascript.info — Canvas
Referências gerais do capítulo: - MDN — Introdução ao DOM - MDN — EventTarget.addEventListener - javascript.info — Document
Atividades — Capítulo 14¶
1. Qual é a diferença entre textContent e innerHTML ao modificar o conteúdo de um elemento?
2. O que é delegação de eventos e por que é preferível a adicionar listeners individuais a cada elemento de uma lista dinâmica?
3. Por que requestAnimationFrame é preferível a setInterval para game loops?
-
GitHub Classroom: Implementar uma aplicação de lista de tarefas completa com: adicionar, concluir e remover tarefas via DOM; persistência no
localStorage; filtros (todas, ativas, concluídas) usando delegação de eventos; e contador de tarefas pendentes atualizado em tempo real. (link será adicionado) -
Desafio (opcional): Estender o jogo Snake adicionando: níveis de dificuldade selecionáveis antes de iniciar; obstáculos fixos que aumentam a cada nível; efeito sonoro de "comer" com a Web Audio API. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 13 — JavaScript Essencial :material-arrow-right: Ir ao Capítulo 15 — Eventos e Formulários com JavaScript
Capítulo 15 — Eventos e Formulários com JavaScript¶
Vídeo curto explicativo (link será adicionado posteriormente)
15.1 — Revisão aprofundada do sistema de eventos¶
Vídeo curto explicativo (link será adicionado posteriormente)
O sistema de eventos do navegador é mais rico do que uma primeira leitura sugere. O Capítulo 14 introduziu addEventListener e os eventos mais comuns. Esta seção aprofunda o modelo de propagação de eventos — um mecanismo que, quando bem compreendido, permite construir interfaces interativas mais sofisticadas com menos código.
15.1.1 — O ciclo de vida de um evento: captura, alvo e borbulhamento¶
Quando um evento ocorre em um elemento, ele não surge diretamente naquele elemento — ele percorre a árvore DOM em três fases distintas:
Fase 1 — CAPTURA (de cima para baixo):
document → html → body → section → div → botão
Fase 2 — ALVO:
O evento chega ao elemento que o originou (botão)
Fase 3 — BORBULHAMENTO (de baixo para cima):
botão → div → section → body → html → document
Imagem sugerida: diagrama vertical da árvore DOM mostrando as três fases com setas — seta descendo para captura (azul), círculo no alvo (laranja) e seta subindo para borbulhamento (verde).
(imagem será adicionada posteriormente)
Por padrão, addEventListener registra listeners na fase de borbulhamento. Para registrar na fase de captura, passe { capture: true } como terceiro argumento:
const pai = document.querySelector('.container');
const filho = document.querySelector('.botao');
// Listener na fase de borbulhamento (padrão)
pai.addEventListener('click', () => {
console.log('PAI — borbulhamento');
});
// Listener na fase de captura
pai.addEventListener('click', () => {
console.log('PAI — captura');
}, { capture: true });
filho.addEventListener('click', () => {
console.log('FILHO — alvo');
});
// Ao clicar no filho, a ordem de saída é:
// PAI — captura (fase 1: desce)
// FILHO — alvo (fase 2: alvo)
// PAI — borbulhamento (fase 3: sobe)
Caso de uso real para captura: interceptar eventos antes que cheguem ao alvo — por exemplo, um sistema de analytics que registra todos os cliques em uma página antes que os handlers individuais os processem.
15.1.2 — stopPropagation vs stopImmediatePropagation¶
const container = document.querySelector('.container');
const botao = document.querySelector('.botao');
container.addEventListener('click', () => {
console.log('Container recebeu o clique');
});
// stopPropagation: impede que o evento continue subindo
botao.addEventListener('click', (e) => {
e.stopPropagation();
console.log('Botão clicado — evento NÃO chega ao container');
});
// stopImmediatePropagation: além de parar a propagação,
// impede outros listeners NO MESMO ELEMENTO de serem chamados
botao.addEventListener('click', (e) => {
e.stopImmediatePropagation();
console.log('Este listener executa');
});
botao.addEventListener('click', () => {
console.log('Este listener NÃO executa — foi bloqueado');
});
Cuidado com
stopPropagation: seu uso indiscriminado pode quebrar funcionalidades que dependem do borbulhamento — como delegação de eventos e bibliotecas de analytics. Use apenas quando houver necessidade explícita de isolar um evento.
15.1.3 — preventDefault: quando e por que usar¶
preventDefault cancela o comportamento padrão do navegador para aquele evento — sem interromper sua propagação:
// Cancelar envio de formulário para processamento via JS
document.querySelector('form').addEventListener('submit', (e) => {
e.preventDefault(); // sem isso, a página recarregaria
validarEEnviar();
});
// Cancelar navegação de link
document.querySelector('a.modal-trigger').addEventListener('click', (e) => {
e.preventDefault(); // sem isso, o navegador seguiria o href
abrirModal();
});
// Cancelar menu de contexto (clique direito)
document.querySelector('.area-especial').addEventListener('contextmenu', (e) => {
e.preventDefault();
exibirMenuCustomizado(e.clientX, e.clientY);
});
// Cancelar drag padrão em imagens
document.querySelector('img').addEventListener('dragstart', (e) => {
e.preventDefault();
});
// Verificar se pode ser cancelado
document.addEventListener('scroll', (e) => {
console.log(e.cancelable); // → false — scroll não pode ser cancelado assim
});
15.1.4 — Eventos customizados com CustomEvent¶
JavaScript permite criar e despachar eventos personalizados — uma forma de comunicação entre componentes sem dependência direta:
// Criar e despachar um evento customizado
const evento = new CustomEvent('tarefa-concluida', {
detail: { // dados anexados ao evento
id: 42,
titulo: 'Estudar DOM',
completadoEm: new Date()
},
bubbles: true, // borbulha pela árvore DOM
cancelable: true // pode ser cancelado com preventDefault
});
document.querySelector('#lista').dispatchEvent(evento);
// Ouvir o evento customizado em qualquer ancestral
document.addEventListener('tarefa-concluida', (e) => {
console.log('Tarefa concluída:', e.detail.titulo);
atualizarContador();
salvarProgresso(e.detail);
});
// Exemplo prático: componente de notificação desacoplado
function notificar(mensagem, tipo = 'info') {
document.dispatchEvent(new CustomEvent('notificacao', {
detail: { mensagem, tipo },
bubbles: false
}));
}
document.addEventListener('notificacao', (e) => {
exibirToast(e.detail.mensagem, e.detail.tipo);
});
// Qualquer parte do código pode chamar:
notificar('Salvo com sucesso!', 'sucesso');
notificar('Erro ao conectar.', 'erro');
15.1.5 — Exercício prático: sistema de abas sem biblioteca¶
Um sistema de abas é um dos componentes mais comuns de interfaces web. Implementá-lo do zero consolida: seleção de elementos, manipulação de classes, eventos e acessibilidade com ARIA.
<!-- HTML do componente de abas -->
<div class="abas" id="abas-curso">
<!-- Lista de abas: role="tablist" para acessibilidade -->
<div class="abas__lista" role="tablist" aria-label="Seções do curso">
<button
class="abas__botao abas__botao--ativo"
role="tab"
id="aba-ementa"
aria-controls="painel-ementa"
aria-selected="true"
type="button"
>Ementa</button>
<button
class="abas__botao"
role="tab"
id="aba-objetivos"
aria-controls="painel-objetivos"
aria-selected="false"
tabindex="-1"
type="button"
>Objetivos</button>
<button
class="abas__botao"
role="tab"
id="aba-avaliacao"
aria-controls="painel-avaliacao"
aria-selected="false"
tabindex="-1"
type="button"
>Avaliação</button>
</div>
<!-- Painéis de conteúdo -->
<div
class="abas__painel"
role="tabpanel"
id="painel-ementa"
aria-labelledby="aba-ementa"
>
<h3>Ementa</h3>
<p>HTML semântico, CSS moderno, JavaScript, APIs...</p>
</div>
<div
class="abas__painel abas__painel--oculto"
role="tabpanel"
id="painel-objetivos"
aria-labelledby="aba-objetivos"
hidden
>
<h3>Objetivos</h3>
<p>Desenvolver interfaces web modernas e responsivas...</p>
</div>
<div
class="abas__painel abas__painel--oculto"
role="tabpanel"
id="painel-avaliacao"
aria-labelledby="aba-avaliacao"
hidden
>
<h3>Avaliação</h3>
<p>Atividades práticas, mini-projetos e projeto final...</p>
</div>
</div>
// Sistema de abas acessível — navegação por teclado incluída
function inicializarAbas(containerSelector) {
const container = document.querySelector(containerSelector);
if (!container) return;
const botoes = container.querySelectorAll('[role="tab"]');
const paineis = container.querySelectorAll('[role="tabpanel"]');
function ativarAba(botaoAlvo) {
// Desativar todas as abas
botoes.forEach(btn => {
btn.setAttribute('aria-selected', 'false');
btn.setAttribute('tabindex', '-1');
btn.classList.remove('abas__botao--ativo');
});
// Ocultar todos os painéis
paineis.forEach(painel => {
painel.hidden = true;
});
// Ativar a aba alvo
botaoAlvo.setAttribute('aria-selected', 'true');
botaoAlvo.removeAttribute('tabindex');
botaoAlvo.classList.add('abas__botao--ativo');
botaoAlvo.focus();
// Exibir painel correspondente
const painelId = botaoAlvo.getAttribute('aria-controls');
const painel = document.getElementById(painelId);
if (painel) painel.hidden = false;
// Disparar evento customizado
container.dispatchEvent(new CustomEvent('aba-mudou', {
detail: { abaId: botaoAlvo.id, painelId },
bubbles: true
}));
}
// Clique nas abas
botoes.forEach(botao => {
botao.addEventListener('click', () => ativarAba(botao));
});
// Navegação por teclado (padrão WAI-ARIA para tablist)
container.addEventListener('keydown', (e) => {
const abaAtiva = container.querySelector('[role="tab"][aria-selected="true"]');
const lista = [...botoes];
const indiceAtual = lista.indexOf(abaAtiva);
let novoIndice;
switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
novoIndice = (indiceAtual + 1) % lista.length;
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
novoIndice = (indiceAtual - 1 + lista.length) % lista.length;
break;
case 'Home':
e.preventDefault();
novoIndice = 0;
break;
case 'End':
e.preventDefault();
novoIndice = lista.length - 1;
break;
default:
return;
}
ativarAba(lista[novoIndice]);
});
}
// Inicializar todas as instâncias de abas na página
inicializarAbas('#abas-curso');
15.2 — Eventos de teclado em profundidade¶
Vídeo curto explicativo (link será adicionado posteriormente)
15.2.1 — keydown, keyup e key vs code¶
document.addEventListener('keydown', (e) => {
// e.key: o valor da tecla conforme o layout do teclado
// Muda com Shift, CapsLock e layout de idioma
console.log(e.key);
// → 'a', 'A' (com Shift), 'Enter', 'ArrowLeft', 'Escape', ' '
// e.code: o código físico da tecla — independente do layout
// Não muda com Shift ou layout de idioma
console.log(e.code);
// → 'KeyA', 'Enter', 'ArrowLeft', 'Escape', 'Space'
});
// Quando usar key vs code:
// - key: para capturar o CARACTERE digitado (busca, formulário)
// - code: para capturar a POSIÇÃO física da tecla (atalhos, jogos)
// Exemplo: atalho que funciona igual no teclado QWERTY e AZERTY
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.code === 'KeyS') { // 'S' físico, independente do layout
e.preventDefault();
salvar();
}
});
// Diferença prática:
// Teclado AZERTY: a tecla na posição de 'Q' no QWERTY
// → e.key = 'a' (caractere do layout AZERTY)
// → e.code = 'KeyQ' (posição física)
15.2.2 — Atalhos de teclado: detectando combinações¶
// Mapa de atalhos — fácil de manter e estender
const atalhos = {
'ctrl+s': () => salvar(),
'ctrl+z': () => desfazer(),
'ctrl+shift+z': () => refazer(),
'ctrl+/': () => toggleComentario(),
'escape': () => fecharModal(),
'f1': () => abrirAjuda(),
'ctrl+k': () => focarBusca(),
};
function obterChaveAtalho(e) {
const partes = [];
if (e.ctrlKey || e.metaKey) partes.push('ctrl');
if (e.altKey) partes.push('alt');
if (e.shiftKey) partes.push('shift');
const tecla = e.key.toLowerCase();
// Não duplica modificadores
if (!['control','alt','shift','meta'].includes(tecla)) {
partes.push(tecla);
}
return partes.join('+');
}
document.addEventListener('keydown', (e) => {
const chave = obterChaveAtalho(e);
const handler = atalhos[chave];
if (handler) {
// Não ativar atalhos quando usuário está digitando em campo
const emCampo = ['INPUT','TEXTAREA','SELECT'].includes(
document.activeElement.tagName
);
if (!emCampo || chave === 'escape') {
e.preventDefault();
handler();
}
}
});
15.2.3 — Navegação por teclado customizada¶
// Navegação por setas em uma lista de itens
function tornarListaNavegavel(listaSelector) {
const lista = document.querySelector(listaSelector);
const itens = () => [...lista.querySelectorAll('[data-navegavel]')];
lista.addEventListener('keydown', (e) => {
const todos = itens();
const ativo = document.activeElement;
const indice = todos.indexOf(ativo);
if (indice === -1) return;
let novoIndice;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
novoIndice = Math.min(indice + 1, todos.length - 1);
break;
case 'ArrowUp':
e.preventDefault();
novoIndice = Math.max(indice - 1, 0);
break;
case 'Home':
e.preventDefault();
novoIndice = 0;
break;
case 'End':
e.preventDefault();
novoIndice = todos.length - 1;
break;
default:
return;
}
todos[novoIndice].focus();
});
// Garantir que apenas o item focado está no tab order
lista.addEventListener('focusin', (e) => {
const todos = itens();
if (!todos.includes(e.target)) return;
todos.forEach(item => item.setAttribute('tabindex', '-1'));
e.target.setAttribute('tabindex', '0');
});
}
15.2.4 — Exercício prático: campo de busca com atalho e navegação por setas¶
<div class="busca-container">
<div class="busca-campo">
<input
type="search"
id="campo-busca"
placeholder="Buscar... (Ctrl+K)"
role="combobox"
aria-expanded="false"
aria-controls="lista-sugestoes"
aria-autocomplete="list"
autocomplete="off"
/>
</div>
<ul
class="busca-sugestoes oculto"
id="lista-sugestoes"
role="listbox"
aria-label="Sugestões de busca"
></ul>
</div>
const DADOS = [
'HTML semântico', 'CSS Flexbox', 'CSS Grid', 'JavaScript ES6',
'Manipulação do DOM', 'Eventos', 'Fetch API', 'Promises',
'async/await', 'Tailwind CSS', 'Design System', 'Acessibilidade',
'Responsividade', 'Media Queries', 'Canvas API', 'LocalStorage'
];
const input = document.querySelector('#campo-busca');
const lista = document.querySelector('#lista-sugestoes');
let sugestoesVisiveis = [];
let indiceFocado = -1;
// Atalho global: Ctrl+K foca o campo de busca
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
input.focus();
input.select();
}
});
// Busca em tempo real
input.addEventListener('input', () => {
const termo = input.value.trim().toLowerCase();
indiceFocado = -1;
if (!termo) {
fecharSugestoes();
return;
}
sugestoesVisiveis = DADOS.filter(item =>
item.toLowerCase().includes(termo)
);
renderizarSugestoes(termo);
});
function renderizarSugestoes(termo) {
lista.innerHTML = '';
if (!sugestoesVisiveis.length) {
fecharSugestoes();
return;
}
const fragment = document.createDocumentFragment();
sugestoesVisiveis.forEach((sugestao, idx) => {
const li = document.createElement('li');
li.role = 'option';
li.setAttribute('tabindex', '-1');
li.dataset.indice = idx;
// Destaca o termo buscado no texto
const regex = new RegExp(`(${escapeRegex(termo)})`, 'gi');
li.innerHTML = sugestao.replace(regex, '<mark>$1</mark>');
li.addEventListener('click', () => selecionarSugestao(sugestao));
fragment.appendChild(li);
});
lista.appendChild(fragment);
lista.classList.remove('oculto');
input.setAttribute('aria-expanded', 'true');
}
// Navegação por teclado nas sugestões
input.addEventListener('keydown', (e) => {
const itens = lista.querySelectorAll('li');
if (!itens.length) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
indiceFocado = Math.min(indiceFocado + 1, itens.length - 1);
atualizarFoco(itens);
break;
case 'ArrowUp':
e.preventDefault();
if (indiceFocado <= 0) {
indiceFocado = -1;
input.focus();
} else {
indiceFocado--;
atualizarFoco(itens);
}
break;
case 'Enter':
if (indiceFocado >= 0) {
e.preventDefault();
selecionarSugestao(sugestoesVisiveis[indiceFocado]);
}
break;
case 'Escape':
fecharSugestoes();
input.focus();
break;
}
});
function atualizarFoco(itens) {
itens.forEach((item, i) => {
const ativo = i === indiceFocado;
item.classList.toggle('sugestao--ativa', ativo);
item.setAttribute('aria-selected', ativo);
if (ativo) item.scrollIntoView({ block: 'nearest' });
});
input.setAttribute('aria-activedescendant',
indiceFocado >= 0 ? `sugestao-${indiceFocado}` : ''
);
}
function selecionarSugestao(valor) {
input.value = valor;
fecharSugestoes();
input.focus();
input.dispatchEvent(new CustomEvent('busca-selecionada', {
detail: { valor }, bubbles: true
}));
}
function fecharSugestoes() {
lista.classList.add('oculto');
lista.innerHTML = '';
input.setAttribute('aria-expanded', 'false');
sugestoesVisiveis = [];
indiceFocado = -1;
}
// Fecha ao clicar fora
document.addEventListener('click', (e) => {
if (!e.target.closest('.busca-container')) fecharSugestoes();
});
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
15.3 — Eventos de mouse e pointer¶
Vídeo curto explicativo (link será adicionado posteriormente)
15.3.1 — A família de eventos de pointer¶
Os Pointer Events unificam mouse, toque (touch) e caneta (stylus) em uma única API, eliminando a necessidade de tratar separadamente mousedown/touchstart:
const elemento = document.querySelector('.interativo');
// Pointer Events — funcionam com mouse, toque e caneta
elemento.addEventListener('pointerdown', handler); // botão/dedo pressionado
elemento.addEventListener('pointerup', handler); // botão/dedo liberado
elemento.addEventListener('pointermove', handler); // movimento
elemento.addEventListener('pointerenter', handler); // entra no elemento
elemento.addEventListener('pointerleave', handler); // sai do elemento
elemento.addEventListener('pointercancel', handler); // interação cancelada (ex: scroll iniciado)
// Propriedades específicas de pointer
elemento.addEventListener('pointerdown', (e) => {
e.pointerId // ID único do ponto de toque (útil para multi-touch)
e.pointerType // → 'mouse', 'touch', 'pen'
e.pressure // → 0 a 1 (pressão — caneta/touch)
e.width // largura da área de contato
e.height // altura da área de contato
e.clientX // posição X
e.clientY // posição Y
e.isPrimary // → true para o primeiro ponto de multi-touch
});
// setPointerCapture — mantém o pointer no elemento mesmo
// quando o cursor sai dos seus limites (essencial para drag)
elemento.addEventListener('pointerdown', (e) => {
elemento.setPointerCapture(e.pointerId);
});
15.3.2 — Drag and drop com eventos de pointer¶
// Implementação de drag reutilizável
function tornarArrastavel(elemento) {
let arrastando = false;
let offsetX, offsetY;
elemento.addEventListener('pointerdown', (e) => {
if (e.button !== 0 && e.pointerType === 'mouse') return; // apenas botão esquerdo
arrastando = true;
elemento.setPointerCapture(e.pointerId);
const rect = elemento.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
elemento.classList.add('arrastando');
elemento.style.cursor = 'grabbing';
});
elemento.addEventListener('pointermove', (e) => {
if (!arrastando) return;
const pai = elemento.parentElement;
const paiRect = pai.getBoundingClientRect();
let x = e.clientX - paiRect.left - offsetX;
let y = e.clientY - paiRect.top - offsetY;
// Conter dentro do pai
x = Math.max(0, Math.min(x, paiRect.width - elemento.offsetWidth));
y = Math.max(0, Math.min(y, paiRect.height - elemento.offsetHeight));
elemento.style.left = `${x}px`;
elemento.style.top = `${y}px`;
});
elemento.addEventListener('pointerup', () => {
arrastando = false;
elemento.classList.remove('arrastando');
elemento.style.cursor = 'grab';
});
elemento.addEventListener('pointercancel', () => {
arrastando = false;
elemento.classList.remove('arrastando');
elemento.style.cursor = 'grab';
});
// Estilo inicial
elemento.style.position = 'absolute';
elemento.style.cursor = 'grab';
elemento.style.userSelect = 'none';
elemento.style.touchAction = 'none'; // previne scroll em touch
}
15.3.3 — Exercício prático: lista reordenável por arrastar e soltar¶
<ul class="lista-reordenavel" id="lista-ordem" aria-label="Lista reordenável">
<li class="item-lista" draggable="true" data-id="1">
<span class="handle" aria-hidden="true">⠿</span>
<span>Fundamentos de HTML</span>
</li>
<li class="item-lista" draggable="true" data-id="2">
<span class="handle" aria-hidden="true">⠿</span>
<span>CSS Moderno</span>
</li>
<li class="item-lista" draggable="true" data-id="3">
<span class="handle" aria-hidden="true">⠿</span>
<span>JavaScript Essencial</span>
</li>
<li class="item-lista" draggable="true" data-id="4">
<span class="handle" aria-hidden="true">⠿</span>
<span>Manipulação do DOM</span>
</li>
<li class="item-lista" draggable="true" data-id="5">
<span class="handle" aria-hidden="true">⠿</span>
<span>Fetch API e Projetos</span>
</li>
</ul>
function inicializarListaReordenavel(listaSelector) {
const lista = document.querySelector(listaSelector);
let itemArrastado = null;
let itemOrigem = null;
// Usando Drag and Drop API nativa (para listas de elementos HTML)
lista.addEventListener('dragstart', (e) => {
itemArrastado = e.target.closest('.item-lista');
itemOrigem = itemArrastado;
// Pequeno delay para o estilo de arrastar não afetar o ghost
requestAnimationFrame(() => {
itemArrastado.classList.add('arrastando');
});
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', itemArrastado.dataset.id);
});
lista.addEventListener('dragend', () => {
if (itemArrastado) {
itemArrastado.classList.remove('arrastando');
itemArrastado = null;
}
lista.querySelectorAll('.item-lista').forEach(item => {
item.classList.remove('sobre-item');
});
salvarOrdem();
});
lista.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const alvo = e.target.closest('.item-lista');
if (!alvo || alvo === itemArrastado) return;
// Determinar se inserir antes ou depois pelo meio do elemento
const rect = alvo.getBoundingClientRect();
const meio = rect.top + rect.height / 2;
const antes = e.clientY < meio;
lista.querySelectorAll('.item-lista').forEach(i =>
i.classList.remove('sobre-item', 'inserir-antes', 'inserir-depois')
);
alvo.classList.add('sobre-item', antes ? 'inserir-antes' : 'inserir-depois');
});
lista.addEventListener('drop', (e) => {
e.preventDefault();
const alvo = e.target.closest('.item-lista');
if (!alvo || alvo === itemArrastado) return;
const rect = alvo.getBoundingClientRect();
const antes = e.clientY < rect.top + rect.height / 2;
if (antes) {
lista.insertBefore(itemArrastado, alvo);
} else {
lista.insertBefore(itemArrastado, alvo.nextSibling);
}
});
// Navegação por teclado para reordenar (acessibilidade)
lista.addEventListener('keydown', (e) => {
const item = e.target.closest('.item-lista');
if (!item) return;
if (e.altKey && e.key === 'ArrowUp') {
e.preventDefault();
const anterior = item.previousElementSibling;
if (anterior) {
lista.insertBefore(item, anterior);
item.focus();
salvarOrdem();
}
}
if (e.altKey && e.key === 'ArrowDown') {
e.preventDefault();
const proximo = item.nextElementSibling;
if (proximo) {
lista.insertBefore(proximo, item);
item.focus();
salvarOrdem();
}
}
});
function salvarOrdem() {
const ordem = [...lista.querySelectorAll('.item-lista')]
.map(item => item.dataset.id);
localStorage.setItem('ordem-lista', JSON.stringify(ordem));
lista.dispatchEvent(new CustomEvent('lista-reordenada', {
detail: { ordem }, bubbles: true
}));
}
// Restaurar ordem salva
const ordemSalva = JSON.parse(localStorage.getItem('ordem-lista') || 'null');
if (ordemSalva) {
ordemSalva.forEach(id => {
const item = lista.querySelector(`[data-id="${id}"]`);
if (item) lista.appendChild(item);
});
}
}
inicializarListaReordenavel('#lista-ordem');
15.4 — Validação de formulários com JavaScript¶
Vídeo curto explicativo (link será adicionado posteriormente)
A validação nativa do HTML (atributos required, pattern, type) oferece uma camada de proteção básica e útil para o usuário. Contudo, ela tem limitações significativas para aplicações reais: mensagens de erro genéricas, ausência de validação cruzada entre campos, impossibilidade de validação assíncrona (verificar se um e-mail já existe no servidor) e dificuldade de customização visual. O JavaScript preenche essas lacunas, permitindo validação rica, em tempo real e completamente customizada.
Princípio fundamental (reforçado do Capítulo 5): validação no frontend é uma conveniência para o usuário. A validação definitiva sempre ocorre no servidor. Todo código de validação JavaScript pode ser contornado por um usuário com conhecimento técnico.
15.4.1 — Limitações da validação nativa do HTML¶
<!-- Problemas com a validação nativa -->
<!-- 1. Mensagens genéricas e não customizáveis em todos os navegadores -->
<input type="email" required />
<!-- Chrome: "Insira um endereço de e-mail válido" -->
<!-- Firefox: "Por favor, insira um endereço de e-mail válido" -->
<!-- 2. Sem validação cruzada -->
<input type="password" id="senha" />
<input type="password" id="confirmar-senha" />
<!-- Não há como verificar se os dois campos são iguais com HTML puro -->
<!-- 3. Feedback apenas no submit — não em tempo real -->
<!-- O usuário só descobre os erros ao tentar enviar -->
<!-- 4. Impossível validar com dados externos -->
<!-- "Este e-mail já está cadastrado?" requer uma requisição ao servidor -->
15.4.2 — A Constraint Validation API¶
O navegador expõe uma API JavaScript para interagir com o sistema de validação nativo:
const input = document.querySelector('#email');
// Verificar validade
input.validity.valid // → boolean: campo é válido?
input.validity.valueMissing // → true se required e vazio
input.validity.typeMismatch // → true se type="email" e formato inválido
input.validity.patternMismatch // → true se pattern não satisfeito
input.validity.tooShort // → true se menor que minlength
input.validity.tooLong // → true se maior que maxlength
input.validity.rangeUnderflow // → true se menor que min
input.validity.rangeOverflow // → true se maior que max
input.validity.stepMismatch // → true se não múltiplo de step
input.validity.customError // → true se setCustomValidity foi chamado
// Obter mensagem de erro do navegador
input.validationMessage // → "Por favor, preencha este campo."
// Definir erro customizado (entra na validação nativa)
input.setCustomValidity('Este e-mail já está cadastrado.');
input.setCustomValidity(''); // limpa o erro customizado
// Verificar se o formulário inteiro é válido
const form = document.querySelector('form');
form.checkValidity() // → boolean
form.reportValidity() // → boolean + exibe erros nativos
// novalidate: desabilita validação nativa para usar a própria
// <form novalidate>
15.4.3 — Validação em tempo real com o evento input¶
A estratégia mais eficaz para UX é validar enquanto o usuário digita, mas com uma experiência cuidadosa: não mostrar erros antes que o usuário interaja com o campo, e não punir precocemente quem ainda está digitando.
// Estratégia: validar no blur (saída do campo) e depois em tempo real
function configurarValidacaoTempoReal(campo, validar) {
let jaInteragiu = false;
// Primeiro erro: mostrado ao sair do campo
campo.addEventListener('blur', () => {
jaInteragiu = true;
const erro = validar(campo.value);
exibirErro(campo, erro);
});
// Erros subsequentes: atualizados em tempo real
campo.addEventListener('input', () => {
if (!jaInteragiu) return;
const erro = validar(campo.value);
exibirErro(campo, erro);
});
}
// Exibir/limpar mensagem de erro acessível
function exibirErro(campo, mensagem) {
const campoWrapper = campo.closest('.campo');
if (!campoWrapper) return;
const erroEl = campoWrapper.querySelector('.campo__erro');
const iconeSucesso = campoWrapper.querySelector('.campo__icone-sucesso');
if (mensagem) {
campo.setAttribute('aria-invalid', 'true');
if (erroEl) {
erroEl.textContent = mensagem;
erroEl.hidden = false;
}
campoWrapper.classList.add('campo--erro');
campoWrapper.classList.remove('campo--valido');
if (iconeSucesso) iconeSucesso.hidden = true;
} else {
campo.setAttribute('aria-invalid', 'false');
if (erroEl) {
erroEl.textContent = '';
erroEl.hidden = true;
}
campoWrapper.classList.remove('campo--erro');
campoWrapper.classList.add('campo--valido');
if (iconeSucesso) iconeSucesso.hidden = false;
}
}
15.4.4 — Validando diferentes tipos de componentes HTML¶
Esta seção demonstra a validação de todos os tipos principais de componentes de formulário — cada um com suas particularidades.
Validando <input type="text">
// Regras de validação para texto livre
function validarNome(valor) {
if (!valor.trim()) return 'O nome é obrigatório.';
if (valor.trim().length < 3) return 'O nome deve ter pelo menos 3 caracteres.';
if (valor.trim().length > 100) return 'O nome deve ter no máximo 100 caracteres.';
if (!/^[a-zA-ZÀ-ÿ\s'-]+$/.test(valor)) return 'O nome deve conter apenas letras.';
return null; // sem erro
}
Validando <input type="email">
function validarEmail(valor) {
if (!valor.trim()) return 'O e-mail é obrigatório.';
// Regex RFC 5322 simplificada para uso prático
const regexEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!regexEmail.test(valor)) return 'Informe um e-mail válido. Ex.: usuario@dominio.com';
return null;
}
Validando <input type="password"> com confirmação
function validarSenha(valor) {
if (!valor) return 'A senha é obrigatória.';
if (valor.length < 8) return 'A senha deve ter pelo menos 8 caracteres.';
if (!/[A-Z]/.test(valor)) return 'A senha deve conter pelo menos uma letra maiúscula.';
if (!/[0-9]/.test(valor)) return 'A senha deve conter pelo menos um número.';
if (!/[!@#$%^&*]/.test(valor)) return 'A senha deve conter pelo menos um caractere especial (!@#$%^&*).';
return null;
}
function validarConfirmacaoSenha(valorConfirmacao, valorSenha) {
if (!valorConfirmacao) return 'A confirmação de senha é obrigatória.';
if (valorConfirmacao !== valorSenha) return 'As senhas não coincidem.';
return null;
}
// Indicador de força da senha
function calcularForcaSenha(senha) {
let pontos = 0;
if (senha.length >= 8) pontos++;
if (senha.length >= 12) pontos++;
if (/[A-Z]/.test(senha)) pontos++;
if (/[0-9]/.test(senha)) pontos++;
if (/[!@#$%^&*]/.test(senha)) pontos++;
if (pontos <= 1) return { nivel: 'fraca', label: 'Fraca', cor: '#dc2626' };
if (pontos <= 3) return { nivel: 'media', label: 'Média', cor: '#d97706' };
return { nivel: 'forte', label: 'Forte', cor: '#16a34a' };
}
Validando <input type="tel">
function validarTelefone(valor) {
if (!valor.trim()) return null; // campo opcional
// Formatos aceitos: (82) 99999-9999 ou (82) 9999-9999
const regex = /^\(\d{2}\)\s?\d{4,5}-\d{4}$/;
if (!regex.test(valor)) {
return 'Informe o telefone no formato (DDD) NNNNN-NNNN.';
}
return null;
}
// Máscara automática para telefone
function aplicarMascaraTelefone(input) {
input.addEventListener('input', (e) => {
let v = e.target.value.replace(/\D/g, ''); // apenas dígitos
if (v.length > 11) v = v.slice(0, 11);
if (v.length <= 10) {
v = v.replace(/(\d{2})(\d{4})(\d{0,4})/, '($1) $2-$3');
} else {
v = v.replace(/(\d{2})(\d{5})(\d{0,4})/, '($1) $2-$3');
}
e.target.value = v;
});
}
Validando <input type="date">
function validarDataNascimento(valor) {
if (!valor) return 'A data de nascimento é obrigatória.';
const data = new Date(valor + 'T00:00:00'); // evita problemas de fuso
const hoje = new Date();
if (isNaN(data.getTime())) return 'Data inválida.';
const idade = hoje.getFullYear() - data.getFullYear();
const aniversarioPassou =
hoje.getMonth() > data.getMonth() ||
(hoje.getMonth() === data.getMonth() && hoje.getDate() >= data.getDate());
const idadeReal = aniversarioPassou ? idade : idade - 1;
if (idadeReal < 16) return 'Você deve ter pelo menos 16 anos.';
if (idadeReal > 120) return 'Data de nascimento inválida.';
return null;
}
Validando <input type="number">
function validarIdade(valor) {
if (!valor) return 'A idade é obrigatória.';
const numero = Number(valor);
if (!Number.isInteger(numero)) return 'Informe um número inteiro.';
if (numero < 16) return 'A idade mínima é 16 anos.';
if (numero > 120) return 'Informe uma idade válida.';
return null;
}
Validando <select>
function validarSelect(valor, nomeCampo = 'campo') {
if (!valor || valor === '') {
return `Selecione uma opção para ${nomeCampo}.`;
}
return null;
}
// Para select múltiplo
function validarSelectMultiplo(select, min = 1, max = Infinity) {
const selecionados = [...select.selectedOptions].map(o => o.value);
if (selecionados.length < min) {
return `Selecione pelo menos ${min} opção${min > 1 ? 'ões' : ''}.`;
}
if (selecionados.length > max) {
return `Selecione no máximo ${max} opção${max > 1 ? 'ões' : ''}.`;
}
return null;
}
Validando <input type="checkbox">
// Checkbox único (aceite de termos)
function validarCheckbox(checkbox, mensagem = 'Este campo é obrigatório.') {
return checkbox.checked ? null : mensagem;
}
// Grupo de checkboxes (múltiplas escolhas)
function validarGrupoCheckbox(nome, min = 1, max = Infinity) {
const marcados = document.querySelectorAll(
`input[type="checkbox"][name="${nome}"]:checked`
);
if (marcados.length < min) {
return `Selecione pelo menos ${min} opção${min > 1 ? 'ões' : ''}.`;
}
if (marcados.length > max) {
return `Selecione no máximo ${max} opção${max > 1 ? 'ões' : ''}.`;
}
return null;
}
Validando <input type="radio">
function validarRadio(nome) {
const selecionado = document.querySelector(
`input[type="radio"][name="${nome}"]:checked`
);
return selecionado ? null : 'Selecione uma opção.';
}
Validando <textarea>
function validarMensagem(valor, minChars = 20, maxChars = 1000) {
if (!valor.trim()) return 'A mensagem é obrigatória.';
if (valor.trim().length < minChars) {
return `A mensagem deve ter pelo menos ${minChars} caracteres. ` +
`Atual: ${valor.trim().length}.`;
}
if (valor.length > maxChars) {
return `A mensagem deve ter no máximo ${maxChars} caracteres. ` +
`Atual: ${valor.length}.`;
}
return null;
}
// Contador de caracteres em tempo real
function adicionarContadorCaracteres(textarea, max) {
const container = textarea.closest('.campo');
const contador = document.createElement('p');
contador.className = 'campo__contador';
contador.setAttribute('aria-live', 'polite');
container.appendChild(contador);
function atualizar() {
const restante = max - textarea.value.length;
contador.textContent = `${textarea.value.length}/${max} caracteres`;
contador.classList.toggle('campo__contador--limite', restante <= 20);
textarea.setAttribute('aria-describedby',
[textarea.getAttribute('aria-describedby'), contador.id]
.filter(Boolean).join(' ')
);
}
textarea.addEventListener('input', atualizar);
atualizar();
}
Validando <input type="file">
function validarArquivo(input, opcoes = {}) {
const {
obrigatorio = false,
tiposPermitidos = [], // ex.: ['image/jpeg', 'image/png', 'application/pdf']
extensoesPermitidas = [], // ex.: ['.jpg', '.png', '.pdf']
tamanhoMaximoMB = 5
} = opcoes;
const arquivo = input.files[0];
if (!arquivo) {
return obrigatorio ? 'Selecione um arquivo.' : null;
}
if (tiposPermitidos.length && !tiposPermitidos.includes(arquivo.type)) {
return `Tipo de arquivo não permitido. Aceitos: ${extensoesPermitidas.join(', ')}.`;
}
const tamanhoMB = arquivo.size / (1024 * 1024);
if (tamanhoMB > tamanhoMaximoMB) {
return `O arquivo é muito grande (${tamanhoMB.toFixed(1)} MB). Máximo: ${tamanhoMaximoMB} MB.`;
}
return null;
}
// Preview de imagem antes do upload
function configurarPreviewImagem(input, imgPreview) {
input.addEventListener('change', () => {
const arquivo = input.files[0];
if (!arquivo || !arquivo.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = (e) => {
imgPreview.src = e.target.result;
imgPreview.hidden = false;
};
reader.readAsDataURL(arquivo);
});
}
15.4.5 — Mensagens de erro acessíveis com ARIA¶
Mensagens de erro devem ser percebidas por todos os usuários — incluindo aqueles que utilizam leitores de tela:
<!-- Estrutura HTML para campo com erro acessível -->
<div class="campo" id="campo-email-wrapper">
<label class="campo__label" for="email">
E-mail
<span class="campo__obrigatorio" aria-hidden="true">*</span>
</label>
<div class="campo__input-wrapper">
<input
class="campo__input"
type="email"
id="email"
name="email"
required
aria-required="true"
aria-invalid="false"
aria-describedby="email-erro email-dica"
autocomplete="email"
/>
<span class="campo__icone-sucesso" hidden aria-hidden="true">✓</span>
</div>
<p class="campo__dica" id="email-dica">
Ex.: usuario@dominio.com.br
</p>
<!-- role="alert": anunciado automaticamente por leitores de tela ao aparecer -->
<p
class="campo__erro"
id="email-erro"
role="alert"
aria-live="assertive"
hidden
></p>
</div>
// Função centralizada de exibição de erro com ARIA completo
function exibirErroAcessivel(campoId, mensagem) {
const campo = document.getElementById(campoId);
const erroEl = document.getElementById(`${campoId}-erro`);
const wrapper = campo.closest('.campo');
if (mensagem) {
campo.setAttribute('aria-invalid', 'true');
erroEl.textContent = mensagem;
erroEl.hidden = false;
wrapper.classList.add('campo--erro');
wrapper.classList.remove('campo--valido');
} else {
campo.setAttribute('aria-invalid', 'false');
erroEl.textContent = '';
erroEl.hidden = true;
wrapper.classList.remove('campo--erro');
wrapper.classList.add('campo--valido');
}
}
// Focar no primeiro campo com erro após tentativa de submit
function focarPrimeiroErro(form) {
const primeiroErro = form.querySelector('[aria-invalid="true"]');
if (primeiroErro) {
primeiroErro.focus();
primeiroErro.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
// Anúncio de resumo de erros para leitores de tela
function anunciarResumoErros(erros) {
const regiao = document.querySelector('#resumo-erros');
if (!regiao) return;
if (erros.length === 0) {
regiao.hidden = true;
regiao.innerHTML = '';
return;
}
regiao.hidden = false;
regiao.innerHTML = `
<h3>Corrija ${erros.length} erro${erros.length > 1 ? 's' : ''} antes de continuar:</h3>
<ul>
${erros.map(e => `<li><a href="#${e.campoId}">${e.mensagem}</a></li>`).join('')}
</ul>
`;
regiao.focus();
}
15.4.6 — Exercício prático: formulário de cadastro completo¶
Este exercício integra a validação de todos os tipos de componentes vistos nas seções anteriores em um formulário coeso:
<form id="form-cadastro" novalidate aria-label="Formulário de cadastro">
<!-- Resumo de erros (para leitores de tela) -->
<div id="resumo-erros" role="alert" aria-live="assertive" tabindex="-1" hidden></div>
<!-- ── Dados Pessoais ── -->
<fieldset>
<legend>Dados Pessoais</legend>
<!-- Nome completo (text) -->
<div class="campo" id="campo-nome-wrapper">
<label class="campo__label" for="nome">
Nome completo <span aria-hidden="true">*</span>
</label>
<input class="campo__input" type="text" id="nome" name="nome"
required aria-required="true" aria-invalid="false"
aria-describedby="nome-erro"
autocomplete="name" placeholder="Maria da Silva" />
<p class="campo__erro" id="nome-erro" role="alert" hidden></p>
</div>
<!-- E-mail (email) -->
<div class="campo" id="campo-email-wrapper">
<label class="campo__label" for="email">
E-mail <span aria-hidden="true">*</span>
</label>
<input class="campo__input" type="email" id="email" name="email"
required aria-required="true" aria-invalid="false"
aria-describedby="email-erro email-dica"
autocomplete="email" />
<p class="campo__dica" id="email-dica">Ex.: usuario@dominio.com</p>
<p class="campo__erro" id="email-erro" role="alert" hidden></p>
</div>
<!-- Telefone (tel) com máscara -->
<div class="campo" id="campo-telefone-wrapper">
<label class="campo__label" for="telefone">Telefone</label>
<input class="campo__input" type="tel" id="telefone" name="telefone"
aria-invalid="false" aria-describedby="telefone-erro telefone-dica"
autocomplete="tel" placeholder="(82) 99999-9999" />
<p class="campo__dica" id="telefone-dica">Formato: (DDD) NNNNN-NNNN</p>
<p class="campo__erro" id="telefone-erro" role="alert" hidden></p>
</div>
<!-- Data de nascimento (date) -->
<div class="campo" id="campo-nascimento-wrapper">
<label class="campo__label" for="nascimento">
Data de nascimento <span aria-hidden="true">*</span>
</label>
<input class="campo__input" type="date" id="nascimento" name="nascimento"
required aria-required="true" aria-invalid="false"
aria-describedby="nascimento-erro"
autocomplete="bday" />
<p class="campo__erro" id="nascimento-erro" role="alert" hidden></p>
</div>
</fieldset>
<!-- ── Curso e Preferências ── -->
<fieldset>
<legend>Curso e Preferências</legend>
<!-- Curso (select) -->
<div class="campo" id="campo-curso-wrapper">
<label class="campo__label" for="curso">
Curso de interesse <span aria-hidden="true">*</span>
</label>
<select class="campo__input" id="curso" name="curso"
required aria-required="true" aria-invalid="false"
aria-describedby="curso-erro">
<option value="">Selecione um curso...</option>
<option value="si">Sistemas de Informação</option>
<option value="cc">Ciência da Computação</option>
<option value="ec">Engenharia da Computação</option>
<option value="ads">Análise e Desenvolvimento de Sistemas</option>
</select>
<p class="campo__erro" id="curso-erro" role="alert" hidden></p>
</div>
<!-- Turno (radio) -->
<div class="campo" id="campo-turno-wrapper">
<fieldset>
<legend class="campo__label">
Turno de preferência <span aria-hidden="true">*</span>
</legend>
<div class="campo__radio-grupo" role="group">
<label class="campo__radio-label">
<input type="radio" name="turno" value="manha" />
Manhã
</label>
<label class="campo__radio-label">
<input type="radio" name="turno" value="tarde" />
Tarde
</label>
<label class="campo__radio-label">
<input type="radio" name="turno" value="noite" />
Noite
</label>
</div>
<p class="campo__erro" id="turno-erro" role="alert" hidden></p>
</fieldset>
</div>
<!-- Áreas de interesse (checkbox múltiplo) -->
<div class="campo" id="campo-interesses-wrapper">
<fieldset>
<legend class="campo__label">
Áreas de interesse <span aria-hidden="true">*</span>
<span class="campo__dica-inline">(selecione pelo menos 2)</span>
</legend>
<div class="campo__checkbox-grupo">
<label><input type="checkbox" name="interesse" value="web" /> Desenvolvimento Web</label>
<label><input type="checkbox" name="interesse" value="dados" /> Ciência de Dados</label>
<label><input type="checkbox" name="interesse" value="infra" /> Infraestrutura</label>
<label><input type="checkbox" name="interesse" value="seguranca" /> Segurança</label>
<label><input type="checkbox" name="interesse" value="ia" /> Inteligência Artificial</label>
</div>
<p class="campo__erro" id="interesses-erro" role="alert" hidden></p>
</fieldset>
</div>
<!-- Mensagem (textarea) com contador -->
<div class="campo" id="campo-mensagem-wrapper">
<label class="campo__label" for="mensagem">
Por que deseja estudar aqui? <span aria-hidden="true">*</span>
</label>
<textarea class="campo__input" id="mensagem" name="mensagem"
rows="5" required aria-required="true" aria-invalid="false"
aria-describedby="mensagem-erro mensagem-contador"
placeholder="Descreva sua motivação (mínimo 50 caracteres)...">
</textarea>
<p class="campo__contador" id="mensagem-contador" aria-live="polite">0/500</p>
<p class="campo__erro" id="mensagem-erro" role="alert" hidden></p>
</div>
<!-- Foto (file) -->
<div class="campo" id="campo-foto-wrapper">
<label class="campo__label" for="foto">Foto de perfil</label>
<input class="campo__input" type="file" id="foto" name="foto"
accept="image/jpeg,image/png,image/webp"
aria-invalid="false" aria-describedby="foto-erro foto-dica" />
<p class="campo__dica" id="foto-dica">JPG, PNG ou WebP. Máximo 2 MB.</p>
<img id="foto-preview" class="campo__preview" hidden alt="Preview da foto" />
<p class="campo__erro" id="foto-erro" role="alert" hidden></p>
</div>
</fieldset>
<!-- ── Segurança ── -->
<fieldset>
<legend>Acesso</legend>
<!-- Senha (password) com indicador de força -->
<div class="campo" id="campo-senha-wrapper">
<label class="campo__label" for="senha">
Senha <span aria-hidden="true">*</span>
</label>
<div class="campo__input-wrapper">
<input class="campo__input" type="password" id="senha" name="senha"
required aria-required="true" aria-invalid="false"
aria-describedby="senha-erro senha-forca senha-dica"
autocomplete="new-password" />
<button type="button" class="campo__toggle-senha"
aria-label="Mostrar senha" data-target="senha">
👁
</button>
</div>
<div class="campo__forca" id="senha-forca" aria-live="polite"></div>
<p class="campo__dica" id="senha-dica">
Mínimo 8 caracteres, com maiúscula, número e símbolo (!@#$%^&*).
</p>
<p class="campo__erro" id="senha-erro" role="alert" hidden></p>
</div>
<!-- Confirmar senha (password) -->
<div class="campo" id="campo-confirmar-wrapper">
<label class="campo__label" for="confirmar-senha">
Confirmar senha <span aria-hidden="true">*</span>
</label>
<div class="campo__input-wrapper">
<input class="campo__input" type="password" id="confirmar-senha"
name="confirmar_senha"
required aria-required="true" aria-invalid="false"
aria-describedby="confirmar-erro"
autocomplete="new-password" />
<button type="button" class="campo__toggle-senha"
aria-label="Mostrar confirmação" data-target="confirmar-senha">
👁
</button>
</div>
<p class="campo__erro" id="confirmar-erro" role="alert" hidden></p>
</div>
</fieldset>
<!-- Aceite de termos (checkbox) -->
<div class="campo" id="campo-termos-wrapper">
<label class="campo__checkbox-label">
<input type="checkbox" id="termos" name="termos"
required aria-required="true" aria-invalid="false"
aria-describedby="termos-erro" />
Li e aceito os <a href="/termos" target="_blank" rel="noopener">termos de uso</a>
e a <a href="/privacidade" target="_blank" rel="noopener">política de privacidade</a>.
<span aria-hidden="true">*</span>
</label>
<p class="campo__erro" id="termos-erro" role="alert" hidden></p>
</div>
<div class="form-acoes">
<button type="reset" class="btn btn--secundario">Limpar</button>
<button type="submit" class="btn btn--primario">Enviar cadastro</button>
</div>
</form>
// ── Controlador do formulário de cadastro ──────────────────
const form = document.querySelector('#form-cadastro');
// Configuração dos campos e suas regras
const CAMPOS = {
nome: { validar: validarNome },
email: { validar: validarEmail },
telefone: { validar: validarTelefone },
nascimento: { validar: validarDataNascimento },
curso: { validar: (v) => validarSelect(v, 'o curso') },
senha: { validar: validarSenha },
'confirmar-senha': {
validar: (v) => validarConfirmacaoSenha(
v, document.getElementById('senha').value
)
},
mensagem: {
validar: (v) => validarMensagem(v, 50, 500)
},
};
// Inicializar validação em tempo real para campos de texto
Object.entries(CAMPOS).forEach(([id, cfg]) => {
const campo = document.getElementById(id);
if (campo) configurarValidacaoTempoReal(campo, cfg.validar);
});
// Máscara de telefone
aplicarMascaraTelefone(document.getElementById('telefone'));
// Preview de foto
configurarPreviewImagem(
document.getElementById('foto'),
document.getElementById('foto-preview')
);
// Contador de caracteres na mensagem
adicionarContadorCaracteres(document.getElementById('mensagem'), 500);
// Indicador de força da senha
document.getElementById('senha').addEventListener('input', (e) => {
const forca = calcularForcaSenha(e.target.value);
const el = document.getElementById('senha-forca');
el.innerHTML = `
<div class="forca-barra forca-barra--${forca.nivel}"></div>
<span>Força: ${forca.label}</span>
`;
});
// Toggle visibilidade de senha
document.querySelectorAll('.campo__toggle-senha').forEach(btn => {
btn.addEventListener('click', () => {
const targetId = btn.dataset.target;
const input = document.getElementById(targetId);
const visivel = input.type === 'text';
input.type = visivel ? 'password' : 'text';
btn.setAttribute('aria-label', visivel ? 'Mostrar senha' : 'Ocultar senha');
btn.textContent = visivel ? '👁' : '🙈';
});
});
// Submissão: valida todos os campos
form.addEventListener('submit', (e) => {
e.preventDefault();
const erros = [];
// Validar campos de texto individuais
Object.entries(CAMPOS).forEach(([id, cfg]) => {
const campo = document.getElementById(id);
if (!campo) return;
const erro = cfg.validar(campo.value);
exibirErroAcessivel(id, erro);
if (erro) erros.push({ campoId: id, mensagem: erro });
});
// Validar turno (radio)
const erroTurno = validarRadio('turno');
const erroTurnoEl = document.getElementById('turno-erro');
erroTurnoEl.textContent = erroTurno || '';
erroTurnoEl.hidden = !erroTurno;
if (erroTurno) erros.push({ campoId: 'turno', mensagem: erroTurno });
// Validar interesses (checkbox múltiplo — mínimo 2)
const erroInteresses = validarGrupoCheckbox('interesse', 2);
const erroInteressesEl = document.getElementById('interesses-erro');
erroInteressesEl.textContent = erroInteresses || '';
erroInteressesEl.hidden = !erroInteresses;
if (erroInteresses) erros.push({ campoId: 'interesse', mensagem: erroInteresses });
// Validar termos (checkbox único)
const checkTermos = document.getElementById('termos');
const erroTermos = validarCheckbox(checkTermos, 'Você deve aceitar os termos.');
exibirErroAcessivel('termos', erroTermos);
if (erroTermos) erros.push({ campoId: 'termos', mensagem: erroTermos });
// Validar foto (opcional, mas com restrições se preenchida)
const inputFoto = document.getElementById('foto');
const erroFoto = validarArquivo(inputFoto, {
tiposPermitidos: ['image/jpeg', 'image/png', 'image/webp'],
extensoesPermitidas: ['.jpg', '.jpeg', '.png', '.webp'],
tamanhoMaximoMB: 2
});
exibirErroAcessivel('foto', erroFoto);
if (erroFoto) erros.push({ campoId: 'foto', mensagem: erroFoto });
// Exibir resumo e focar primeiro erro
anunciarResumoErros(erros);
if (erros.length > 0) {
focarPrimeiroErro(form);
return;
}
// Formulário válido: coletar dados
const dados = coletarDados();
console.log('Dados válidos:', dados);
enviarFormulario(dados);
});
function coletarDados() {
const fd = new FormData(form);
return {
nome: fd.get('nome'),
email: fd.get('email'),
telefone: fd.get('telefone'),
nascimento: fd.get('nascimento'),
curso: fd.get('curso'),
turno: fd.get('turno'),
interesses: fd.getAll('interesse'),
mensagem: fd.get('mensagem'),
foto: fd.get('foto'),
};
}
async function enviarFormulario(dados) {
const btnEnviar = form.querySelector('[type="submit"]');
btnEnviar.disabled = true;
btnEnviar.textContent = 'Enviando...';
try {
// Simulação de envio — substituir por fetch real
await new Promise(resolve => setTimeout(resolve, 1500));
form.dispatchEvent(new CustomEvent('cadastro-enviado', {
detail: dados, bubbles: true
}));
exibirSucesso();
} catch (erro) {
console.error('Erro ao enviar:', erro);
exibirErroEnvio();
} finally {
btnEnviar.disabled = false;
btnEnviar.textContent = 'Enviar cadastro';
}
}
function exibirSucesso() {
form.innerHTML = `
<div class="sucesso" role="alert" tabindex="-1">
<span class="sucesso__icone" aria-hidden="true">✅</span>
<h2>Cadastro enviado com sucesso!</h2>
<p>Em breve você receberá um e-mail de confirmação.</p>
<button type="button" onclick="location.reload()" class="btn btn--primario">
Novo cadastro
</button>
</div>
`;
form.querySelector('.sucesso').focus();
}
function exibirErroEnvio() {
const aviso = document.querySelector('#resumo-erros');
aviso.hidden = false;
aviso.innerHTML = `
<p>Ocorreu um erro ao enviar. Tente novamente em alguns instantes.</p>
`;
aviso.focus();
}
15.5 — Componentes interativos com DOM e Eventos¶
Vídeo curto explicativo (link será adicionado posteriormente)
15.5.1 — Modal: abrir, fechar, foco e acessibilidade¶
Um modal acessível requer: gestão de foco (ao abrir, focar dentro; ao fechar, retornar ao gatilho), aprisionamento de foco dentro do modal enquanto aberto, fechar com Escape, e atributos ARIA corretos:
<!-- Botão que abre o modal -->
<button type="button" class="btn btn--primario" id="btn-abrir-modal"
aria-haspopup="dialog">
Abrir modal
</button>
<!-- Modal -->
<div class="modal" id="modal-confirmacao" role="dialog"
aria-modal="true" aria-labelledby="modal-titulo"
aria-describedby="modal-descricao" hidden>
<div class="modal__overlay" data-fechar-modal></div>
<div class="modal__caixa">
<header class="modal__cabecalho">
<h2 class="modal__titulo" id="modal-titulo">Confirmar ação</h2>
<button type="button" class="modal__fechar"
aria-label="Fechar modal" data-fechar-modal>
✕
</button>
</header>
<div class="modal__corpo">
<p id="modal-descricao">
Tem certeza que deseja continuar? Esta ação não pode ser desfeita.
</p>
</div>
<footer class="modal__rodape">
<button type="button" class="btn btn--secundario" data-fechar-modal>
Cancelar
</button>
<button type="button" class="btn btn--perigo" id="btn-confirmar-modal">
Confirmar
</button>
</footer>
</div>
</div>
class Modal {
constructor(modalId, gatilhoId) {
this.modal = document.getElementById(modalId);
this.gatilho = document.getElementById(gatilhoId);
this.elementoFocavel = null;
this.inicializar();
}
inicializar() {
// Abrir
this.gatilho?.addEventListener('click', () => this.abrir());
// Fechar
this.modal.addEventListener('click', (e) => {
if (e.target.dataset.fecharModal !== undefined) this.fechar();
});
// Fechar com Escape
this.modal.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.fechar();
if (e.key === 'Tab') this.gerenciarFoco(e);
});
}
abrir() {
this.elementoFocavel = document.activeElement;
this.modal.hidden = false;
document.body.style.overflow = 'hidden'; // previne scroll do fundo
// Focar no primeiro elemento focável do modal
const primeiroPodeFocar = this.obterElementosFocaveis()[0];
if (primeiroPodeFocar) {
requestAnimationFrame(() => primeiroPodeFocar.focus());
}
this.modal.dispatchEvent(new CustomEvent('modal-aberto', { bubbles: true }));
}
fechar() {
this.modal.hidden = true;
document.body.style.overflow = '';
// Retornar foco ao gatilho original
this.elementoFocavel?.focus();
this.modal.dispatchEvent(new CustomEvent('modal-fechado', { bubbles: true }));
}
gerenciarFoco(e) {
const focaveis = this.obterElementosFocaveis();
if (!focaveis.length) return;
const primeiro = focaveis[0];
const ultimo = focaveis[focaveis.length - 1];
if (e.shiftKey) {
if (document.activeElement === primeiro) {
e.preventDefault();
ultimo.focus();
}
} else {
if (document.activeElement === ultimo) {
e.preventDefault();
primeiro.focus();
}
}
}
obterElementosFocaveis() {
return [...this.modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)].filter(el => !el.disabled && !el.hidden);
}
}
const modalConfirmacao = new Modal('modal-confirmacao', 'btn-abrir-modal');
document.getElementById('btn-confirmar-modal').addEventListener('click', () => {
modalConfirmacao.fechar();
executarAcaoConfirmada();
});
15.5.2 — Dropdown: abrir, fechar ao clicar fora e navegação por teclado¶
<div class="dropdown" id="dropdown-usuario">
<button type="button" class="dropdown__gatilho btn"
aria-haspopup="true" aria-expanded="false"
aria-controls="menu-usuario" id="btn-dropdown-usuario">
Maria Silva ▾
</button>
<ul class="dropdown__menu" id="menu-usuario"
role="menu" aria-labelledby="btn-dropdown-usuario" hidden>
<li role="none">
<a class="dropdown__item" href="/perfil" role="menuitem">Meu perfil</a>
</li>
<li role="none">
<a class="dropdown__item" href="/configuracoes" role="menuitem">
Configurações
</a>
</li>
<li role="separator" aria-hidden="true"></li>
<li role="none">
<button class="dropdown__item" type="button" role="menuitem"
id="btn-sair">
Sair
</button>
</li>
</ul>
</div>
function inicializarDropdown(dropdownSelector) {
const dropdown = document.querySelector(dropdownSelector);
const gatilho = dropdown.querySelector('.dropdown__gatilho');
const menu = dropdown.querySelector('.dropdown__menu');
const itens = () => [...menu.querySelectorAll('[role="menuitem"]')];
function abrir() {
menu.hidden = false;
gatilho.setAttribute('aria-expanded', 'true');
const primeiroItem = itens()[0];
primeiroItem?.focus();
}
function fechar(retornarFoco = true) {
menu.hidden = true;
gatilho.setAttribute('aria-expanded', 'false');
if (retornarFoco) gatilho.focus();
}
function estaAberto() {
return !menu.hidden;
}
// Toggle ao clicar no gatilho
gatilho.addEventListener('click', () => {
estaAberto() ? fechar() : abrir();
});
// Fechar ao clicar fora
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target)) fechar(false);
});
// Navegação por teclado (padrão WAI-ARIA para menu)
dropdown.addEventListener('keydown', (e) => {
const lista = itens();
const ativo = document.activeElement;
const indice = lista.indexOf(ativo);
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (!estaAberto()) { abrir(); return; }
lista[Math.min(indice + 1, lista.length - 1)]?.focus();
break;
case 'ArrowUp':
e.preventDefault();
if (!estaAberto()) { abrir(); return; }
if (indice <= 0) { fechar(); return; }
lista[indice - 1]?.focus();
break;
case 'Home':
if (estaAberto()) { e.preventDefault(); lista[0]?.focus(); }
break;
case 'End':
if (estaAberto()) {
e.preventDefault();
lista[lista.length - 1]?.focus();
}
break;
case 'Escape':
fechar();
break;
case 'Tab':
fechar(false); // fecha sem roubar o foco do tab
break;
}
});
return { abrir, fechar };
}
inicializarDropdown('#dropdown-usuario');
15.5.3 — Accordion: expandir e colapsar seções¶
<div class="accordion" id="accordion-faq">
<div class="accordion__item">
<h3 class="accordion__titulo">
<button class="accordion__botao" type="button"
aria-expanded="false"
aria-controls="painel-1" id="botao-1">
<span>O que é HTML semântico?</span>
<span class="accordion__icone" aria-hidden="true">+</span>
</button>
</h3>
<div class="accordion__painel" id="painel-1"
role="region" aria-labelledby="botao-1" hidden>
<div class="accordion__conteudo">
<p>HTML semântico refere-se ao uso de elementos HTML que
transmitem significado sobre o conteúdo que envolvem...</p>
</div>
</div>
</div>
<div class="accordion__item">
<h3 class="accordion__titulo">
<button class="accordion__botao" type="button"
aria-expanded="false"
aria-controls="painel-2" id="botao-2">
<span>Qual a diferença entre Flexbox e Grid?</span>
<span class="accordion__icone" aria-hidden="true">+</span>
</button>
</h3>
<div class="accordion__painel" id="painel-2"
role="region" aria-labelledby="botao-2" hidden>
<div class="accordion__conteudo">
<p>Flexbox é unidimensional (linha ou coluna);
Grid é bidimensional (linhas e colunas)...</p>
</div>
</div>
</div>
</div>
function inicializarAccordion(selector, opcoes = {}) {
const accordion = document.querySelector(selector);
const { apenasUm = true } = opcoes; // true: fecha outros ao abrir um
accordion.addEventListener('click', (e) => {
const botao = e.target.closest('.accordion__botao');
if (!botao) return;
const expandido = botao.getAttribute('aria-expanded') === 'true';
const painelId = botao.getAttribute('aria-controls');
const painel = document.getElementById(painelId);
const icone = botao.querySelector('.accordion__icone');
if (apenasUm && !expandido) {
// Fecha todos os outros
accordion.querySelectorAll('.accordion__botao[aria-expanded="true"]')
.forEach(outroBotao => {
if (outroBotao !== botao) {
outroBotao.setAttribute('aria-expanded', 'false');
const outroIcone = outroBotao.querySelector('.accordion__icone');
if (outroIcone) outroIcone.textContent = '+';
const outroPainel = document.getElementById(
outroBotao.getAttribute('aria-controls')
);
if (outroPainel) outroPainel.hidden = true;
}
});
}
// Toggle do item clicado
const novoEstado = !expandido;
botao.setAttribute('aria-expanded', novoEstado);
painel.hidden = !novoEstado;
if (icone) icone.textContent = novoEstado ? '−' : '+';
});
// Navegação por teclado entre cabeçalhos
accordion.addEventListener('keydown', (e) => {
const botoes = [...accordion.querySelectorAll('.accordion__botao')];
const indice = botoes.indexOf(e.target);
if (indice === -1) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
botoes[Math.min(indice + 1, botoes.length - 1)].focus();
}
if (e.key === 'ArrowUp') {
e.preventDefault();
botoes[Math.max(indice - 1, 0)].focus();
}
if (e.key === 'Home') { e.preventDefault(); botoes[0].focus(); }
if (e.key === 'End') { e.preventDefault(); botoes[botoes.length - 1].focus(); }
});
}
inicializarAccordion('#accordion-faq', { apenasUm: true });
15.5.4 — Exercício prático: página de FAQ com accordion acessível¶
Combinar o accordion, o sistema de abas e a busca com teclado em uma página de FAQ completa:
// FAQ com busca integrada ao accordion
const termoBusca = document.querySelector('#busca-faq');
termoBusca?.addEventListener('input', () => {
const termo = termoBusca.value.trim().toLowerCase();
const itens = document.querySelectorAll('.accordion__item');
itens.forEach(item => {
const pergunta = item.querySelector('.accordion__botao span:first-child')
.textContent.toLowerCase();
const resposta = item.querySelector('.accordion__conteudo')
.textContent.toLowerCase();
const corresponde = !termo ||
pergunta.includes(termo) ||
resposta.includes(termo);
item.hidden = !corresponde;
// Expandir automaticamente se há termo de busca e corresponde
if (termo && corresponde) {
const botao = item.querySelector('.accordion__botao');
const painel = document.getElementById(botao.getAttribute('aria-controls'));
botao.setAttribute('aria-expanded', 'true');
painel.hidden = false;
}
});
// Anunciar resultados para leitores de tela
const visiveis = [...itens].filter(i => !i.hidden).length;
document.querySelector('#faq-contagem').textContent =
`${visiveis} pergunta${visiveis !== 1 ? 's' : ''} encontrada${visiveis !== 1 ? 's' : ''}`;
});
15.6 — Projeto integrador do capítulo¶
Vídeo curto explicativo (link será adicionado posteriormente)
15.6.1 — Especificação¶
O projeto integrador do Capítulo 15 consolida todos os conceitos em uma aplicação coesa: um formulário de matrícula com validação completa, modal de confirmação e feedback visual de sucesso.
Funcionalidades obrigatórias: - Validação em tempo real de todos os campos (texto, email, senha, select, radio, checkbox, textarea) - Indicador de força de senha - Modal de confirmação antes de enviar - Feedback visual de sucesso após envio - Acessibilidade: navegação por teclado, ARIA, foco gerenciado
Estrutura sugerida:
projeto-cap15/
├── index.html ← formulário completo
├── css/
│ ├── style.css ← estilos do formulário e componentes
│ └── modal.css ← estilos do modal
└── js/
├── validacao.js ← funções de validação (reutilizáveis)
├── componentes.js ← Modal, Dropdown, Accordion
└── app.js ← lógica principal e inicialização
15.6.2 — Extensões sugeridas para prática autônoma¶
Após concluir o projeto base, experimente estender com:
- Salvamento automático em
localStorage: preservar os dados do formulário ao fechar acidentalmente a página e restaurá-los ao reabrir - Validação assíncrona: simular verificação de e-mail já cadastrado com
setTimeoute exibir estado de "verificando..." - Multi-step form: dividir o formulário em etapas com barra de progresso e navegação entre passos
- Temas claro/escuro: toggle de tema com persistência em
localStorage
15.7 — Laboratório de jogos: aprofundando com DOM e Eventos¶
Vídeo curto explicativo (link será adicionado posteriormente)
Conteúdo avançado e opcional. Esta seção é uma extensão do laboratório de jogos iniciado no Capítulo 14. Os conceitos aqui introduzidos — física com gravidade, detecção de colisão AABB e gerenciamento de estados de jogo — são pré-requisitos comuns a praticamente todo jogo 2D e servem de base para frameworks como Phaser.
15.7.1 — Revisão: o que temos e o que falta¶
No Capítulo 14 construímos três jogos que demonstraram:
| Jogo | Conceitos demonstrados |
|---|---|
| Adivinhe o número | Lógica, DOM, estado simples |
| Clique no alvo | Posicionamento dinâmico, timer, níveis |
| Snake | Canvas, game loop, colisão por grade |
O que ainda não abordamos — e que abre o universo de jogos 2D mais ricos:
- Física: objetos que se movem com velocidade, aceleração e gravidade
- Colisão AABB: detecção precisa de sobreposição entre retângulos em posições contínuas
- Estados de jogo: máquina de estados para gerenciar telas (menu → jogo → pausa → game over)
15.7.2 — Física básica: gravidade, velocidade e aceleração¶
No Snake, cada segmento se movia por uma grade discreta. Em jogos de plataforma, os objetos se movem em espaço contínuo com física simulada:
// Modelo de entidade física
function criarEntidade(x, y, largura, altura) {
return {
// Posição
x, y,
// Dimensões
largura, altura,
// Velocidade (pixels por segundo)
vx: 0,
vy: 0,
// Flags de estado
noChao: false,
};
}
// Constantes de física
const FISICA = {
GRAVIDADE: 1800, // pixels/s² — aceleração para baixo
FORCA_PULO: -600, // pixels/s — velocidade inicial do pulo (negativo = cima)
VELOCIDADE_MAX_QUEDA: 800, // pixels/s — velocidade terminal
FRICCAO: 0.85, // fator multiplicativo — desacelera movimento horizontal
};
// Atualizar física de uma entidade
function atualizarFisica(entidade, deltaTime) {
const dt = deltaTime / 1000; // converter ms para segundos
// Aplicar gravidade (acelera a queda)
entidade.vy += FISICA.GRAVIDADE * dt;
// Limitar velocidade de queda
entidade.vy = Math.min(entidade.vy, FISICA.VELOCIDADE_MAX_QUEDA);
// Atualizar posição com base na velocidade
entidade.x += entidade.vx * dt;
entidade.y += entidade.vy * dt;
// Aplicar fricção horizontal (desacelera quando não há input)
entidade.vx *= FISICA.FRICCAO;
// Zerear velocidade mínima (evita deslizamento infinito)
if (Math.abs(entidade.vx) < 0.5) entidade.vx = 0;
}
// Pulo — só permite se estiver no chão
function pular(entidade) {
if (!entidade.noChao) return;
entidade.vy = FISICA.FORCA_PULO;
entidade.noChao = false;
}
15.7.3 — Detecção de colisão AABB¶
AABB (Axis-Aligned Bounding Box) é o método mais simples e eficiente de detecção de colisão entre retângulos alinhados aos eixos (sem rotação). Dois retângulos se sobrepõem se e somente se há sobreposição em ambos os eixos simultaneamente:
// Verificar se dois retângulos se sobrepõem
function colidem(a, b) {
return (
a.x < b.x + b.largura &&
a.x + a.largura > b.x &&
a.y < b.y + b.altura &&
a.y + a.altura > b.y
);
}
// Resolver colisão com plataforma — empurra o objeto para fora
// e determina de qual direção a colisão ocorreu
function resolverColisaoPlataforma(jogador, plataforma) {
if (!colidem(jogador, plataforma)) return false;
// Calcular profundidade de sobreposição em cada eixo
const overlapX = Math.min(
jogador.x + jogador.largura - plataforma.x,
plataforma.x + plataforma.largura - jogador.x
);
const overlapY = Math.min(
jogador.y + jogador.altura - plataforma.y,
plataforma.y + plataforma.altura - jogador.y
);
// Resolver pelo eixo de menor sobreposição
if (overlapX < overlapY) {
// Colisão lateral
if (jogador.x < plataforma.x) {
jogador.x = plataforma.x - jogador.largura; // empurra para esquerda
} else {
jogador.x = plataforma.x + plataforma.largura; // empurra para direita
}
jogador.vx = 0;
} else {
// Colisão vertical
if (jogador.y < plataforma.y) {
// Veio de cima — pousa no chão
jogador.y = plataforma.y - jogador.altura;
jogador.vy = 0;
jogador.noChao = true;
} else {
// Veio de baixo — bate na cabeça
jogador.y = plataforma.y + plataforma.altura;
jogador.vy = 0;
}
}
return true;
}
15.7.4 — Máquina de estados de jogo¶
Jogos com múltiplas telas (menu, jogo, pausa, game over) se beneficiam de uma máquina de estados explícita — evitando condicionais aninhados e tornando as transições previsíveis:
// Definição dos estados possíveis
const ESTADOS = {
MENU: 'menu',
JOGANDO: 'jogando',
PAUSADO: 'pausado',
GAME_OVER: 'game_over',
VITORIA: 'vitoria',
};
// Máquina de estados
const maquinaEstados = {
atual: ESTADOS.MENU,
// Transições válidas
transicoes: {
[ESTADOS.MENU]: [ESTADOS.JOGANDO],
[ESTADOS.JOGANDO]: [ESTADOS.PAUSADO, ESTADOS.GAME_OVER, ESTADOS.VITORIA],
[ESTADOS.PAUSADO]: [ESTADOS.JOGANDO, ESTADOS.MENU],
[ESTADOS.GAME_OVER]: [ESTADOS.MENU, ESTADOS.JOGANDO],
[ESTADOS.VITORIA]: [ESTADOS.MENU],
},
// Callbacks executados ao entrar em cada estado
aoEntrar: {
[ESTADOS.MENU]: () => exibirTela('tela-menu'),
[ESTADOS.JOGANDO]: () => { ocultarTodasTelas(); resumirLoop(); },
[ESTADOS.PAUSADO]: () => { pausarLoop(); exibirTela('tela-pausa'); },
[ESTADOS.GAME_OVER]: () => { pausarLoop(); exibirTela('tela-game-over'); },
[ESTADOS.VITORIA]: () => { pausarLoop(); exibirTela('tela-vitoria'); },
},
// Tentar transição para novo estado
ir(novoEstado) {
const transacoesValidas = this.transicoes[this.atual] || [];
if (!transacoesValidas.includes(novoEstado)) {
console.warn(`Transição inválida: ${this.atual} → ${novoEstado}`);
return false;
}
this.atual = novoEstado;
this.aoEntrar[novoEstado]?.();
return true;
},
eh(estado) { return this.atual === estado; },
};
// Uso
maquinaEstados.ir(ESTADOS.JOGANDO); // menu → jogando ✓
maquinaEstados.ir(ESTADOS.PAUSADO); // jogando → pausado ✓
maquinaEstados.ir(ESTADOS.VITORIA); // pausado → vitoria ✗ (inválido)
15.7.5 — Jogo 4: Plataformer simples¶
Integrando física, colisão AABB e máquina de estados em um plataformer completo:
Imagem sugerida: captura do jogo em execução mostrando o personagem (quadrado verde com olhos) sobre plataformas coloridas, moedas para coletar, HUD com pontuação e timer, e a tela do nível com fundo degradê.
(imagem será adicionada posteriormente)
<div class="jogo-plataformer">
<div class="hud">
<span>⭐ <strong id="pf-pontos">0</strong></span>
<span>❤️ <strong id="pf-vidas">3</strong></span>
<span>⏱ <strong id="pf-tempo">60</strong></span>
<span>Nível <strong id="pf-nivel">1</strong></span>
</div>
<canvas id="canvas-plataformer" width="600" height="400"
tabindex="0"
aria-label="Jogo de plataforma — use setas para mover e espaço para pular">
</canvas>
<!-- Telas de estado -->
<div class="pf-tela" id="tela-menu">
<h2>🎮 Plataformer</h2>
<p>Colete todas as moedas antes do tempo acabar!</p>
<p class="controles">
← → Mover | Espaço ou ↑ Pular
</p>
<button type="button" id="pf-btn-iniciar">Iniciar</button>
</div>
<div class="pf-tela oculto" id="tela-pausa">
<h2>⏸ Pausado</h2>
<button type="button" id="pf-btn-retomar">Retomar</button>
<button type="button" id="pf-btn-menu-pausa">Menu</button>
</div>
<div class="pf-tela oculto" id="tela-game-over">
<h2>💀 Game Over</h2>
<p>Pontuação: <strong id="pf-pontuacao-final">0</strong></p>
<button type="button" id="pf-btn-reiniciar">Tentar novamente</button>
</div>
<div class="pf-tela oculto" id="tela-vitoria">
<h2>🏆 Você venceu!</h2>
<p>Pontuação: <strong id="pf-pontuacao-vitoria">0</strong></p>
<button type="button" id="pf-btn-proximo">Próximo nível</button>
</div>
</div>
// ── Configuração ────────────────────────────────────────────
const canvas = document.querySelector('#canvas-plataformer');
const ctx = canvas.getContext('2d');
const LARGURA = canvas.width;
const ALTURA = canvas.height;
// ── Estado global ───────────────────────────────────────────
let jogador, moedas, plataformas, particulas;
let pontos, vidas, tempo, nivel;
let loopId, timerInterval, ultimoTimestamp;
const teclas = new Set();
// ── Definição dos níveis ────────────────────────────────────
const NIVEIS_CONFIG = [
{
cor: '#1a1a2e',
plataformas: [
{ x: 0, y: 360, largura: 600, altura: 40 }, // chão
{ x: 80, y: 280, largura: 120, altura: 16 },
{ x: 280, y: 220, largura: 140, altura: 16 },
{ x: 460, y: 160, largura: 100, altura: 16 },
{ x: 160, y: 140, largura: 100, altura: 16 },
],
moedas: [
{ x: 120, y: 250 }, { x: 160, y: 250 },
{ x: 320, y: 190 }, { x: 360, y: 190 },
{ x: 490, y: 130 }, { x: 190, y: 110 },
{ x: 530, y: 130 },
],
posicaoInicial: { x: 30, y: 300 },
tempoLimite: 60,
},
{
cor: '#0d1b2a',
plataformas: [
{ x: 0, y: 360, largura: 600, altura: 40 },
{ x: 50, y: 300, largura: 80, altura: 16 },
{ x: 200, y: 250, largura: 80, altura: 16 },
{ x: 350, y: 200, largura: 80, altura: 16 },
{ x: 480, y: 150, largura: 80, altura: 16 },
{ x: 300, y: 120, largura: 80, altura: 16 },
{ x: 100, y: 160, largura: 80, altura: 16 },
],
moedas: [
{ x: 70, y: 270 }, { x: 220, y: 220 },
{ x: 370, y: 170 }, { x: 500, y: 120 },
{ x: 320, y: 90 }, { x: 120, y: 130 },
{ x: 150, y: 130 },
],
posicaoInicial: { x: 30, y: 310 },
tempoLimite: 50,
},
];
// ── Inicialização ───────────────────────────────────────────
function inicializarNivel(n) {
const cfg = NIVEIS_CONFIG[(n - 1) % NIVEIS_CONFIG.length];
jogador = criarEntidade(
cfg.posicaoInicial.x,
cfg.posicaoInicial.y,
28, 36
);
plataformas = cfg.plataformas.map(p => ({ ...p }));
moedas = cfg.moedas.map((m, i) => ({
id: i,
x: m.x,
y: m.y,
raio: 10,
coletada: false,
angulo: Math.random() * Math.PI * 2, // para animação
}));
particulas = [];
tempo = cfg.tempoLimite;
ultimoTimestamp = 0;
document.getElementById('pf-nivel').textContent = n;
document.getElementById('pf-tempo').textContent = tempo;
}
// ── Game Loop ───────────────────────────────────────────────
function gameLoop(timestamp) {
if (!estado.eh(ESTADOS.JOGANDO)) return;
const dt = ultimoTimestamp ? Math.min(timestamp - ultimoTimestamp, 50) : 16;
ultimoTimestamp = timestamp;
procesarInput();
atualizarFisica(jogador, dt);
resolverColisoes();
verificarMoedas();
atualizarParticulas(dt);
verificarCondicoes();
desenhar(timestamp);
loopId = requestAnimationFrame(gameLoop);
}
function pausarLoop() { cancelAnimationFrame(loopId); }
function resumirLoop() { ultimoTimestamp = 0; loopId = requestAnimationFrame(gameLoop); }
// ── Input ───────────────────────────────────────────────────
canvas.addEventListener('keydown', (e) => {
teclas.add(e.code);
e.preventDefault();
if (e.code === 'KeyP' || e.code === 'Escape') {
if (estado.eh(ESTADOS.JOGANDO)) estado.ir(ESTADOS.PAUSADO);
else if (estado.eh(ESTADOS.PAUSADO)) estado.ir(ESTADOS.JOGANDO);
}
});
canvas.addEventListener('keyup', (e) => teclas.delete(e.code));
function procesarInput() {
const VELOCIDADE_HORIZONTAL = 200;
if (teclas.has('ArrowLeft') || teclas.has('KeyA')) {
jogador.vx = -VELOCIDADE_HORIZONTAL;
}
if (teclas.has('ArrowRight') || teclas.has('KeyD')) {
jogador.vx = VELOCIDADE_HORIZONTAL;
}
if (
(teclas.has('ArrowUp') || teclas.has('KeyW') || teclas.has('Space')) &&
jogador.noChao
) {
pular(jogador);
criarParticulasPulo();
}
}
// ── Colisão ─────────────────────────────────────────────────
function resolverColisoes() {
jogador.noChao = false;
plataformas.forEach(p => resolverColisaoPlataforma(jogador, p));
// Limites laterais do canvas
if (jogador.x < 0) { jogador.x = 0; jogador.vx = 0; }
if (jogador.x + jogador.largura > LARGURA) {
jogador.x = LARGURA - jogador.largura;
jogador.vx = 0;
}
// Cair fora do canvas = perder vida
if (jogador.y > ALTURA + 50) {
perderVida();
}
}
function verificarMoedas() {
moedas.forEach(moeda => {
if (moeda.coletada) return;
// Colisão círculo com retângulo (simplificada para quadrado)
const dx = (jogador.x + jogador.largura / 2) - moeda.x;
const dy = (jogador.y + jogador.altura / 2) - moeda.y;
const distancia = Math.sqrt(dx * dx + dy * dy);
if (distancia < moeda.raio + 18) {
moeda.coletada = true;
pontos += 10;
criarParticulasMoeda(moeda.x, moeda.y);
document.getElementById('pf-pontos').textContent = pontos;
}
});
}
function verificarCondicoes() {
// Vitória: todas as moedas coletadas
if (moedas.every(m => m.coletada)) {
pontos += tempo * 5; // bônus de tempo
document.getElementById('pf-pontuacao-vitoria').textContent = pontos;
estado.ir(ESTADOS.VITORIA);
}
}
// ── Partículas ──────────────────────────────────────────────
function criarParticulasMoeda(x, y) {
for (let i = 0; i < 8; i++) {
const angulo = (i / 8) * Math.PI * 2;
particulas.push({
x, y,
vx: Math.cos(angulo) * (80 + Math.random() * 80),
vy: Math.sin(angulo) * (80 + Math.random() * 80),
vida: 1,
cor: `hsl(${45 + Math.random() * 30}, 100%, 60%)`,
raio: 3 + Math.random() * 3,
});
}
}
function criarParticulasPulo() {
for (let i = 0; i < 5; i++) {
particulas.push({
x: jogador.x + jogador.largura / 2,
y: jogador.y + jogador.altura,
vx: (Math.random() - 0.5) * 100,
vy: 50 + Math.random() * 100,
vida: 1,
cor: 'rgba(255,255,255,0.6)',
raio: 3,
});
}
}
function atualizarParticulas(dt) {
const dt_s = dt / 1000;
particulas = particulas.filter(p => p.vida > 0);
particulas.forEach(p => {
p.x += p.vx * dt_s;
p.y += p.vy * dt_s;
p.vy += 400 * dt_s; // gravidade nas partículas
p.vida -= 2 * dt_s;
});
}
function perderVida() {
vidas--;
document.getElementById('pf-vidas').textContent = vidas;
if (vidas <= 0) {
document.getElementById('pf-pontuacao-final').textContent = pontos;
estado.ir(ESTADOS.GAME_OVER);
return;
}
// Reposicionar jogador
const cfg = NIVEIS_CONFIG[(nivel - 1) % NIVEIS_CONFIG.length];
jogador.x = cfg.posicaoInicial.x;
jogador.y = cfg.posicaoInicial.y;
jogador.vx = 0;
jogador.vy = 0;
}
// ── Renderização ────────────────────────────────────────────
function desenhar(timestamp) {
const cfg = NIVEIS_CONFIG[(nivel - 1) % NIVEIS_CONFIG.length];
// Fundo com gradiente
const grad = ctx.createLinearGradient(0, 0, 0, ALTURA);
grad.addColorStop(0, cfg.cor);
grad.addColorStop(1, '#0a0a1a');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, LARGURA, ALTURA);
// Plataformas
plataformas.forEach((p, i) => {
// Gradiente para cada plataforma
const pGrad = ctx.createLinearGradient(p.x, p.y, p.x, p.y + p.altura);
pGrad.addColorStop(0, i === 0 ? '#2d5a27' : '#4a7c59');
pGrad.addColorStop(1, i === 0 ? '#1a3a15' : '#2d4f38');
ctx.fillStyle = pGrad;
ctx.beginPath();
ctx.roundRect(p.x, p.y, p.largura, p.altura, 4);
ctx.fill();
// Brilho no topo
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(p.x + 2, p.y + 2, p.largura - 4, 3);
});
// Moedas (animadas)
moedas.forEach(moeda => {
if (moeda.coletada) return;
moeda.angulo += 0.05;
// Sombra
ctx.beginPath();
ctx.ellipse(moeda.x, moeda.y + 12, moeda.raio * 0.8, 4, 0, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fill();
// Efeito de achatamento (simula rotação 3D)
const escalaX = Math.abs(Math.cos(moeda.angulo));
ctx.save();
ctx.translate(moeda.x, moeda.y);
ctx.scale(escalaX + 0.1, 1);
ctx.beginPath();
ctx.arc(0, 0, moeda.raio, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${45 + Math.sin(moeda.angulo * 2) * 10}, 100%, 55%)`;
ctx.fill();
// Brilho
ctx.beginPath();
ctx.arc(-2, -3, 3, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.fill();
ctx.restore();
});
// Partículas
particulas.forEach(p => {
ctx.globalAlpha = Math.max(0, p.vida);
ctx.beginPath();
ctx.arc(p.x, p.y, p.raio * p.vida, 0, Math.PI * 2);
ctx.fillStyle = p.cor;
ctx.fill();
});
ctx.globalAlpha = 1;
// Jogador
const jx = jogador.x, jy = jogador.y;
const jl = jogador.largura, ja = jogador.altura;
// Sombra do jogador
ctx.beginPath();
ctx.ellipse(jx + jl / 2, jy + ja + 4, jl * 0.4, 5, 0, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fill();
// Corpo
const corCorpo = jogador.noChao ? '#4CAF50' : '#66BB6A';
const jGrad = ctx.createLinearGradient(jx, jy, jx + jl, jy + ja);
jGrad.addColorStop(0, corCorpo);
jGrad.addColorStop(1, '#2E7D32');
ctx.fillStyle = jGrad;
ctx.beginPath();
ctx.roundRect(jx, jy, jl, ja, 6);
ctx.fill();
// Olhos — apontam para a direção do movimento
const olhoY = jy + ja * 0.3;
const olhoRaio = 4;
const pupillaOffset = jogador.vx > 0 ? 1.5 : jogador.vx < 0 ? -1.5 : 0;
[jx + jl * 0.3, jx + jl * 0.7].forEach(ox => {
ctx.beginPath();
ctx.arc(ox, olhoY, olhoRaio, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.fill();
ctx.beginPath();
ctx.arc(ox + pupillaOffset, olhoY + (jogador.noChao ? 0 : 1), 2, 0, Math.PI * 2);
ctx.fillStyle = '#1a1a1a';
ctx.fill();
});
}
// ── Telas de estado (implementação simplificada) ─────────────
function exibirTela(id) {
document.querySelectorAll('.pf-tela').forEach(t => t.classList.add('oculto'));
document.getElementById(id)?.classList.remove('oculto');
}
function ocultarTodasTelas() {
document.querySelectorAll('.pf-tela').forEach(t => t.classList.add('oculto'));
}
// ── Event listeners de UI ────────────────────────────────────
document.getElementById('pf-btn-iniciar').addEventListener('click', () => {
pontos = 0;
vidas = 3;
nivel = 1;
document.getElementById('pf-pontos').textContent = 0;
document.getElementById('pf-vidas').textContent = 3;
inicializarNivel(nivel);
estado.ir(ESTADOS.JOGANDO);
canvas.focus();
// Timer
clearInterval(timerInterval);
timerInterval = setInterval(() => {
if (!estado.eh(ESTADOS.JOGANDO)) return;
tempo--;
document.getElementById('pf-tempo').textContent = tempo;
if (tempo <= 0) {
clearInterval(timerInterval);
document.getElementById('pf-pontuacao-final').textContent = pontos;
estado.ir(ESTADOS.GAME_OVER);
}
}, 1000);
});
document.getElementById('pf-btn-retomar')?.addEventListener('click', () => {
estado.ir(ESTADOS.JOGANDO);
canvas.focus();
});
document.getElementById('pf-btn-menu-pausa')?.addEventListener('click', () => {
clearInterval(timerInterval);
estado.ir(ESTADOS.MENU);
});
document.getElementById('pf-btn-reiniciar')?.addEventListener('click', () => {
document.getElementById('pf-btn-iniciar').click();
});
document.getElementById('pf-btn-proximo')?.addEventListener('click', () => {
nivel++;
if (nivel > NIVEIS_CONFIG.length) {
nivel = 1; // reinicia os níveis
}
inicializarNivel(nivel);
estado.ir(ESTADOS.JOGANDO);
canvas.focus();
clearInterval(timerInterval);
timerInterval = setInterval(() => {
if (!estado.eh(ESTADOS.JOGANDO)) return;
tempo--;
document.getElementById('pf-tempo').textContent = tempo;
if (tempo <= 0) {
clearInterval(timerInterval);
document.getElementById('pf-pontuacao-final').textContent = pontos;
estado.ir(ESTADOS.GAME_OVER);
}
}, 1000);
});
// ── Inicializar o jogo ───────────────────────────────────────
// Desenhar tela de título antes de qualquer interação
(function desenharTelaInicial() {
const grad = ctx.createLinearGradient(0, 0, 0, ALTURA);
grad.addColorStop(0, '#1a1a2e');
grad.addColorStop(1, '#0a0a1a');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, LARGURA, ALTURA);
ctx.fillStyle = 'rgba(255,255,255,0.05)';
for (let i = 0; i < 50; i++) {
ctx.beginPath();
ctx.arc(
Math.random() * LARGURA,
Math.random() * ALTURA,
Math.random() * 2,
0, Math.PI * 2
);
ctx.fill();
}
})();
15.7.6 — Persistência de recordes com localStorage¶
// Sistema de recordes por nível
const Recordes = {
obter(nivel) {
const dados = JSON.parse(localStorage.getItem('pf-recordes') || '{}');
return dados[nivel] || 0;
},
salvar(nivel, pontuacao) {
const dados = JSON.parse(localStorage.getItem('pf-recordes') || '{}');
if (pontuacao > (dados[nivel] || 0)) {
dados[nivel] = pontuacao;
localStorage.setItem('pf-recordes', JSON.stringify(dados));
return true; // novo recorde
}
return false;
},
listar() {
return JSON.parse(localStorage.getItem('pf-recordes') || '{}');
},
limpar() {
localStorage.removeItem('pf-recordes');
}
};
// Integração com a tela de game over e vitória
function verificarRecorde(pontuacaoFinal) {
const novoRecorde = Recordes.salvar(nivel, pontuacaoFinal);
const recordeAtual = Recordes.obter(nivel);
if (novoRecorde) {
return `🏆 Novo recorde! ${recordeAtual} pontos`;
}
return `Recorde do nível ${nivel}: ${recordeAtual} pontos`;
}
15.7.7 — Desafios de extensão¶
Para praticar de forma autônoma, experimente estender o plataformer com:
- Inimigos patrulheiros: um inimigo que se move entre dois pontos e reinicia o nível ao tocar o jogador
- Plataformas móveis: plataformas que se movem horizontalmente, alterando a dificuldade
- Power-ups: itens especiais que dobram os pontos por 10 segundos ou tornam o jogador invulnerável
- Efeitos sonoros: usar a Web Audio API para gerar sons proceduralmente (sem arquivos externos) ao pular e coletar moedas
- Parallax scrolling: fundo com camadas que se movem em velocidades diferentes para criar profundidade
Referências: - MDN — Pointer Events - MDN — HTML Drag and Drop API - MDN — Constraint Validation - W3C — WAI-ARIA Authoring Practices — Dialog Modal - W3C — WAI-ARIA Authoring Practices — Tabs - W3C — WAI-ARIA Authoring Practices — Accordion
Atividades — Capítulo 15¶
1. Qual é a ordem correta das fases de propagação de eventos no DOM?
2. Por que é preferível usar e.code em vez de e.key para atalhos de teclado em jogos?
3. O que é detecção de colisão AABB e por que ela é adequada para jogos 2D simples?
-
GitHub Classroom — Projeto principal: Implementar o formulário de cadastro completo da seção 15.6 com: validação de todos os tipos de componentes (text, email, password, tel, date, select, radio, checkbox, textarea, file), mensagens de erro acessíveis com ARIA, modal de confirmação com gestão de foco e feedback visual de sucesso. (link será adicionado)
-
GitHub Classroom — Desafio de jogos (opcional): Estender o plataformer da seção 15.7 adicionando pelo menos dois dos seguintes: inimigos patrulheiros, plataformas móveis, power-ups ou efeitos sonoros com Web Audio API. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 14 — Manipulação do DOM :material-arrow-right: Ir ao Capítulo 16 — Consumo de APIs
Capítulo 16 — Consumo de APIs¶
Vídeo curto explicativo (link será adicionado posteriormente)
16.1 — O que é uma API REST¶
Vídeo curto explicativo (link será adicionado posteriormente)
Uma API (Application Programming Interface — Interface de Programação de Aplicações) é um contrato que define como dois sistemas de software podem se comunicar. No contexto web, uma API é tipicamente um serviço acessível via HTTP que recebe requisições, processa dados e retorna respostas estruturadas — geralmente em formato JSON.
A analogia mais precisa é a de um garçom em um restaurante: o cliente (frontend) não vai até a cozinha (banco de dados) diretamente. Ele comunica seu pedido ao garçom (API), que o transmite à cozinha, aguarda o preparo e traz o resultado ao cliente. O cliente não precisa saber como a cozinha funciona — apenas como se comunicar com o garçom.
16.1.1 — Conceito de API e o modelo cliente-servidor revisitado¶
O modelo cliente-servidor, introduzido no Capítulo 1, ganha uma nova dimensão quando consideramos APIs:
FRONTEND (cliente) API (servidor) BANCO DE DADOS
┌──────────────────┐ ┌──────────────┐ ┌─────────────┐
│ HTML + CSS + JS │ HTTP │ Node.js │ SQL │ PostgreSQL │
│ │ ──────▶│ Python │ ───────▶│ MongoDB │
│ Faz requisições │◀────── │ PHP │◀─────── │ MySQL │
│ Exibe dados │ JSON │ Java │ JSON │ │
└──────────────────┘ └──────────────┘ └─────────────┘
O frontend nunca acessa o banco de dados diretamente — ele sempre passa pela API. Isso garante segurança (credenciais do banco ficam no servidor), controle (a API valida e autoriza cada operação) e independência (o frontend não precisa saber qual banco de dados é usado).
16.1.2 — REST: princípios e convenções¶
REST (Representational State Transfer) é um estilo arquitetural para APIs web definido por Roy Fielding em sua dissertação de doutorado em 2000. Não é um protocolo ou padrão formal — é um conjunto de princípios que, quando seguidos, produzem APIs previsíveis, escaláveis e fáceis de consumir.
Os princípios REST mais relevantes para o frontend:
Interface uniforme: recursos são identificados por URLs. A mesma URL com métodos HTTP diferentes realiza operações distintas sobre o mesmo recurso.
Stateless (sem estado): cada requisição contém todas as informações necessárias para ser processada. O servidor não mantém estado entre requisições — autenticação, contexto e dados de sessão são enviados em cada requisição.
Recursos e representações: tudo é um recurso (usuário, produto, pedido) identificado por uma URL. A representação do recurso (o dado retornado) pode variar conforme o Accept header — JSON, XML, HTML.
16.1.3 — Métodos HTTP: GET, POST, PUT, PATCH, DELETE¶
Os métodos HTTP expressam a intenção da operação sobre um recurso:
| Método | Operação | Idempotente | Corpo |
|---|---|---|---|
GET |
Ler/listar | ✅ Sim | ❌ Não |
POST |
Criar | ❌ Não | ✅ Sim |
PUT |
Substituir (completo) | ✅ Sim | ✅ Sim |
PATCH |
Atualizar (parcial) | ✅ Sim* | ✅ Sim |
DELETE |
Remover | ✅ Sim | Opcional |
Idempotente significa que repetir a operação múltiplas vezes produz o mesmo resultado que executá-la uma única vez. GET /produtos/1 retorna sempre o mesmo produto; DELETE /produtos/1 aplicado duas vezes tem o mesmo efeito que uma vez (o produto já foi removido). POST /produtos cria um novo produto a cada chamada — não é idempotente.
Convenções de URL em APIs REST:
GET /produtos → lista todos os produtos
GET /produtos/42 → retorna o produto com ID 42
POST /produtos → cria um novo produto
PUT /produtos/42 → substitui completamente o produto 42
PATCH /produtos/42 → atualiza campos específicos do produto 42
DELETE /produtos/42 → remove o produto 42
GET /usuarios/7/pedidos → pedidos do usuário 7
GET /usuarios/7/pedidos/3 → pedido 3 do usuário 7
POST /usuarios/7/pedidos → cria pedido para o usuário 7
GET /produtos?categoria=eletronicos&preco_max=500
→ filtra por query string
16.1.4 — Códigos de status HTTP¶
O código de status na resposta HTTP comunica o resultado da operação:
| Faixa | Categoria | Exemplos mais comuns |
|---|---|---|
| 2xx | Sucesso | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirecionamento | 301 Moved Permanently, 304 Not Modified |
| 4xx | Erro do cliente | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Entity |
| 5xx | Erro do servidor | 500 Internal Server Error, 503 Service Unavailable |
Os mais relevantes para o frontend:
// 200 OK — requisição bem-sucedida com corpo de resposta
// 201 Created — recurso criado com sucesso (resposta a POST)
// 204 No Content — sucesso sem corpo (resposta comum a DELETE)
// 400 Bad Request — dados enviados são inválidos
// 401 Unauthorized — autenticação necessária ou inválida
// 403 Forbidden — autenticado mas sem permissão
// 404 Not Found — recurso não existe
// 422 Unprocessable Entity — dados válidos sintaticamente mas inválidos semanticamente
// 429 Too Many Requests — limite de requisições excedido
// 500 Internal Server Error — erro inesperado no servidor
16.1.5 — Endpoints, recursos e parâmetros de URL¶
Uma URL de API é composta por partes com semânticas distintas:
https://api.exemplo.com/v1/produtos/42?campos=nome,preco&formato=resumido
│─────────────────────│ │─────────│ │─│ │──────────────────────────────│
Base URL Recurso ID Query parameters
(host + versão) (coleção) (item) (filtros e opções)
Path parameters identificam recursos específicos:
/usuarios/{id} → /usuarios/42
/produtos/{id}/avaliacoes/{avId} → /produtos/15/avaliacoes/3
Query parameters filtram, ordenam e paginam:
/produtos?categoria=livros → filtro
/produtos?ordem=preco&direcao=asc → ordenação
/produtos?pagina=2&limite=20 → paginação
/produtos?busca=javascript → busca textual
/produtos?categoria=livros&preco_max=100&pagina=1&limite=10 → combinado
16.2 — JSON: estrutura e manipulação¶
Vídeo curto explicativo (link será adicionado posteriormente)
16.2.1 — O que é JSON e por que é o formato padrão de APIs¶
JSON (JavaScript Object Notation) é um formato de texto para serialização de dados estruturados. Criado por Douglas Crockford nos anos 2000 a partir da sintaxe de objetos do JavaScript, tornou-se o formato padrão para troca de dados em APIs web por três razões principais: é legível por humanos, é facilmente parseável por máquinas, e é nativo ao JavaScript (sem necessidade de bibliotecas externas).
{
"id": 42,
"nome": "Maria Silva",
"email": "maria@exemplo.com",
"ativo": true,
"saldo": 1250.75,
"tags": ["estudante", "frontend"],
"endereco": {
"rua": "Av. Lourival Melo Mota",
"numero": "s/n",
"cidade": "Maceió",
"estado": "AL",
"cep": "57072-970"
},
"ultimoAcesso": "2026-03-15T14:30:00Z",
"preferencias": null
}
Tipos de dados válidos em JSON:
| Tipo JSON | Exemplo | Equivalente JS |
|---|---|---|
| string | "texto" |
string |
| number | 42, 3.14 |
number |
| boolean | true, false |
boolean |
| null | null |
null |
| array | [1, 2, 3] |
Array |
| object | {"chave": "valor"} |
Object |
O que JSON NÃO suporta: undefined, funções, Date (datas são strings), Symbol, BigInt, referências circulares.
16.2.2 — JSON.parse() e JSON.stringify()¶
// JSON.parse() — converte string JSON em objeto JavaScript
const jsonString = '{"nome":"Ana","idade":22,"ativo":true}';
const objeto = JSON.parse(jsonString);
console.log(objeto.nome); // → "Ana"
console.log(objeto.idade); // → 22
console.log(typeof objeto.idade); // → "number"
// JSON.stringify() — converte objeto JavaScript em string JSON
const usuario = {
nome: 'Bruno',
idade: 25,
hobbies: ['leitura', 'programação'],
senha: undefined, // undefined é omitido
saldo: 1500.50
};
const json = JSON.stringify(usuario);
// → '{"nome":"Bruno","idade":25,"hobbies":["leitura","programação"],"saldo":1500.5}'
// Indentação para legibilidade (útil para debug)
const jsonFormatado = JSON.stringify(usuario, null, 2);
/*
{
"nome": "Bruno",
"idade": 25,
...
}
*/
// Replacer: filtrar ou transformar propriedades
const jsonSemSenha = JSON.stringify(usuario, (chave, valor) => {
if (chave === 'senha') return undefined; // omite a propriedade
return valor;
});
// Reviver: transformar valores ao fazer parse
const dados = JSON.parse('{"nascimento":"1995-08-20"}', (chave, valor) => {
if (chave === 'nascimento') return new Date(valor);
return valor;
});
console.log(dados.nascimento instanceof Date); // → true
// Tratamento de erros: JSON inválido lança SyntaxError
try {
JSON.parse('{nome: "Ana"}'); // chaves sem aspas — JSON inválido
} catch (erro) {
console.error('JSON inválido:', erro.message);
}
16.2.3 — Estruturas JSON complexas¶
// Resposta típica de uma API paginada
const resposta = {
"dados": [
{ "id": 1, "titulo": "HTML Semântico", "autor": { "id": 5, "nome": "Prof. Silva" } },
{ "id": 2, "titulo": "CSS Grid", "autor": { "id": 5, "nome": "Prof. Silva" } },
{ "id": 3, "titulo": "JavaScript", "autor": { "id": 7, "nome": "Prof. Lima" } }
],
"meta": {
"total": 42,
"pagina": 1,
"porPagina": 3,
"totalPaginas": 14
},
"links": {
"self": "/artigos?pagina=1",
"proximo": "/artigos?pagina=2",
"anterior": null,
"ultimo": "/artigos?pagina=14"
}
};
// Acessando dados aninhados
const primeiroTitulo = resposta.dados[0].titulo;
const nomeAutor = resposta.dados[0].autor.nome;
const totalPaginas = resposta.meta.totalPaginas;
// Desestruturação de resposta de API
const { dados: artigos, meta: { total, pagina } } = resposta;
// Mapeando para estrutura simplificada
const titulosPorAutor = artigos.map(a => ({
titulo: a.titulo,
autor: a.autor.nome
}));
16.2.4 — Armadilhas comuns com JSON¶
// 1. Datas em JSON são strings — não objetos Date
const evento = JSON.parse('{"data":"2026-03-22T10:00:00Z"}');
console.log(evento.data instanceof Date); // → false (é string!)
const dataReal = new Date(evento.data); // conversão manual necessária
// 2. null ≠ undefined em JSON
const obj = JSON.parse('{"nome":null}');
console.log(obj.nome); // → null
console.log(obj.sobrenome); // → undefined (propriedade não existe)
// 3. Números grandes perdem precisão
// JSON.parse preserva apenas até Number.MAX_SAFE_INTEGER (2^53 - 1)
const grande = JSON.parse('{"id":9007199254740993}');
console.log(grande.id); // → 9007199254740992 (impreciso!)
// Solução: APIs modernas enviam IDs grandes como strings
// 4. JSON.stringify omite undefined, functions e Symbol
const obj2 = { a: 1, b: undefined, c: () => {}, d: Symbol() };
JSON.stringify(obj2); // → '{"a":1}'
// 5. Referências circulares causam erro
const circular = {};
circular.self = circular;
JSON.stringify(circular); // TypeError: Converting circular structure to JSON
16.3 — Fetch API¶
Vídeo curto explicativo (link será adicionado posteriormente)
A Fetch API é a interface nativa do navegador para realizar requisições HTTP. Introduzida no ES2015 e amplamente suportada desde então, substituiu o antigo XMLHttpRequest com uma API baseada em Promises — muito mais legível e integrável com async/await.
16.3.1 — Sintaxe básica e o objeto Response¶
// fetch() retorna uma Promise que resolve para um objeto Response
const resposta = await fetch('https://viacep.com.br/ws/57072970/json/');
// O objeto Response contém metadados da resposta HTTP
console.log(resposta.ok); // → true (status 200-299)
console.log(resposta.status); // → 200
console.log(resposta.statusText); // → "OK"
console.log(resposta.url); // → URL final (após redirecionamentos)
console.log(resposta.headers.get('content-type')); // → "application/json"
// O corpo da resposta é lido com métodos assíncronos
const dados = await resposta.json(); // → objeto JavaScript
// ou
const texto = await resposta.text(); // → string
// ou
const blob = await resposta.blob(); // → Blob (para arquivos/imagens)
16.3.2 — Por que fetch não rejeita em erros HTTP¶
Este é o comportamento mais contraintuitivo da Fetch API e causa de bugs frequentes:
// fetch() APENAS rejeita (throw) em caso de erro de REDE
// (sem conexão, DNS falhou, timeout, etc.)
// Erros HTTP (404, 500, 403) NÃO causam rejeição — chegam como Response normal
// ❌ CÓDIGO BUGADO: não detecta erros HTTP
async function buscarUsuarioBugado(id) {
const dados = await fetch(`/api/usuarios/${id}`).then(r => r.json());
// Se a API retornar 404, dados conterá {"erro": "não encontrado"}
// mas nenhum erro será lançado
return dados;
}
// ✅ CÓDIGO CORRETO: verifica resposta.ok antes de parsear
async function buscarUsuario(id) {
const resposta = await fetch(`/api/usuarios/${id}`);
if (!resposta.ok) {
throw new Error(`Erro HTTP ${resposta.status}: ${resposta.statusText}`);
}
return resposta.json();
}
16.3.3 — Requisições GET com async/await¶
// Padrão completo e robusto para requisições GET
async function buscarDados(url) {
try {
const resposta = await fetch(url);
if (!resposta.ok) {
throw new Error(`HTTP ${resposta.status}: ${resposta.statusText}`);
}
return await resposta.json();
} catch (erro) {
if (erro instanceof TypeError) {
// TypeError: falha de rede (sem conexão, CORS, URL inválida)
throw new Error('Falha de rede. Verifique sua conexão.');
}
throw erro; // relança outros erros
}
}
// Uso
const cep = await buscarDados('https://viacep.com.br/ws/57072970/json/');
console.log(cep.logradouro); // → "Av. Lourival Melo Mota"
// Com query parameters
function construirUrl(base, params) {
const url = new URL(base);
Object.entries(params).forEach(([chave, valor]) => {
if (valor !== undefined && valor !== null) {
url.searchParams.append(chave, valor);
}
});
return url.toString();
}
const url = construirUrl('https://api.exemplo.com/produtos', {
categoria: 'eletronicos',
preco_max: 500,
pagina: 1,
limite: 20
});
// → "https://api.exemplo.com/produtos?categoria=eletronicos&preco_max=500&pagina=1&limite=20"
const produtos = await buscarDados(url);
16.3.4 — Requisições POST, PUT, PATCH e DELETE¶
// POST — criar recurso
async function criarProduto(dadosProduto) {
const resposta = await fetch('https://api.exemplo.com/produtos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${obterToken()}`
},
body: JSON.stringify(dadosProduto)
});
if (!resposta.ok) {
const erro = await resposta.json().catch(() => ({}));
throw new Error(erro.mensagem || `Erro ${resposta.status}`);
}
return resposta.json(); // retorna o recurso criado (geralmente com ID)
}
// PUT — substituição completa
async function substituirProduto(id, dadosCompletos) {
const resposta = await fetch(`https://api.exemplo.com/produtos/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dadosCompletos)
});
if (!resposta.ok) throw new Error(`Erro ${resposta.status}`);
return resposta.json();
}
// PATCH — atualização parcial
async function atualizarProduto(id, camposParaAtualizar) {
const resposta = await fetch(`https://api.exemplo.com/produtos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(camposParaAtualizar) // apenas os campos alterados
});
if (!resposta.ok) throw new Error(`Erro ${resposta.status}`);
return resposta.json();
}
// DELETE — remoção
async function removerProduto(id) {
const resposta = await fetch(`https://api.exemplo.com/produtos/${id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${obterToken()}` }
});
// DELETE frequentemente retorna 204 No Content (sem corpo)
if (!resposta.ok) throw new Error(`Erro ${resposta.status}`);
if (resposta.status === 204) return null;
return resposta.json();
}
16.3.5 — Enviando dados JSON no corpo da requisição¶
// Padrão completo para POST com JSON
const novoPedido = {
clienteId: 42,
itens: [
{ produtoId: 1, quantidade: 2 },
{ produtoId: 5, quantidade: 1 }
],
enderecoEntrega: {
cep: '57072-970',
numero: '100'
},
observacoes: 'Entregar pela manhã'
};
const resposta = await fetch('/api/pedidos', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // OBRIGATÓRIO para JSON
'Accept': 'application/json', // indica que esperamos JSON de volta
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(novoPedido)
});
// Verificar status específico de criação
if (resposta.status === 201) {
const pedidoCriado = await resposta.json();
console.log('Pedido criado com ID:', pedidoCriado.id);
} else if (!resposta.ok) {
const erro = await resposta.json();
// API pode retornar erros detalhados de validação
if (erro.erros) {
erro.erros.forEach(e => console.error(`Campo ${e.campo}: ${e.mensagem}`));
}
}
16.3.6 — Headers: Content-Type, Authorization e outros¶
// Configuração de headers comuns
const headersBase = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Accept-Language': 'pt-BR',
};
// Autenticação Bearer (JWT — o mais comum em APIs modernas)
function headersAutenticados() {
const token = localStorage.getItem('token');
return {
...headersBase,
'Authorization': `Bearer ${token}`
};
}
// Autenticação por API Key (comum em APIs públicas)
function headersApiKey(chave) {
return {
...headersBase,
'X-API-Key': chave
};
}
// Usando o objeto Headers para manipulação mais rica
const headers = new Headers({
'Content-Type': 'application/json'
});
headers.append('Authorization', `Bearer ${token}`);
headers.has('Content-Type'); // → true
headers.get('Content-Type'); // → "application/json"
headers.delete('Authorization');
// ⚠️ Headers que o navegador não permite definir manualmente
// (por segurança — são definidos automaticamente):
// Cookie, Host, Referer, Origin, User-Agent
16.3.7 — Enviando formulários com FormData¶
// FormData para envio de arquivos (multipart/form-data)
const form = document.querySelector('#form-upload');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
// FormData captura automaticamente todos os campos do formulário
// incluindo arquivos do type="file"
// NÃO definir Content-Type manualmente — o browser define
// automaticamente com o boundary correto para multipart
const resposta = await fetch('/api/upload', {
method: 'POST',
body: formData
// sem headers Content-Type aqui!
});
if (!resposta.ok) throw new Error('Falha no upload');
const resultado = await resposta.json();
console.log('Arquivo enviado:', resultado.url);
});
// FormData programático — construindo manualmente
const fd = new FormData();
fd.append('nome', 'Maria');
fd.append('foto', arquivoInput.files[0], 'foto-perfil.jpg');
fd.append('dados', JSON.stringify({ role: 'estudante' }));
// Inspecionar FormData
for (const [chave, valor] of fd.entries()) {
console.log(chave, valor);
}
16.4 — Tratamento de erros e UX¶
Vídeo curto explicativo (link será adicionado posteriormente)
16.4.1 — Erros de rede vs erros HTTP¶
// Classificação completa de erros em requisições fetch
async function requisicaoRobusta(url, opcoes = {}) {
try {
const resposta = await fetch(url, opcoes);
// Erro HTTP: servidor respondeu com código de erro
if (!resposta.ok) {
const corpo = await resposta.json().catch(() => null);
const erro = new Error(corpo?.mensagem || `HTTP ${resposta.status}`);
erro.status = resposta.status;
erro.corpo = corpo;
// Classificar por tipo de erro HTTP
if (resposta.status === 401) erro.tipo = 'nao_autenticado';
else if (resposta.status === 403) erro.tipo = 'sem_permissao';
else if (resposta.status === 404) erro.tipo = 'nao_encontrado';
else if (resposta.status === 422) erro.tipo = 'validacao';
else if (resposta.status === 429) erro.tipo = 'limite_excedido';
else if (resposta.status >= 500) erro.tipo = 'servidor';
else erro.tipo = 'cliente';
throw erro;
}
return resposta;
} catch (erro) {
// TypeError: erro de REDE (sem conexão, CORS, timeout, DNS)
if (erro instanceof TypeError) {
const erroRede = new Error('Falha de rede. Verifique sua conexão.');
erroRede.tipo = 'rede';
throw erroRede;
}
throw erro;
}
}
16.4.2 — Estados de interface: carregando, sucesso, erro, vazio¶
Toda operação assíncrona com uma API deve ser refletida na interface com estados visuais claros:
// Gerenciador de estado de UI para operações assíncronas
class EstadoUI {
constructor(containerSelector) {
this.container = document.querySelector(containerSelector);
}
carregando(mensagem = 'Carregando...') {
this.container.innerHTML = `
<div class="estado estado--carregando" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>${mensagem}</p>
</div>
`;
}
sucesso(html) {
this.container.innerHTML = html;
}
erro(mensagem, onRetry = null) {
this.container.innerHTML = `
<div class="estado estado--erro" role="alert">
<span class="estado__icone" aria-hidden="true">⚠️</span>
<p class="estado__mensagem">${mensagem}</p>
${onRetry ? `
<button type="button" class="btn btn--secundario" id="btn-tentar-novamente">
Tentar novamente
</button>
` : ''}
</div>
`;
if (onRetry) {
this.container.querySelector('#btn-tentar-novamente')
.addEventListener('click', onRetry);
}
}
vazio(mensagem = 'Nenhum item encontrado.') {
this.container.innerHTML = `
<div class="estado estado--vazio" role="status">
<span class="estado__icone" aria-hidden="true">📭</span>
<p>${mensagem}</p>
</div>
`;
}
}
// Uso integrado com fetch
async function carregarProdutos(filtros = {}) {
const ui = new EstadoUI('#lista-produtos');
ui.carregando('Buscando produtos...');
try {
const url = construirUrl('/api/produtos', filtros);
const dados = await buscarDados(url);
if (!dados.items.length) {
ui.vazio('Nenhum produto encontrado para os filtros selecionados.');
return;
}
ui.sucesso(renderizarProdutos(dados.items));
} catch (erro) {
const mensagem = erro.tipo === 'rede'
? 'Sem conexão com a internet.'
: 'Erro ao carregar produtos. Tente novamente.';
ui.erro(mensagem, () => carregarProdutos(filtros));
}
}
16.4.3 — Indicadores de carregamento acessíveis¶
<!-- Spinner acessível -->
<div class="spinner-container" role="status" aria-label="Carregando">
<div class="spinner" aria-hidden="true"></div>
<!-- Texto visível apenas para leitores de tela -->
<span class="sr-only">Carregando, aguarde...</span>
</div>
<!-- Skeleton loading — mais elegante que spinner para listas -->
<div class="skeleton-lista" aria-busy="true" aria-label="Carregando lista">
<div class="skeleton-item">
<div class="skeleton skeleton--titulo"></div>
<div class="skeleton skeleton--texto"></div>
<div class="skeleton skeleton--texto skeleton--curto"></div>
</div>
<!-- repetir N vezes -->
</div>
/* Animação de skeleton */
@keyframes skeleton-shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(
90deg,
#e2e8f0 25%,
#f1f5f9 50%,
#e2e8f0 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 4px;
}
/* Respeitar preferências de movimento reduzido */
@media (prefers-reduced-motion: reduce) {
.skeleton { animation: none; }
}
16.4.4 — Retry e timeout: padrões de resiliência¶
// Retry automático com backoff exponencial
async function fetchComRetry(url, opcoes = {}, maxTentativas = 3) {
let tentativa = 0;
while (tentativa < maxTentativas) {
try {
const resposta = await fetch(url, opcoes);
// Tentar novamente apenas em erros de servidor (5xx) ou 429
if (resposta.status >= 500 || resposta.status === 429) {
tentativa++;
if (tentativa >= maxTentativas) throw new Error(`HTTP ${resposta.status}`);
// Backoff exponencial: 1s, 2s, 4s...
const espera = Math.pow(2, tentativa - 1) * 1000;
await new Promise(resolve => setTimeout(resolve, espera));
continue;
}
return resposta;
} catch (erro) {
if (erro instanceof TypeError) { // erro de rede
tentativa++;
if (tentativa >= maxTentativas) throw erro;
await new Promise(resolve => setTimeout(resolve, 1000 * tentativa));
continue;
}
throw erro;
}
}
}
// Timeout com AbortController
async function fetchComTimeout(url, opcoes = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const resposta = await fetch(url, {
...opcoes,
signal: controller.signal
});
return resposta;
} catch (erro) {
if (erro.name === 'AbortError') {
throw new Error(`Timeout: a requisição demorou mais de ${timeoutMs}ms`);
}
throw erro;
} finally {
clearTimeout(timeout);
}
}
16.4.5 — Exercício prático: busca de CEP com ViaCEP¶
<form class="form-cep" id="form-cep" novalidate>
<div class="campo">
<label for="cep">CEP</label>
<div class="campo__input-wrapper">
<input type="text" id="cep" name="cep"
placeholder="00000-000" maxlength="9"
aria-describedby="cep-erro" autocomplete="postal-code" />
<button type="submit" class="btn btn--primario">Buscar</button>
</div>
<p class="campo__erro" id="cep-erro" role="alert" hidden></p>
</div>
</form>
<div id="resultado-cep"></div>
// Máscara automática de CEP
document.getElementById('cep').addEventListener('input', (e) => {
let v = e.target.value.replace(/\D/g, '').slice(0, 8);
if (v.length > 5) v = v.slice(0, 5) + '-' + v.slice(5);
e.target.value = v;
});
// Busca ao pressionar Enter ou sair do campo
document.getElementById('cep').addEventListener('blur', buscarCEP);
document.getElementById('form-cep').addEventListener('submit', (e) => {
e.preventDefault();
buscarCEP();
});
async function buscarCEP() {
const input = document.getElementById('cep');
const erroEl = document.getElementById('cep-erro');
const resultEl = document.getElementById('resultado-cep');
const cep = input.value.replace(/\D/g, '');
// Validação
if (cep.length !== 8) {
erroEl.textContent = 'Informe um CEP válido com 8 dígitos.';
erroEl.hidden = false;
input.setAttribute('aria-invalid', 'true');
return;
}
erroEl.hidden = true;
input.setAttribute('aria-invalid', 'false');
// Estado de carregamento
resultEl.innerHTML = `
<div class="estado estado--carregando" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Buscando endereço...</p>
</div>
`;
try {
const resposta = await fetchComTimeout(
`https://viacep.com.br/ws/${cep}/json/`,
{}, 5000
);
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
const dados = await resposta.json();
// ViaCEP retorna { erro: true } para CEPs inexistentes
if (dados.erro) {
resultEl.innerHTML = `
<div class="estado estado--vazio" role="alert">
<p>CEP não encontrado. Verifique o número informado.</p>
</div>
`;
return;
}
// Renderizar resultado
resultEl.innerHTML = `
<div class="endereco-card" aria-label="Endereço encontrado">
<dl class="endereco-dados">
<div class="endereco-campo">
<dt>Logradouro</dt>
<dd>${dados.logradouro || '—'}</dd>
</div>
<div class="endereco-campo">
<dt>Bairro</dt>
<dd>${dados.bairro || '—'}</dd>
</div>
<div class="endereco-campo">
<dt>Cidade</dt>
<dd>${dados.localidade} — ${dados.uf}</dd>
</div>
<div class="endereco-campo">
<dt>DDD</dt>
<dd>${dados.ddd || '—'}</dd>
</div>
<div class="endereco-campo">
<dt>IBGE</dt>
<dd>${dados.ibge || '—'}</dd>
</div>
</dl>
</div>
`;
// Preencher formulário automaticamente (se existir)
preencherFormularioComEndereco(dados);
} catch (erro) {
const mensagem = erro.message.includes('Timeout')
? 'A busca demorou muito. Tente novamente.'
: 'Erro ao buscar o CEP. Tente novamente.';
resultEl.innerHTML = `
<div class="estado estado--erro" role="alert">
<p>${mensagem}</p>
</div>
`;
}
}
function preencherFormularioComEndereco(dados) {
const mapa = {
'endereco-rua': dados.logradouro,
'endereco-bairro': dados.bairro,
'endereco-cidade': dados.localidade,
'endereco-estado': dados.uf,
};
Object.entries(mapa).forEach(([id, valor]) => {
const campo = document.getElementById(id);
if (campo && valor) campo.value = valor;
});
document.getElementById('endereco-numero')?.focus();
}
16.5 — APIs públicas: exemplos práticos¶
Vídeo curto explicativo (link será adicionado posteriormente)
16.5.1 — Critérios para escolher uma API pública¶
| Critério | O que verificar |
|---|---|
| Documentação | Clara, com exemplos de requisição e resposta |
| Autenticação | Gratuita sem chave? Cadastro necessário? |
| Rate limiting | Quantas requisições por minuto/hora/dia? |
| CORS | Permite requisições de qualquer origem? |
| Formato | JSON? XML? |
| Confiabilidade | SLA? Uptime histórico? |
| Versioning | URL versionada /v1/? Política de deprecação? |
16.5.2 — ViaCEP: busca de endereço por CEP¶
// Documentação: https://viacep.com.br
// Sem autenticação, sem rate limit declarado, CORS liberado
class ViaCEP {
static BASE_URL = 'https://viacep.com.br/ws';
static async buscarPorCEP(cep) {
const cepNumerico = cep.replace(/\D/g, '');
if (cepNumerico.length !== 8) throw new Error('CEP inválido');
const resposta = await fetch(`${this.BASE_URL}/${cepNumerico}/json/`);
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
const dados = await resposta.json();
if (dados.erro) throw new Error('CEP não encontrado');
return dados;
}
static async buscarPorLogradouro(uf, cidade, logradouro) {
const params = [uf, cidade, logradouro].map(encodeURIComponent).join('/');
const resposta = await fetch(`${this.BASE_URL}/${params}/json/`);
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return resposta.json();
}
}
// Uso
const endereco = await ViaCEP.buscarPorCEP('57072-970');
console.log(endereco.logradouro); // → "Av. Lourival Melo Mota"
16.5.3 — OpenWeatherMap: previsão do tempo¶
// Documentação: https://openweathermap.org/api
// Requer cadastro gratuito para obter API key
class OpenWeather {
#apiKey;
static BASE_URL = 'https://api.openweathermap.org/data/2.5';
constructor(apiKey) {
this.#apiKey = apiKey;
}
async buscarPorCidade(cidade) {
const url = new URL(`${OpenWeather.BASE_URL}/weather`);
url.searchParams.set('q', cidade);
url.searchParams.set('appid', this.#apiKey);
url.searchParams.set('units', 'metric'); // Celsius
url.searchParams.set('lang', 'pt_br');
const resposta = await fetch(url);
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return resposta.json();
}
async previsao5Dias(cidade) {
const url = new URL(`${OpenWeather.BASE_URL}/forecast`);
url.searchParams.set('q', cidade);
url.searchParams.set('appid', this.#apiKey);
url.searchParams.set('units', 'metric');
url.searchParams.set('lang', 'pt_br');
url.searchParams.set('cnt', '5'); // 5 períodos de 3h
const resposta = await fetch(url);
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return resposta.json();
}
}
// Renderizar dados do tempo
async function exibirTempo(cidade) {
const weather = new OpenWeather('SUA_CHAVE_AQUI');
try {
const dados = await weather.buscarPorCidade(cidade);
document.getElementById('tempo-card').innerHTML = `
<div class="tempo">
<h2 class="tempo__cidade">${dados.name}, ${dados.sys.country}</h2>
<div class="tempo__principal">
<img
src="https://openweathermap.org/img/wn/${dados.weather[0].icon}@2x.png"
alt="${dados.weather[0].description}"
class="tempo__icone"
/>
<span class="tempo__temperatura">${Math.round(dados.main.temp)}°C</span>
</div>
<p class="tempo__descricao">${dados.weather[0].description}</p>
<dl class="tempo__detalhes">
<div><dt>Sensação</dt><dd>${Math.round(dados.main.feels_like)}°C</dd></div>
<div><dt>Umidade</dt><dd>${dados.main.humidity}%</dd></div>
<div><dt>Vento</dt><dd>${Math.round(dados.wind.speed * 3.6)} km/h</dd></div>
</dl>
</div>
`;
} catch (erro) {
console.error('Erro ao buscar tempo:', erro);
}
}
16.5.4 — JSONPlaceholder: simulação de CRUD¶
// Documentação: https://jsonplaceholder.typicode.com
// API de teste sem autenticação — simula operações CRUD
// As operações POST/PUT/PATCH/DELETE são simuladas (não persistem)
class JSONPlaceholder {
static BASE = 'https://jsonplaceholder.typicode.com';
// Posts
static async listarPosts(params = {}) {
const url = construirUrl(`${this.BASE}/posts`, params);
return buscarDados(url);
}
static async buscarPost(id) {
return buscarDados(`${this.BASE}/posts/${id}`);
}
static async criarPost(dados) {
const resposta = await fetch(`${this.BASE}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dados)
});
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return resposta.json();
}
static async atualizarPost(id, dados) {
const resposta = await fetch(`${this.BASE}/posts/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dados)
});
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return resposta.json();
}
static async removerPost(id) {
const resposta = await fetch(`${this.BASE}/posts/${id}`, {
method: 'DELETE'
});
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return true;
}
// Comentários de um post
static async comentariosDoPost(postId) {
return buscarDados(`${this.BASE}/posts/${postId}/comments`);
}
// Usuários
static async listarUsuarios() {
return buscarDados(`${this.BASE}/users`);
}
}
16.5.5 — IBGE: dados geográficos do Brasil¶
// Documentação: https://servicodados.ibge.gov.br/api/docs
// Sem autenticação, dados geográficos e populacionais oficiais do Brasil
class IBGE {
static BASE = 'https://servicodados.ibge.gov.br/api/v1';
static async listarEstados() {
const estados = await buscarDados(`${this.BASE}/localidades/estados?orderBy=nome`);
return estados.sort((a, b) => a.nome.localeCompare(b.nome));
}
static async municipiosPorEstado(uf) {
return buscarDados(
`${this.BASE}/localidades/estados/${uf}/municipios?orderBy=nome`
);
}
static async buscarMunicipio(id) {
return buscarDados(`${this.BASE}/localidades/municipios/${id}`);
}
}
// Exemplo: select dinâmico de estado → município
async function inicializarSelectsLocalizacao() {
const selectEstado = document.getElementById('estado');
const selectMunicipio = document.getElementById('municipio');
// Carregar estados
const estados = await IBGE.listarEstados();
estados.forEach(estado => {
const option = document.createElement('option');
option.value = estado.sigla;
option.textContent = estado.nome;
selectEstado.appendChild(option);
});
// Carregar municípios ao selecionar estado
selectEstado.addEventListener('change', async () => {
const uf = selectEstado.value;
selectMunicipio.innerHTML = '<option value="">Carregando...</option>';
selectMunicipio.disabled = true;
try {
const municipios = await IBGE.municipiosPorEstado(uf);
selectMunicipio.innerHTML = '<option value="">Selecione o município...</option>';
municipios.forEach(m => {
const option = document.createElement('option');
option.value = m.id;
option.textContent = m.nome;
selectMunicipio.appendChild(option);
});
selectMunicipio.disabled = false;
} catch (erro) {
selectMunicipio.innerHTML = '<option value="">Erro ao carregar</option>';
}
});
}
16.5.6 — Exercício prático: dashboard com múltiplas APIs¶
// Dashboard que exibe tempo + dados geográficos + posts simulados
async function carregarDashboard() {
const ui = {
tempo: new EstadoUI('#widget-tempo'),
estados: new EstadoUI('#widget-estados'),
posts: new EstadoUI('#widget-posts'),
};
// Carregar tudo em paralelo
ui.tempo.carregando();
ui.estados.carregando();
ui.posts.carregando();
const [tempoResult, estadosResult, postsResult] = await Promise.allSettled([
new OpenWeather('SUA_CHAVE').buscarPorCidade('Maceió'),
IBGE.listarEstados(),
JSONPlaceholder.listarPosts({ _limit: 5 }),
]);
// Processar cada resultado independentemente
if (tempoResult.status === 'fulfilled') {
const d = tempoResult.value;
ui.tempo.sucesso(`<p>${d.name}: ${Math.round(d.main.temp)}°C</p>`);
} else {
ui.tempo.erro('Não foi possível carregar o tempo.');
}
if (estadosResult.status === 'fulfilled') {
const html = estadosResult.value
.slice(0, 5)
.map(e => `<li>${e.nome} (${e.sigla})</li>`)
.join('');
ui.estados.sucesso(`<ul>${html}</ul>`);
} else {
ui.estados.erro('Erro ao carregar estados.');
}
if (postsResult.status === 'fulfilled') {
const html = postsResult.value
.map(p => `<li>${p.title}</li>`)
.join('');
ui.posts.sucesso(`<ul>${html}</ul>`);
} else {
ui.posts.erro('Erro ao carregar posts.');
}
}
Referências: - MDN — Fetch API - MDN — Using Fetch - ViaCEP - JSONPlaceholder - IBGE API - OpenWeatherMap API
Atividades — Capítulo 16¶
1. Por que fetch() não lança um erro quando o servidor retorna status 404 ou 500?
2. Qual a diferença entre os métodos HTTP PUT e PATCH?
3. Por que ao enviar arquivos com FormData não se deve definir o header Content-Type: multipart/form-data manualmente?
- GitHub Classroom: Construir um buscador de endereço que: (1) use a API ViaCEP para buscar dados por CEP com máscara automática; (2) use a API IBGE para popular selects dinâmicos de estado/município; (3) exiba todos os estados de interface (carregando, sucesso, erro, vazio) com indicadores acessíveis. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 15 — Eventos e Formulários :material-arrow-right: Ir ao Capítulo 17 — Integração Frontend + API
Capítulo 17 — Integração Frontend + API¶
Vídeo curto explicativo (link será adicionado posteriormente)
17.1 — Arquitetura de uma SPA simples¶
Vídeo curto explicativo (link será adicionado posteriormente)
Nos capítulos anteriores, cada script JavaScript era um arquivo único responsável por tudo: buscar dados, manipular o DOM, gerenciar estado e lidar com eventos. Para projetos pequenos, isso é suficiente. À medida que a aplicação cresce — múltiplas telas, diversas chamadas de API, estado compartilhado entre componentes —, a ausência de organização transforma o código em um conjunto frágil e difícil de manter.
Este capítulo apresenta os padrões de organização que tornam projetos front-end reais sustentáveis, sem a necessidade de frameworks como React ou Vue — apenas JavaScript moderno com módulos ES6.
17.1.1 — O que é uma Single Page Application¶
Uma SPA (Single Page Application) é uma aplicação web que carrega um único documento HTML e atualiza dinamicamente seu conteúdo via JavaScript — sem recarregar a página ao navegar entre seções. A navegação é interceptada pelo JavaScript, que renderiza o conteúdo correto com base na URL atual.
Aplicação tradicional (MPA): SPA:
┌──────────────────────┐ ┌──────────────────────┐
│ /index.html │ │ /index.html │
│ /produtos.html │ vs. │ │
│ /produto-detalhe.html│ │ JavaScript controla │
│ /carrinho.html │ │ o que é exibido │
│ /sobre.html │ │ com base na URL │
└──────────────────────┘ └──────────────────────┘
Cada navegação = Navegação = JS
novo request ao servidor atualiza o DOM
Vantagens de uma SPA: - Navegação mais rápida (sem recarregar a página inteira) - Experiência mais fluida para o usuário - Reutilização de componentes entre telas - Estado da aplicação preservado entre navegações
Limitações que devem ser conhecidas: - SEO mais complexo (conteúdo renderizado via JS) - Carregamento inicial mais lento - Gestão de histórico do navegador requer atenção
17.1.2 — Separação de responsabilidades¶
O princípio de separação de responsabilidades (separation of concerns) divide o código em camadas com funções bem definidas:
┌─────────────────────────────────────────────────┐
│ UI Layer │
│ Renderização de HTML, manipulação de DOM, │
│ event listeners, estados visuais │
│ Arquivos: pages/, components/ │
└─────────────────────┬───────────────────────────┘
│ chama
┌─────────────────────▼───────────────────────────┐
│ State Layer │
│ Estado global da aplicação, notificação │
│ de mudanças para a UI │
│ Arquivo: store.js │
└─────────────────────┬───────────────────────────┘
│ chama
┌─────────────────────▼───────────────────────────┐
│ Service Layer │
│ Comunicação com APIs, cache, transformação │
│ de dados, tratamento de erros de rede │
│ Arquivos: services/ │
└─────────────────────────────────────────────────┘
Regra fundamental: a camada de UI nunca faz fetch() diretamente. Ela chama funções da camada de serviços. A camada de serviços nunca manipula o DOM. Essa separação permite testar cada camada isoladamente e trocar implementações sem cascata de mudanças.
17.1.3 — Módulos ES6: import e export¶
Os módulos ES6 permitem dividir o código em arquivos com escopos isolados, exportando apenas o que deve ser público:
// ── services/api.js ──────────────────────────────────────
// Export nomeado: exporta uma função específica
export async function buscarDados(url, opcoes = {}) {
const resposta = await fetch(url, opcoes);
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return resposta.json();
}
export function construirUrl(base, params = {}) {
const url = new URL(base);
Object.entries(params).forEach(([k, v]) => {
if (v !== null && v !== undefined) url.searchParams.set(k, v);
});
return url.toString();
}
// Export padrão: um único export principal por arquivo
export default class ApiClient {
constructor(baseUrl, opcoesPadrao = {}) {
this.baseUrl = baseUrl;
this.opcoesPadrao = opcoesPadrao;
}
async get(endpoint, params = {}) {
const url = construirUrl(`${this.baseUrl}${endpoint}`, params);
return buscarDados(url, this.opcoesPadrao);
}
async post(endpoint, dados) {
return buscarDados(`${this.baseUrl}${endpoint}`, {
...this.opcoesPadrao,
method: 'POST',
headers: { 'Content-Type': 'application/json',
...this.opcoesPadrao.headers },
body: JSON.stringify(dados)
});
}
}
// ── app.js — importando ──────────────────────────────────
// Import nomeado
import { buscarDados, construirUrl } from './services/api.js';
// Import padrão
import ApiClient from './services/api.js';
// Import misto
import ApiClient, { buscarDados } from './services/api.js';
// Import com alias
import { buscarDados as fetchData } from './services/api.js';
// Import de namespace
import * as Api from './services/api.js';
// Api.buscarDados(...), Api.construirUrl(...)
// Import dinâmico (lazy loading — carrega quando necessário)
const { renderizarGrafico } = await import('./components/grafico.js');
Importante: módulos ES6 só funcionam com o protocolo HTTP/HTTPS — não funcionam com
file://. Para desenvolvimento local, é necessário um servidor local simples. A extensão Live Server do VS Code resolve isso com um clique.
<!-- Declarar o script de entrada como módulo -->
<script type="module" src="js/app.js"></script>
17.1.4 — Organização de arquivos para projetos com API¶
projeto/
├── index.html
├── css/
│ ├── variables.css
│ ├── base.css
│ ├── components.css
│ └── pages.css
└── js/
├── app.js ← entrada: inicializa router e estado global
├── router.js ← roteamento baseado em hash
├── store.js ← estado global simplificado
├── services/
│ ├── api.js ← cliente HTTP genérico
│ ├── produtos.js ← serviços específicos de produtos
│ └── usuarios.js ← serviços específicos de usuários
├── components/
│ ├── card.js ← componente de card reutilizável
│ ├── modal.js ← componente modal
│ └── paginacao.js ← componente de paginação
└── pages/
├── listagem.js ← página de listagem
├── detalhe.js ← página de detalhe
└── formulario.js ← página de formulário
17.2 — Renderização dinâmica de dados¶
Vídeo curto explicativo (link será adicionado posteriormente)
17.2.1 — Do JSON ao HTML: padrões de renderização¶
O processo de converter dados JSON em HTML é o núcleo do desenvolvimento frontend com APIs. Existem três abordagens principais, cada uma com trade-offs:
// Abordagem 1: innerHTML com template literal
// Rápida e legível, mas requer cuidado com XSS
function renderizarCard(produto) {
return `
<article class="card" data-id="${produto.id}">
<img src="${produto.imagem}" alt="${escapar(produto.nome)}" loading="lazy" />
<div class="card__corpo">
<h2 class="card__titulo">${escapar(produto.nome)}</h2>
<p class="card__preco">R$ ${produto.preco.toFixed(2)}</p>
</div>
</article>
`;
}
// Abordagem 2: createElement (sem risco de XSS, mais verboso)
function criarCardSeguro(produto) {
const article = document.createElement('article');
article.className = 'card';
article.dataset.id = produto.id;
const img = document.createElement('img');
img.src = produto.imagem;
img.alt = produto.nome; // textContent é safe por padrão
img.loading = 'lazy';
const titulo = document.createElement('h2');
titulo.className = 'card__titulo';
titulo.textContent = produto.nome; // textContent nunca interpreta HTML
article.appendChild(img);
article.appendChild(titulo);
return article;
}
// Abordagem 3: <template> HTML (melhor performance, reutilizável)
// HTML: <template id="card-template">...</template>
function criarCardComTemplate(produto) {
const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);
clone.querySelector('.card__titulo').textContent = produto.nome;
clone.querySelector('.card__preco').textContent =
`R$ ${produto.preco.toFixed(2)}`;
clone.querySelector('img').src = produto.imagem;
clone.querySelector('img').alt = produto.nome;
clone.querySelector('.card').dataset.id = produto.id;
return clone;
}
// Função auxiliar de escape para uso seguro com innerHTML
function escapar(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
17.2.2 — Renderização de listas com map() e Fragment¶
// Renderização eficiente de listas usando DocumentFragment
function renderizarLista(container, itens, renderizarItem) {
container.innerHTML = '';
const fragment = document.createDocumentFragment();
itens.forEach(item => fragment.appendChild(renderizarItem(item)));
container.appendChild(fragment);
}
// Renderização com innerHTML (para HTML complexo)
function renderizarListaHTML(container, itens, renderizarItem) {
container.innerHTML = itens.map(renderizarItem).join('');
}
// Exemplo de uso
const produtos = await ProdutosService.listar();
const lista = document.querySelector('#lista-produtos');
renderizarListaHTML(lista, produtos, renderizarCard);
17.2.3 — Atualização parcial do DOM¶
Rerenderizar a lista inteira a cada mudança é ineficiente e destrói o estado visual (scroll position, foco, animações em andamento). A atualização parcial preserva o que não mudou:
// Atualizar apenas um item específico na lista
function atualizarItemNaLista(id, novosDados) {
const itemEl = document.querySelector(`[data-id="${id}"]`);
if (!itemEl) return;
// Atualizar apenas os campos que mudaram
const tituloEl = itemEl.querySelector('.card__titulo');
if (tituloEl && novosDados.nome) {
tituloEl.textContent = novosDados.nome;
}
const precoEl = itemEl.querySelector('.card__preco');
if (precoEl && novosDados.preco !== undefined) {
precoEl.textContent = `R$ ${novosDados.preco.toFixed(2)}`;
}
}
// Remover item sem re-renderizar a lista
function removerItemDaLista(id) {
const itemEl = document.querySelector(`[data-id="${id}"]`);
if (!itemEl) return;
// Animação de saída antes de remover
itemEl.classList.add('saindo');
itemEl.addEventListener('animationend', () => itemEl.remove(), { once: true });
}
// Adicionar item sem re-renderizar a lista
function adicionarItemNaLista(container, novoItem, renderizarItem) {
const novoEl = renderizarItem(novoItem);
novoEl.classList.add('entrando');
container.prepend(novoEl); // adiciona no início
}
17.2.4 — Exercício prático: listagem com filtro e ordenação¶
// Estado local da listagem
const estadoListagem = {
todos: [], // dados originais da API
filtrados: [], // dados após filtros
filtros: {
busca: '',
categoria: '',
precoMax: Infinity,
},
ordenacao: {
campo: 'nome',
direcao: 'asc',
},
pagina: 1,
porPagina: 12,
};
// Aplicar filtros e ordenação
function aplicarFiltrosOrdenacao() {
let resultado = [...estadoListagem.todos];
// Filtros
const { busca, categoria, precoMax } = estadoListagem.filtros;
if (busca) {
const termo = busca.toLowerCase();
resultado = resultado.filter(p =>
p.nome.toLowerCase().includes(termo) ||
p.descricao?.toLowerCase().includes(termo)
);
}
if (categoria) {
resultado = resultado.filter(p => p.categoria === categoria);
}
if (precoMax < Infinity) {
resultado = resultado.filter(p => p.preco <= precoMax);
}
// Ordenação
const { campo, direcao } = estadoListagem.ordenacao;
resultado.sort((a, b) => {
let valA = a[campo];
let valB = b[campo];
if (typeof valA === 'string') valA = valA.toLowerCase();
if (typeof valB === 'string') valB = valB.toLowerCase();
const comparacao = valA < valB ? -1 : valA > valB ? 1 : 0;
return direcao === 'asc' ? comparacao : -comparacao;
});
estadoListagem.filtrados = resultado;
estadoListagem.pagina = 1; // voltar à primeira página ao filtrar
renderizarPaginaAtual();
atualizarContador();
}
// Renderizar apenas a página atual
function renderizarPaginaAtual() {
const { filtrados, pagina, porPagina } = estadoListagem;
const inicio = (pagina - 1) * porPagina;
const fim = inicio + porPagina;
const itensPagina = filtrados.slice(inicio, fim);
const container = document.querySelector('#lista-produtos');
const ui = new EstadoUI('#lista-produtos');
if (!itensPagina.length) {
ui.vazio('Nenhum produto encontrado para os filtros aplicados.');
return;
}
renderizarListaHTML(container, itensPagina, renderizarCard);
renderizarPaginacao(filtrados.length);
}
function atualizarContador() {
const el = document.querySelector('#contador-resultados');
if (el) {
el.textContent = `${estadoListagem.filtrados.length} produto(s) encontrado(s)`;
}
}
// Event listeners para filtros
document.querySelector('#busca-produto')?.addEventListener('input',
debounce((e) => {
estadoListagem.filtros.busca = e.target.value;
aplicarFiltrosOrdenacao();
}, 300)
);
document.querySelector('#filtro-categoria')?.addEventListener('change', (e) => {
estadoListagem.filtros.categoria = e.target.value;
aplicarFiltrosOrdenacao();
});
document.querySelector('#ordenar-por')?.addEventListener('change', (e) => {
const [campo, direcao] = e.target.value.split(':');
estadoListagem.ordenacao = { campo, direcao };
aplicarFiltrosOrdenacao();
});
// Carregamento inicial
async function inicializarListagem() {
const ui = new EstadoUI('#lista-produtos');
ui.carregando('Carregando produtos...');
try {
estadoListagem.todos = await ProdutosService.listar();
estadoListagem.filtrados = [...estadoListagem.todos];
renderizarPaginaAtual();
await popularFiltros();
} catch (erro) {
ui.erro('Erro ao carregar produtos.', inicializarListagem);
}
}
17.3 — Busca dinâmica com API¶
Vídeo curto explicativo (link será adicionado posteriormente)
17.3.1 — Debounce: evitando requisições excessivas¶
Sem debounce, cada tecla digitada em um campo de busca dispara uma requisição. Em uma digitação de 10 caracteres, isso gera 10 requisições — 9 das quais são desnecessárias:
// Implementação de debounce
function debounce(fn, espera) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), espera);
};
}
// Implementação de throttle (para scroll, resize)
function throttle(fn, limite) {
let emEspera = false;
return function(...args) {
if (emEspera) return;
fn.apply(this, args);
emEspera = true;
setTimeout(() => { emEspera = false; }, limite);
};
}
// Busca com debounce de 300ms
const buscarComDebounce = debounce(async (termo) => {
if (!termo.trim()) {
limparResultados();
return;
}
await executarBusca(termo);
}, 300);
document.querySelector('#campo-busca').addEventListener('input', (e) => {
buscarComDebounce(e.target.value);
});
17.3.2 — Cancelamento de requisições com AbortController¶
O debounce evita requisições desnecessárias, mas não resolve o problema de race condition: se o usuário digitar "re", depois "reac" rapidamente, e a resposta de "re" chegar depois da de "reac", o resultado errado será exibido.
// Solução: cancelar a requisição anterior ao iniciar uma nova
let controladorAtual = null;
async function executarBusca(termo) {
// Cancelar requisição anterior se ainda estiver em andamento
if (controladorAtual) {
controladorAtual.abort();
}
controladorAtual = new AbortController();
const { signal } = controladorAtual;
const ui = new EstadoUI('#resultados-busca');
ui.carregando(`Buscando "${termo}"...`);
try {
const url = construirUrl('https://api.github.com/search/repositories', {
q: termo,
sort: 'stars',
per_page: 10
});
const resposta = await fetch(url, { signal });
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
const dados = await resposta.json();
controladorAtual = null; // limpa após sucesso
if (!dados.items.length) {
ui.vazio(`Nenhum resultado para "${termo}".`);
return;
}
ui.sucesso(renderizarResultadosBusca(dados.items, dados.total_count));
} catch (erro) {
// AbortError é esperado — não é um erro real
if (erro.name === 'AbortError') return;
ui.erro('Erro ao buscar. Tente novamente.');
console.error('Erro na busca:', erro);
}
}
17.3.3 — Cache simples no cliente¶
// Cache em memória usando Map
class CacheAPI {
#cache = new Map();
#ttl; // tempo de vida em ms
constructor(ttlMs = 5 * 60 * 1000) { // 5 minutos padrão
this.#ttl = ttlMs;
}
set(chave, valor) {
this.#cache.set(chave, {
valor,
expira: Date.now() + this.#ttl
});
}
get(chave) {
const item = this.#cache.get(chave);
if (!item) return null;
if (Date.now() > item.expira) {
this.#cache.delete(chave);
return null;
}
return item.valor;
}
has(chave) { return this.get(chave) !== null; }
clear() { this.#cache.clear(); }
delete(chave) { this.#cache.delete(chave); }
}
// Wrapper fetch com cache
const cache = new CacheAPI(2 * 60 * 1000); // 2 minutos
async function buscarComCache(url) {
// Retornar do cache se disponível
if (cache.has(url)) {
return cache.get(url);
}
const dados = await buscarDados(url);
cache.set(url, dados);
return dados;
}
17.3.4 — Exercício prático: busca de repositórios no GitHub¶
<div class="busca-github">
<form class="busca-form" id="form-github" novalidate>
<div class="busca-campo">
<label for="busca-repo" class="sr-only">Buscar repositórios</label>
<input
type="search"
id="busca-repo"
placeholder="Buscar repositórios no GitHub..."
autocomplete="off"
aria-label="Buscar repositórios no GitHub"
/>
</div>
<div class="busca-filtros">
<select id="linguagem-filtro" aria-label="Filtrar por linguagem">
<option value="">Todas as linguagens</option>
<option value="javascript">JavaScript</option>
<option value="typescript">TypeScript</option>
<option value="python">Python</option>
<option value="java">Java</option>
</select>
<select id="ordenar-github" aria-label="Ordenar por">
<option value="stars">Mais estrelas</option>
<option value="forks">Mais forks</option>
<option value="updated">Atualizado recentemente</option>
</select>
</div>
</form>
<p id="total-resultados" aria-live="polite" class="sr-only"></p>
<div id="resultados-github"></div>
</div>
// Serviço GitHub
class GitHubService {
static BASE = 'https://api.github.com';
static #cache = new CacheAPI(5 * 60 * 1000);
static async buscarRepositorios(params) {
const { termo, linguagem, ordenar = 'stars', pagina = 1 } = params;
let query = termo;
if (linguagem) query += ` language:${linguagem}`;
const url = construirUrl(`${this.BASE}/search/repositories`, {
q: query,
sort: ordenar,
order: 'desc',
per_page: 12,
page: pagina,
});
if (this.#cache.has(url)) return this.#cache.get(url);
const resposta = await fetch(url, {
headers: { 'Accept': 'application/vnd.github.v3+json' }
});
if (resposta.status === 403) {
throw new Error('Limite de requisições excedido. Aguarde um momento.');
}
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
const dados = await resposta.json();
this.#cache.set(url, dados);
return dados;
}
static async buscarUsuario(login) {
const url = `${this.BASE}/users/${login}`;
if (this.#cache.has(url)) return this.#cache.get(url);
const dados = await buscarDados(url);
this.#cache.set(url, dados);
return dados;
}
}
// Renderização
function renderizarRepositorio(repo) {
const linguagemHtml = repo.language
? `<span class="repo__linguagem">${escapar(repo.language)}</span>`
: '';
return `
<article class="repo-card" data-id="${repo.id}">
<div class="repo-card__cabecalho">
<img
src="${repo.owner.avatar_url}"
alt="${escapar(repo.owner.login)}"
class="repo-card__avatar"
loading="lazy"
/>
<div>
<h3 class="repo-card__titulo">
<a href="${repo.html_url}" target="_blank" rel="noopener noreferrer"
class="repo-card__link">
${escapar(repo.full_name)}
</a>
</h3>
${linguagemHtml}
</div>
</div>
${repo.description ? `
<p class="repo-card__descricao">
${escapar(repo.description)}
</p>
` : ''}
<div class="repo-card__stats">
<span title="Estrelas">⭐ ${formatarNumero(repo.stargazers_count)}</span>
<span title="Forks">🍴 ${formatarNumero(repo.forks_count)}</span>
<span title="Issues abertas">🐛 ${formatarNumero(repo.open_issues_count)}</span>
<span title="Última atualização">
📅 ${formatarData(repo.updated_at)}
</span>
</div>
</article>
`;
}
// Controlador da busca
let abortController = null;
const cacheBusca = new CacheAPI(2 * 60 * 1000);
const buscarComDebounce = debounce(async () => {
const termo = document.getElementById('busca-repo').value.trim();
const linguagem = document.getElementById('linguagem-filtro').value;
const ordenar = document.getElementById('ordenar-github').value;
if (!termo) {
document.getElementById('resultados-github').innerHTML = '';
return;
}
if (abortController) abortController.abort();
abortController = new AbortController();
const ui = new EstadoUI('#resultados-github');
ui.carregando('Buscando repositórios...');
try {
const dados = await GitHubService.buscarRepositorios({
termo, linguagem, ordenar
});
document.getElementById('total-resultados').textContent =
`${dados.total_count.toLocaleString('pt-BR')} repositórios encontrados`;
if (!dados.items.length) {
ui.vazio('Nenhum repositório encontrado.');
return;
}
const html = `
<p class="resultados-info">
${dados.total_count.toLocaleString('pt-BR')} resultados
${linguagem ? `em ${linguagem}` : ''}
</p>
<div class="repos-grid">
${dados.items.map(renderizarRepositorio).join('')}
</div>
`;
ui.sucesso(html);
abortController = null;
} catch (erro) {
if (erro.name === 'AbortError') return;
ui.erro(erro.message || 'Erro ao buscar repositórios.', buscarComDebounce);
}
}, 400);
// Inicializar
['busca-repo', 'linguagem-filtro', 'ordenar-github'].forEach(id => {
document.getElementById(id)?.addEventListener('input', buscarComDebounce);
document.getElementById(id)?.addEventListener('change', buscarComDebounce);
});
// Utilitários
function formatarNumero(n) {
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
return n.toString();
}
function formatarData(iso) {
return new Date(iso).toLocaleDateString('pt-BR', {
day: '2-digit', month: 'short', year: 'numeric'
});
}
17.4 — Aplicação completa: listagem, detalhe e busca¶
Vídeo curto explicativo (link será adicionado posteriormente)
17.4.1 — Roteamento simples via hash¶
O roteamento baseado em hash (#) utiliza a parte da URL após # para determinar qual "página" exibir — sem recarregar o documento:
// router.js
class Router {
#rotas = new Map();
#rotaAtual = null;
#rotaNotFound = null;
// Registrar rota
on(caminho, handler) {
this.#rotas.set(caminho, handler);
return this;
}
notFound(handler) {
this.#rotaNotFound = handler;
return this;
}
// Inicializar: ouvir mudanças de hash e rota inicial
inicializar() {
window.addEventListener('hashchange', () => this.#navegar());
this.#navegar(); // processar rota inicial
return this;
}
#navegar() {
const hash = window.location.hash.slice(1) || '/'; // remove o #
const [caminho, ...query] = hash.split('?');
// Buscar rota exata
if (this.#rotas.has(caminho)) {
const params = Object.fromEntries(
new URLSearchParams(query.join('?'))
);
this.#rotas.get(caminho)(params);
this.#rotaAtual = caminho;
return;
}
// Buscar rota com parâmetros (ex.: /produtos/:id)
for (const [padrao, handler] of this.#rotas) {
const match = this.#matchRota(padrao, caminho);
if (match) {
handler(match);
this.#rotaAtual = caminho;
return;
}
}
// Rota não encontrada
this.#rotaNotFound?.();
}
#matchRota(padrao, caminho) {
const partesP = padrao.split('/');
const partesC = caminho.split('/');
if (partesP.length !== partesC.length) return null;
const params = {};
for (let i = 0; i < partesP.length; i++) {
if (partesP[i].startsWith(':')) {
params[partesP[i].slice(1)] = decodeURIComponent(partesC[i]);
} else if (partesP[i] !== partesC[i]) {
return null;
}
}
return params;
}
// Navegar programaticamente
static ir(caminho) {
window.location.hash = caminho;
}
}
// app.js — configurando o router
import Router from './router.js';
import { renderizarListagem } from './pages/listagem.js';
import { renderizarDetalhe } from './pages/detalhe.js';
const router = new Router();
router
.on('/', () => renderizarListagem())
.on('/produtos', () => renderizarListagem())
.on('/produtos/:id', ({ id }) => renderizarDetalhe(id))
.on('/sobre', () => renderizarSobre())
.notFound( () => renderizarNotFound())
.inicializar();
17.4.2 — Tela de listagem com paginação¶
// pages/listagem.js
import ProdutosService from '../services/produtos.js';
import { renderizarCard } from '../components/card.js';
import Router from '../router.js';
export async function renderizarListagem(params = {}) {
const app = document.getElementById('app');
const pagina = parseInt(params.pagina) || 1;
const busca = params.busca || '';
app.innerHTML = `
<section class="listagem" aria-labelledby="titulo-listagem">
<header class="listagem__cabecalho">
<h1 id="titulo-listagem">Produtos</h1>
<form class="listagem__busca" id="form-busca">
<input type="search" name="busca" value="${escapar(busca)}"
placeholder="Buscar produtos..." aria-label="Buscar" />
<button type="submit" class="btn btn--primario">Buscar</button>
</form>
</header>
<div id="conteudo-listagem" aria-live="polite" aria-busy="true">
<div class="skeleton-grid">
${Array(12).fill('<div class="skeleton skeleton--card"></div>').join('')}
</div>
</div>
</section>
`;
// Form de busca
document.getElementById('form-busca').addEventListener('submit', (e) => {
e.preventDefault();
const termo = e.target.busca.value.trim();
Router.ir(`/produtos?busca=${encodeURIComponent(termo)}&pagina=1`);
});
try {
const resultado = await ProdutosService.listar({ busca, pagina });
const conteudo = document.getElementById('conteudo-listagem');
conteudo.setAttribute('aria-busy', 'false');
if (!resultado.dados.length) {
conteudo.innerHTML = `
<div class="estado estado--vazio">
<p>Nenhum produto encontrado${busca ? ` para "${escapar(busca)}"` : ''}.</p>
${busca ? `<a href="#/produtos" class="btn btn--secundario">Ver todos</a>` : ''}
</div>
`;
return;
}
conteudo.innerHTML = `
<p class="listagem__total">${resultado.total} produto(s)</p>
<div class="grade-produtos" id="grade-produtos">
${resultado.dados.map(renderizarCard).join('')}
</div>
${renderizarPaginacao(resultado.total, resultado.porPagina, pagina, busca)}
`;
// Delegação de eventos nos cards
document.getElementById('grade-produtos').addEventListener('click', (e) => {
const card = e.target.closest('[data-id]');
if (card) Router.ir(`/produtos/${card.dataset.id}`);
});
} catch (erro) {
document.getElementById('conteudo-listagem').innerHTML = `
<div class="estado estado--erro" role="alert">
<p>Erro ao carregar produtos. Tente novamente.</p>
<button type="button" class="btn btn--secundario"
onclick="renderizarListagem(${JSON.stringify({ busca, pagina })})">
Tentar novamente
</button>
</div>
`;
}
}
function renderizarPaginacao(total, porPagina, paginaAtual, busca = '') {
const totalPaginas = Math.ceil(total / porPagina);
if (totalPaginas <= 1) return '';
const parametroBusca = busca ? `busca=${encodeURIComponent(busca)}&` : '';
const botoes = Array.from({ length: totalPaginas }, (_, i) => {
const p = i + 1;
const ativo = p === paginaAtual;
return `
<li>
<a
href="#/produtos?${parametroBusca}pagina=${p}"
class="paginacao__botao ${ativo ? 'paginacao__botao--ativo' : ''}"
aria-label="Página ${p}"
${ativo ? 'aria-current="page"' : ''}
>${p}</a>
</li>
`;
}).join('');
return `
<nav class="paginacao" aria-label="Paginação">
<ul class="paginacao__lista">
${paginaAtual > 1 ? `
<li>
<a href="#/produtos?${parametroBusca}pagina=${paginaAtual - 1}"
class="paginacao__botao" aria-label="Página anterior">
← Anterior
</a>
</li>
` : ''}
${botoes}
${paginaAtual < totalPaginas ? `
<li>
<a href="#/produtos?${parametroBusca}pagina=${paginaAtual + 1}"
class="paginacao__botao" aria-label="Próxima página">
Próxima →
</a>
</li>
` : ''}
</ul>
</nav>
`;
}
17.4.3 — Tela de detalhe com parâmetros de rota¶
// pages/detalhe.js
import ProdutosService from '../services/produtos.js';
import Router from '../router.js';
export async function renderizarDetalhe(id) {
const app = document.getElementById('app');
// Esqueleto imediato
app.innerHTML = `
<section class="detalhe" aria-labelledby="titulo-detalhe">
<a href="#/produtos" class="voltar-link">← Voltar</a>
<div id="conteudo-detalhe" aria-live="polite" aria-busy="true">
<div class="skeleton-detalhe">
<div class="skeleton skeleton--imagem"></div>
<div class="skeleton-info">
<div class="skeleton skeleton--titulo"></div>
<div class="skeleton skeleton--texto"></div>
<div class="skeleton skeleton--texto skeleton--curto"></div>
</div>
</div>
</div>
</section>
`;
try {
const produto = await ProdutosService.buscarPorId(id);
const conteudo = document.getElementById('conteudo-detalhe');
conteudo.setAttribute('aria-busy', 'false');
conteudo.innerHTML = `
<article class="produto-detalhe">
<div class="produto-detalhe__galeria">
<img
src="${escapar(produto.imagem)}"
alt="${escapar(produto.nome)}"
class="produto-detalhe__imagem"
/>
</div>
<div class="produto-detalhe__info">
<span class="produto-detalhe__categoria">
${escapar(produto.categoria)}
</span>
<h1 class="produto-detalhe__titulo" id="titulo-detalhe">
${escapar(produto.nome)}
</h1>
<p class="produto-detalhe__preco">
R$ ${produto.preco.toFixed(2)}
</p>
<div class="produto-detalhe__avaliacao" aria-label="Avaliação">
${renderizarEstrelas(produto.avaliacao)}
<span>(${produto.totalAvaliacoes} avaliações)</span>
</div>
<p class="produto-detalhe__descricao">
${escapar(produto.descricao)}
</p>
<div class="produto-detalhe__acoes">
<div class="quantidade">
<button type="button" class="btn-quantidade" id="btn-menos"
aria-label="Diminuir quantidade">−</button>
<input type="number" id="quantidade" value="1" min="1"
max="${produto.estoque}" aria-label="Quantidade" />
<button type="button" class="btn-quantidade" id="btn-mais"
aria-label="Aumentar quantidade">+</button>
</div>
<button type="button" class="btn btn--primario btn--bloco"
id="btn-adicionar">
Adicionar ao carrinho
</button>
</div>
<p class="produto-detalhe__estoque">
${produto.estoque > 0
? `${produto.estoque} unidades disponíveis`
: '<strong>Esgotado</strong>'}
</p>
</div>
</article>
`;
inicializarControlesQuantidade(produto.estoque);
document.getElementById('btn-adicionar').addEventListener('click', () => {
const qtd = parseInt(document.getElementById('quantidade').value);
adicionarAoCarrinho(produto, qtd);
});
} catch (erro) {
document.getElementById('conteudo-detalhe').innerHTML = `
<div class="estado estado--erro" role="alert">
<p>${erro.status === 404 ? 'Produto não encontrado.' : 'Erro ao carregar produto.'}</p>
<a href="#/produtos" class="btn btn--secundario">Ver todos os produtos</a>
</div>
`;
}
}
function renderizarEstrelas(nota) {
return Array.from({ length: 5 }, (_, i) => {
if (i < Math.floor(nota)) return '★';
if (i < nota) return '⯨'; // meia estrela
return '☆';
}).join('');
}
function inicializarControlesQuantidade(max) {
const input = document.getElementById('quantidade');
document.getElementById('btn-menos').addEventListener('click', () => {
input.value = Math.max(1, parseInt(input.value) - 1);
});
document.getElementById('btn-mais').addEventListener('click', () => {
input.value = Math.min(max, parseInt(input.value) + 1);
});
}
17.4.4 — Estado global simples sem framework¶
// store.js — estado global reativo sem framework
class Store {
#estado;
#ouvintes = new Map();
constructor(estadoInicial) {
this.#estado = estadoInicial;
}
// Ler estado (imutável externamente)
get(chave) {
return structuredClone(this.#estado[chave]);
}
getAll() {
return structuredClone(this.#estado);
}
// Atualizar estado
set(chave, valor) {
const anterior = this.#estado[chave];
this.#estado[chave] = valor;
// Notificar ouvintes
this.#notificar(chave, valor, anterior);
this.#notificar('*', this.#estado, this.#estado);
}
// Atualização parcial de objeto
merge(chave, parcial) {
const atual = this.#estado[chave];
if (typeof atual !== 'object') throw new Error(`${chave} não é objeto`);
this.set(chave, { ...atual, ...parcial });
}
// Inscrever em mudanças
subscribe(chave, callback) {
if (!this.#ouvintes.has(chave)) {
this.#ouvintes.set(chave, new Set());
}
this.#ouvintes.get(chave).add(callback);
// Retorna função de cancelamento
return () => this.#ouvintes.get(chave)?.delete(callback);
}
#notificar(chave, novoValor, valorAnterior) {
this.#ouvintes.get(chave)?.forEach(cb => cb(novoValor, valorAnterior));
}
}
// Estado global da aplicação
export const store = new Store({
usuario: null,
carrinho: { itens: [], total: 0 },
tema: localStorage.getItem('tema') || 'claro',
notificacoes: [],
});
// Persistir tema no localStorage
store.subscribe('tema', (novoTema) => {
localStorage.setItem('tema', novoTema);
document.documentElement.dataset.tema = novoTema;
});
// Atualizar badge do carrinho ao mudar
store.subscribe('carrinho', (carrinho) => {
const badge = document.getElementById('badge-carrinho');
if (badge) {
const total = carrinho.itens.reduce((s, i) => s + i.quantidade, 0);
badge.textContent = total;
badge.hidden = total === 0;
}
});
// Uso
store.set('tema', 'escuro');
store.merge('carrinho', {
itens: [...store.get('carrinho').itens, { id: 1, nome: 'Produto', quantidade: 1 }]
});
17.4.5 — Exercício prático: catálogo de filmes com OMDb API¶
// Documentação: https://www.omdbapi.com
// Requer chave de API gratuita (1000 req/dia)
class OMDbService {
static #apiKey = 'SUA_CHAVE_AQUI';
static BASE = 'https://www.omdbapi.com';
static #cache = new CacheAPI(10 * 60 * 1000); // 10 min
static async buscar(params) {
const url = construirUrl(this.BASE, {
apikey: this.#apiKey,
...params
});
if (this.#cache.has(url)) return this.#cache.get(url);
const dados = await buscarDados(url);
if (dados.Response === 'False') {
throw new Error(dados.Error || 'Erro na API');
}
this.#cache.set(url, dados);
return dados;
}
static async pesquisar(titulo, tipo = '', pagina = 1) {
return this.buscar({
s: titulo,
type: tipo || undefined,
page: pagina
});
}
static async detalhe(imdbId) {
return this.buscar({ i: imdbId, plot: 'full' });
}
}
// Página de catálogo de filmes
export async function renderizarCatalogo(params = {}) {
const app = document.getElementById('app');
const busca = params.busca || 'Matrix';
const tipo = params.tipo || '';
const pagina = parseInt(params.pagina) || 1;
app.innerHTML = `
<main class="catalogo" aria-labelledby="titulo-catalogo">
<header class="catalogo__cabecalho">
<h1 id="titulo-catalogo">🎬 Catálogo de Filmes</h1>
<form id="form-busca-filmes">
<input type="search" id="busca-filmes" value="${escapar(busca)}"
placeholder="Buscar filmes, séries..." />
<select id="tipo-filtro">
<option value="">Todos</option>
<option value="movie" ${tipo === 'movie' ? 'selected' : ''}>Filmes</option>
<option value="series" ${tipo === 'series' ? 'selected' : ''}>Séries</option>
</select>
<button type="submit" class="btn btn--primario">Buscar</button>
</form>
</header>
<div id="resultados-filmes" aria-live="polite" aria-busy="true">
${gerarSkeletons(6, 'skeleton--card')}
</div>
</main>
`;
document.getElementById('form-busca-filmes').addEventListener('submit', (e) => {
e.preventDefault();
const t = document.getElementById('busca-filmes').value.trim();
const tp = document.getElementById('tipo-filtro').value;
Router.ir(`/?busca=${encodeURIComponent(t)}&tipo=${tp}&pagina=1`);
});
const ui = new EstadoUI('#resultados-filmes');
try {
const resultado = await OMDbService.pesquisar(busca, tipo, pagina);
const filmes = resultado.Search;
const total = parseInt(resultado.totalResults);
ui.sucesso(`
<p class="catalogo__total">${total.toLocaleString('pt-BR')} resultado(s)</p>
<div class="filmes-grade">
${filmes.map(renderizarCardFilme).join('')}
</div>
${renderizarPaginacao(total, 10, pagina, `busca=${encodeURIComponent(busca)}&tipo=${tipo}`)}
`);
// Click nos cards → detalhe
document.querySelector('.filmes-grade')?.addEventListener('click', (e) => {
const card = e.target.closest('[data-imdb]');
if (card) Router.ir(`/filme/${card.dataset.imdb}`);
});
} catch (erro) {
ui.erro(erro.message || 'Erro ao buscar filmes.');
}
}
function renderizarCardFilme(filme) {
const poster = filme.Poster !== 'N/A'
? filme.Poster
: 'https://via.placeholder.com/300x445?text=Sem+imagem';
return `
<article class="filme-card" data-imdb="${filme.imdbID}" tabindex="0"
role="button" aria-label="${escapar(filme.Title)} (${filme.Year})">
<img src="${poster}" alt="Poster: ${escapar(filme.Title)}" loading="lazy" />
<div class="filme-card__info">
<h3 class="filme-card__titulo">${escapar(filme.Title)}</h3>
<span class="filme-card__ano">${filme.Year}</span>
<span class="filme-card__tipo">${traduzirTipo(filme.Type)}</span>
</div>
</article>
`;
}
function traduzirTipo(tipo) {
return { movie: 'Filme', series: 'Série', episode: 'Episódio' }[tipo] || tipo;
}
function gerarSkeletons(n, classe) {
return `<div class="grade-skeleton">${
Array(n).fill(`<div class="skeleton ${classe}"></div>`).join('')
}</div>`;
}
17.5 — Boas práticas e próximos passos¶
Vídeo curto explicativo (link será adicionado posteriormente)
17.5.1 — Separando camada de serviços da camada de UI¶
// services/produtos.js — camada de serviços isolada
import { buscarDados, construirUrl } from './api.js';
const BASE = 'https://fakestoreapi.com';
export default class ProdutosService {
static async listar(params = {}) {
const url = construirUrl(`${BASE}/products`, params);
const dados = await buscarDados(url);
// Transformação: adapta o formato da API ao formato interno da app
return {
dados: dados.map(normalizarProduto),
total: dados.length,
porPagina: params.limit || dados.length
};
}
static async buscarPorId(id) {
const dados = await buscarDados(`${BASE}/products/${id}`);
return normalizarProduto(dados);
}
static async listarCategorias() {
return buscarDados(`${BASE}/products/categories`);
}
}
// Normalização: garante que os dados têm a forma que a UI espera
// independentemente de mudanças na API
function normalizarProduto(p) {
return {
id: p.id,
nome: p.title, // 'title' na API → 'nome' na app
descricao: p.description,
preco: p.price,
imagem: p.image,
categoria: p.category,
avaliacao: p.rating?.rate ?? 0,
totalAvaliacoes: p.rating?.count ?? 0,
estoque: Math.floor(Math.random() * 50) + 1 // simulado
};
}
17.5.2 — Variáveis de ambiente e segurança de chaves de API¶
// ⚠️ NUNCA expor chaves de API sensíveis no frontend
// Todo código JavaScript enviado ao browser é público e legível
// Chaves de APIs PÚBLICAS (leitura apenas, sem permissões perigosas)
// podem ser expostas no frontend com precaução:
const CONFIG = {
OMDB_KEY: 'abc123', // apenas leitura, sem risco financeiro
MAPS_KEY: 'xyz789', // restringir por domínio no painel da API
WEATHER_KEY: 'def456',
};
// Chaves com permissões de escrita ou acesso a dados sensíveis
// NUNCA devem estar no frontend — usar proxy no backend:
// Frontend → Seu Backend → API externa (com chave segura)
// Para projetos acadêmicos: usar variáveis em arquivo de configuração
// que não é commitado no git
// config.js (no .gitignore):
export const API_KEY = 'SUA_CHAVE_AQUI';
// config.example.js (versionado):
export const API_KEY = 'SUBSTITUA_PELA_SUA_CHAVE';
// .gitignore:
// config.js
// .env
17.5.3 — O que vem depois: frameworks modernos¶
Com os conceitos deste capítulo dominados — módulos, serviços, roteamento, estado, renderização dinâmica —, a transição para frameworks modernos é natural. Eles resolvem os mesmos problemas com mais elegância e produtividade:
| Conceito (Vanilla JS) | React | Vue | Angular |
|---|---|---|---|
| Template literals | JSX | Template syntax | Template syntax |
createElement manual |
Componentes | Componentes | Componentes |
store.js customizado |
useState / Redux | Pinia / Vuex | NgRx / Services |
Router customizado |
React Router | Vue Router | Angular Router |
ProdutosService |
hooks customizados | Composables | Services |
EventEmitter customizado |
Context API | emit/props | EventEmitter |
O código escrito neste capítulo não é descartado ao aprender um framework — ele expõe os fundamentos que os frameworks abstraem. Desenvolvedores que aprendem React sem entender o DOM e o ciclo de renderização manual têm dificuldade em depurar problemas reais. A jornada deste curso foi deliberada.
Referências: - MDN — JavaScript Modules - MDN — History API - OMDb API - Fake Store API
Atividades — Capítulo 17¶
1. Por que é importante separar a camada de serviços da camada de UI?
2. O que é uma race condition em buscas dinâmicas e como o AbortController resolve esse problema?
3. Qual é a vantagem de normalizar os dados da API em uma camada de serviços antes de passá-los para a UI?
- GitHub Classroom: Construir um catálogo de filmes com a OMDb API (ou JSONPlaceholder como alternativa) que implemente: roteamento por hash com telas de listagem e detalhe, busca dinâmica com debounce e AbortController, paginação funcional, todos os estados de UI (carregando, sucesso, erro, vazio) e camada de serviços separada da UI. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 16 — Consumo de APIs :material-arrow-right: Ir ao Capítulo 18 — Projeto Final
Capítulo 18 — Projeto Final¶
Vídeo curto explicativo (link será adicionado posteriormente)
18.1 — Especificação e planejamento¶
Vídeo curto explicativo (link será adicionado posteriormente)
O Projeto Final é a síntese de tudo que foi estudado ao longo do ano letivo. Ele não é um exercício isolado — é a demonstração de que o estudante é capaz de integrar HTML semântico, CSS moderno, JavaScript e consumo de APIs em uma aplicação front-end coesa, funcional e acessível.
O projeto é desenvolvido em três sprints ao longo das semanas 37 a 40, com checkpoints de revisão ao final de cada sprint. Esta estrutura simula o desenvolvimento iterativo usado em equipes de software reais.
18.1.1 — Escopo do projeto: o que deve ser entregue¶
A aplicação final deve ser uma SPA (Single Page Application) que consome pelo menos uma API pública e oferece ao usuário as seguintes funcionalidades:
Funcionalidades obrigatórias:
| # | Funcionalidade | Conceitos envolvidos |
|---|---|---|
| 1 | Listagem de dados da API com filtro e busca | Fetch, renderização dinâmica, debounce |
| 2 | Tela de detalhe de um item | Roteamento por hash, parâmetros de URL |
| 3 | Formulário com validação JavaScript completa | Validação de múltiplos tipos de campos |
| 4 | Navegação entre telas sem recarregar a página | Hash routing |
| 5 | Estados de UI: carregando, sucesso, erro, vazio | Gestão de estados assíncronos |
| 6 | Design responsivo (mobile-first) | Flexbox, Grid, media queries |
| 7 | Acessibilidade mínima | ARIA, navegação por teclado, contraste |
| 8 | Persistência local de pelo menos um dado | localStorage |
Funcionalidades opcionais (bônus):
- Tema claro/escuro com toggle e persistência
- Paginação na listagem
- Busca com AbortController (sem race condition)
- Animações e transições com CSS
- Componentes reutilizáveis com
@applydo Tailwind ou CSS customizado - Deploy no GitHub Pages
18.1.2 — Requisitos técnicos¶
HTML:
- Marcação semântica com os elementos corretos (<main>, <nav>, <article>, <section>, <header>, <footer>)
- Meta tag viewport e charset
- Atributos alt em todas as imagens
- <title> descritivo
- Formulários com <label> associados a todos os campos
CSS:
- Sistema de variáveis CSS com tokens de design (cores, tipografia, espaçamento)
- Layout responsivo mobile-first com pelo menos dois breakpoints
- Reset com box-sizing: border-box
- Sem uso de !important indiscriminado
- Organização em seções comentadas
JavaScript:
- Código organizado em módulos ES6 (import/export)
- Camada de serviços separada da camada de UI
- Tratamento de erros em todas as operações assíncronas
- Sem uso de var; uso consciente de const e let
- Sem manipulação de DOM dentro de serviços
- Nenhuma chave de API sensível exposta no código versionado
Acessibilidade: - Score ≥ 80 no Lighthouse - Navegação completa por teclado - Contraste mínimo 4,5:1 para texto normal - Atributos ARIA nos componentes interativos (modal, accordion, abas)
18.1.3 — Escolha da API pública¶
A escolha da API define o domínio da aplicação. Critérios a considerar:
✅ APIs recomendadas para o projeto:
API Domínio Auth Docs
─────────────────────────────────────────────────
OMDb Filmes/séries Key* omdbapi.com
Open Library Livros Não openlibrary.org/developers
PokéAPI Pokémon Não pokeapi.co
Rick and Morty Série animada Não rickandmortyapi.com
GitHub Repositórios Não** docs.github.com/rest
OpenWeather Clima Key* openweathermap.org/api
TheMealDB Receitas Não themealdb.com/api.php
NewsAPI Notícias Key* newsapi.org
IBGE + ViaCEP Dados BR Não (combinação)
* Key gratuita com cadastro simples
** Sem autenticação para leitura básica (limite de req/h)
Imagem sugerida: capturas de tela das documentações das APIs recomendadas, mostrando o endpoint de listagem e um exemplo de resposta JSON — para que os alunos possam comparar o formato dos dados antes de escolher.
(imagem será adicionada posteriormente)
18.1.4 — Prototipação: wireframes¶
Antes de escrever uma linha de código, o projeto deve ser prototipado. Wireframes evitam retrabalho e forçam decisões de layout antes que elas sejam caras de mudar.
Telas mínimas a prototipar:
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ Tela 1: Listagem │ │ Tela 2: Detalhe │
│ │ │ │
│ [Header/Nav] │ │ [← Voltar] │
│ │ │ │
│ [Busca] [Filtros] │ │ [Imagem grande] │
│ │ │ [Título] │
│ [Card] [Card] [Card] │ │ [Descrição] │
│ [Card] [Card] [Card] │ │ [Detalhes] │
│ │ │ [Ação] │
│ [Paginação] │ │ │
│ │ │ │
│ [Footer] │ │ [Footer] │
└─────────────────────────────┘ └─────────────────────────────┘
┌─────────────────────────────┐
│ Tela 3: Formulário │
│ │
│ [Campo 1] │
│ [Campo 2] │
│ [Campo 3 — select] │
│ [Checkbox] │
│ [Botão Enviar] │
│ │
└─────────────────────────────┘
Ferramentas gratuitas para wireframes: papel e caneta, Excalidraw, Figma (plano gratuito), draw.io.
18.1.5 — Estrutura de arquivos e setup inicial¶
projeto-final/
├── index.html
├── README.md
├── .gitignore
├── config.example.js ← modelo de configuração (sem chaves reais)
│
├── css/
│ ├── tokens.css ← variáveis CSS (cores, tipografia, espaçamento)
│ ├── reset.css ← reset + base
│ ├── components.css ← componentes reutilizáveis
│ ├── pages.css ← estilos específicos de páginas
│ └── utilities.css ← classes utilitárias
│
└── js/
├── app.js ← entrada + inicialização do router
├── router.js ← roteamento por hash
├── store.js ← estado global (opcional)
├── utils.js ← funções utilitárias (debounce, formatadores, etc.)
│
├── services/
│ ├── api.js ← cliente HTTP genérico
│ └── [dominio].js ← serviço específico (filmes.js, livros.js...)
│
├── components/
│ ├── modal.js
│ ├── paginacao.js
│ └── notificacao.js
│
└── pages/
├── listagem.js
├── detalhe.js
└── formulario.js
Setup inicial — index.html:
<!DOCTYPE html>
<html lang="pt-BR" data-tema="claro">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Descrição da sua aplicação" />
<title>Nome do Projeto — Programação Web 1</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet" />
<link rel="stylesheet" href="css/tokens.css" />
<link rel="stylesheet" href="css/reset.css" />
<link rel="stylesheet" href="css/components.css" />
<link rel="stylesheet" href="css/pages.css" />
<link rel="stylesheet" href="css/utilities.css" />
<script type="module" src="js/app.js" defer></script>
</head>
<body>
<!-- Navegação principal -->
<header class="site-header" role="banner">
<nav class="navbar" aria-label="Navegação principal">
<a href="#/" class="navbar__logo">
<span>🎬 Nome do Projeto</span>
</a>
<ul class="navbar__links" role="list">
<li><a href="#/" class="navbar__link">Início</a></li>
<li><a href="#/favoritos" class="navbar__link">Favoritos</a></li>
<li><a href="#/sobre" class="navbar__link">Sobre</a></li>
</ul>
<button type="button" id="btn-tema" class="btn-icone"
aria-label="Alternar tema">🌙</button>
</nav>
</header>
<!-- Conteúdo principal — atualizado pelo router -->
<main id="app" tabindex="-1">
<!-- Conteúdo dinâmico renderizado aqui -->
</main>
<footer class="site-footer" role="contentinfo">
<p>Projeto Final — Programação Web 1 — IFAL © 2026</p>
</footer>
</body>
</html>
18.2 — Sprint 1: estrutura HTML e dados¶
Vídeo curto explicativo (link será adicionado posteriormente)
Objetivo do Sprint 1: ter a aplicação funcionando com dados reais da API, sem preocupação com CSS detalhado.
Entregável: página com listagem de dados da API renderizados no DOM, navegação entre listagem e detalhe funcionando via hash routing.
18.2.1 — Checklist Sprint 1¶
HTML:
□ index.html com estrutura semântica completa
□ Meta tags: charset, viewport, description, title
□ <header> com <nav> acessível
□ <main id="app"> como container do conteúdo dinâmico
□ <footer> com informações do projeto
□ Links de navegação apontando para rotas hash (#/, #/detalhe/:id)
JavaScript — Serviços:
□ services/api.js com função buscarDados() e construirUrl()
□ services/[dominio].js com funções listar() e buscarPorId()
□ Normalização dos dados da API para o formato interno
□ Tratamento de erros com classificação (rede vs HTTP)
JavaScript — Roteamento:
□ router.js implementado com on(), notFound() e inicializar()
□ Rotas configuradas em app.js: /, /[recurso], /[recurso]/:id
□ Navegação funcional entre listagem e detalhe
JavaScript — Páginas:
□ pages/listagem.js: renderiza lista de itens da API
□ pages/detalhe.js: renderiza detalhe de um item pelo ID
□ Estados de UI: carregando e erro implementados
□ Estado vazio tratado
18.2.2 — Template de serviço (ponto de partida)¶
// services/filmes.js — adapte para sua API escolhida
import { buscarDados, construirUrl } from './api.js';
const BASE_URL = 'https://www.omdbapi.com';
const API_KEY = 'SUA_CHAVE'; // mover para config.js em produção
export default class FilmesService {
static async listar({ busca = 'Matrix', tipo = '', pagina = 1 } = {}) {
const url = construirUrl(BASE_URL, {
apikey: API_KEY,
s: busca || 'Matrix',
type: tipo || undefined,
page: pagina,
});
const dados = await buscarDados(url);
if (dados.Response === 'False') {
if (dados.Error === 'Movie not found!') return { dados: [], total: 0 };
throw new Error(dados.Error);
}
return {
dados: dados.Search.map(normalizarFilme),
total: parseInt(dados.totalResults),
porPagina: 10,
pagina,
};
}
static async buscarPorId(imdbId) {
const url = construirUrl(BASE_URL, {
apikey: API_KEY,
i: imdbId,
plot: 'full',
});
const dados = await buscarDados(url);
if (dados.Response === 'False') throw Object.assign(
new Error(dados.Error), { status: 404 }
);
return normalizarFilmeDetalhe(dados);
}
}
// Normalização — listagem
function normalizarFilme(f) {
return {
id: f.imdbID,
titulo: f.Title,
ano: f.Year,
tipo: f.Type,
poster: f.Poster !== 'N/A' ? f.Poster : null,
};
}
// Normalização — detalhe (mais campos)
function normalizarFilmeDetalhe(f) {
return {
...normalizarFilme(f),
diretor: f.Director,
elenco: f.Actors,
genero: f.Genre,
sinopse: f.Plot,
avaliacao: parseFloat(f.imdbRating) || 0,
duracao: f.Runtime,
idioma: f.Language,
premiacao: f.Awards,
};
}
18.2.3 — Checkpoint Sprint 1: revisão de código¶
Ao final do Sprint 1, o professor realizará uma revisão de código verificando:
- A API está sendo consumida corretamente?
- Os dados estão sendo normalizados antes de chegar na UI?
- O tratamento de erros está presente em todas as funções assíncronas?
- A navegação entre telas funciona?
- O código está organizado em módulos separados?
18.3 — Sprint 2: CSS e responsividade¶
Vídeo curto explicativo (link será adicionado posteriormente)
Objetivo do Sprint 2: aplicar o Design System e tornar a aplicação visualmente refinada e responsiva em todos os dispositivos.
Entregável: aplicação com visual completo, responsiva em mobile (375px), tablet (768px) e desktop (1024px+).
18.3.1 — Checklist Sprint 2¶
CSS — Tokens e base:
□ tokens.css: variáveis de cor (primária, secundária, feedback, superfície, texto)
□ tokens.css: escala tipográfica com clamp() nos títulos
□ tokens.css: escala de espaçamento (--espaco-1 a --espaco-16)
□ tokens.css: tokens de borda, sombra e transição
□ reset.css: box-sizing border-box universal
□ reset.css: reset de margens e paddings
□ reset.css: imagens responsivas (max-width: 100%)
CSS — Componentes:
□ Navbar: logo à esquerda, links à direita, responsiva (hamburguer em mobile)
□ Cards: imagem, corpo, rodapé; hover state; layout responsivo
□ Botões: pelo menos duas variantes com estados hover, focus-visible, disabled
□ Formulário: campos com labels, estados de erro e sucesso
□ Estados de UI: carregando (skeleton), erro, vazio
□ Modal (se implementado): overlay, caixa, animação de entrada
CSS — Layout responsivo:
□ Layout de listagem: 1 coluna mobile → 2 colunas tablet → 3+ desktop
□ Layout de detalhe: empilhado mobile → lado a lado desktop
□ Navbar: empilhada mobile → linha desktop
□ Imagens com object-fit: cover em containers de dimensão fixa
□ Pelo menos dois breakpoints com @media (min-width: ...)
CSS — Tema escuro (opcional):
□ @media (prefers-color-scheme: dark) com redefinição dos tokens semânticos
□ Toggle manual com [data-tema="escuro"] e persistência em localStorage
18.3.2 — Template de tokens (ponto de partida)¶
/* css/tokens.css */
/* ── Primitivos ─────────────────────────────────── */
:root {
/* Escala de azul */
--azul-50: #eff6ff;
--azul-100: #dbeafe;
--azul-500: #3b82f6;
--azul-700: #1d4ed8;
--azul-900: #1e3a8a;
/* Escala de cinza */
--cinza-50: #f9fafb;
--cinza-100: #f3f4f6;
--cinza-200: #e5e7eb;
--cinza-500: #6b7280;
--cinza-700: #374151;
--cinza-900: #111827;
/* Feedback */
--verde-600: #16a34a;
--verde-50: #f0fdf4;
--vermelho-600: #dc2626;
--vermelho-50: #fef2f2;
--amarelo-600: #d97706;
--amarelo-50: #fffbeb;
}
/* ── Semânticos — Tema Claro ────────────────────── */
:root,
[data-tema="claro"] {
--cor-primaria: var(--azul-700);
--cor-primaria-hover: var(--azul-900);
--cor-primaria-suave: var(--azul-50);
--cor-fundo: var(--cinza-50);
--cor-fundo-card: #ffffff;
--cor-fundo-sutil: var(--cinza-100);
--cor-texto: var(--cinza-900);
--cor-texto-2: var(--cinza-500);
--cor-texto-inverso: #ffffff;
--cor-borda: var(--cinza-200);
--cor-borda-foco: var(--azul-500);
--cor-sucesso: var(--verde-600);
--cor-sucesso-fundo: var(--verde-50);
--cor-erro: var(--vermelho-600);
--cor-erro-fundo: var(--vermelho-50);
--cor-aviso: var(--amarelo-600);
--cor-aviso-fundo: var(--amarelo-50);
--sombra-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1);
--sombra-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--sombra-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
/* ── Semânticos — Tema Escuro ───────────────────── */
@media (prefers-color-scheme: dark) {
:root:not([data-tema="claro"]) {
--cor-primaria: var(--azul-500);
--cor-primaria-hover: var(--azul-100);
--cor-primaria-suave: #1e3a8a33;
--cor-fundo: #0f172a;
--cor-fundo-card: #1e293b;
--cor-fundo-sutil: #1e293b;
--cor-texto: #f1f5f9;
--cor-texto-2: #94a3b8;
--cor-texto-inverso: #0f172a;
--cor-borda: #334155;
--cor-borda-foco: var(--azul-500);
--sombra-sm: 0 1px 3px 0 rgb(0 0 0 / 0.4);
--sombra-md: 0 4px 6px -1px rgb(0 0 0 / 0.5);
--sombra-lg: 0 10px 15px -3px rgb(0 0 0 / 0.6);
}
}
[data-tema="escuro"] {
--cor-primaria: var(--azul-500);
/* ... mesmo que o @media acima ... */
}
/* ── Tipografia ─────────────────────────────────── */
:root {
--fonte-sans: 'Inter', system-ui, sans-serif;
--fonte-mono: 'Fira Code', Consolas, monospace;
--texto-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.8rem);
--texto-sm: clamp(0.875rem, 0.82rem + 0.25vw, 0.95rem);
--texto-base: clamp(1rem, 0.95rem + 0.25vw, 1.0625rem);
--texto-lg: clamp(1.125rem, 1rem + 0.5vw, 1.25rem);
--texto-xl: clamp(1.25rem, 1rem + 1vw, 1.5rem);
--texto-2xl: clamp(1.5rem, 1rem + 2vw, 2rem);
--texto-3xl: clamp(1.875rem, 1rem + 3vw, 2.5rem);
}
/* ── Espaçamento ────────────────────────────────── */
:root {
--espaco-1: 0.25rem;
--espaco-2: 0.5rem;
--espaco-3: 0.75rem;
--espaco-4: 1rem;
--espaco-6: 1.5rem;
--espaco-8: 2rem;
--espaco-12: 3rem;
--espaco-16: 4rem;
}
/* ── Bordas e raios ─────────────────────────────── */
:root {
--raio-sm: 4px;
--raio-md: 8px;
--raio-lg: 12px;
--raio-xl: 16px;
--raio-full: 9999px;
--transicao: 200ms ease;
}
18.3.3 — Checkpoint Sprint 2: revisão de código¶
Ao final do Sprint 2, verificar:
- Os tokens CSS estão sendo usados de forma consistente (sem valores hardcoded em
components.css)? - O layout é responsivo? Testar em 375px, 768px, 1024px e 1440px.
- As imagens não transbordam seus containers?
- Os estados de foco são visíveis em todos os elementos interativos?
- O contraste mínimo de 4,5:1 está sendo respeitado? (Verificar com DevTools)
18.4 — Sprint 3: JavaScript e integração completa¶
Vídeo curto explicativo (link será adicionado posteriormente)
Objetivo do Sprint 3: completar toda a interatividade, validação, acessibilidade e persistência de dados.
Entregável: aplicação completa e funcional, pronta para deploy e apresentação.
18.4.1 — Checklist Sprint 3¶
JavaScript — Interatividade:
□ Busca dinâmica com debounce (≥ 300ms)
□ Filtros que atualizam a listagem em tempo real
□ Paginação funcional (via URL hash ou estado)
□ AbortController na busca (sem race condition)
□ Formulário com validação de todos os campos obrigatórios
□ Modal de confirmação (se aplicável) com gestão de foco
□ Toggle de tema (claro/escuro) com persistência em localStorage
□ Pelo menos um item persistido em localStorage (favoritos, histórico, etc.)
JavaScript — Qualidade:
□ Todos os fetchs têm tratamento de erro com try/catch
□ Nenhum console.error ignorado sem feedback ao usuário
□ Sem referências a variáveis inexistentes (verificar DevTools → Console)
□ Código organizado em módulos ES6
□ Nenhuma lógica de negócio em event listeners (delegar para funções nomeadas)
Acessibilidade:
□ Lighthouse Accessibility ≥ 80
□ WAVE sem erros críticos (vermelho)
□ Todas as imagens têm alt descritivo ou alt="" (decorativas)
□ Todos os botões e links têm textos descritivos ou aria-label
□ Foco visível em todos os elementos interativos
□ Formulário com labels associados a todos os campos
□ Estados de loading anunciados com aria-live ou role="status"
□ Modal com gestão de foco (se implementado)
□ Navegação completa por Tab sem armadilhas de foco
18.4.2 — Implementando favoritos com localStorage¶
// Funcionalidade de favoritos — persistência local
class Favoritos {
static #CHAVE = 'app-favoritos';
static listar() {
return JSON.parse(localStorage.getItem(this.#CHAVE) || '[]');
}
static adicionar(item) {
const lista = this.listar();
if (!lista.find(f => f.id === item.id)) {
lista.unshift({ ...item, adicionadoEm: new Date().toISOString() });
localStorage.setItem(this.#CHAVE, JSON.stringify(lista));
this.#notificar();
}
}
static remover(id) {
const lista = this.listar().filter(f => f.id !== id);
localStorage.setItem(this.#CHAVE, JSON.stringify(lista));
this.#notificar();
}
static ehFavorito(id) {
return this.listar().some(f => f.id === id);
}
static toggle(item) {
this.ehFavorito(item.id) ? this.remover(item.id) : this.adicionar(item);
return this.ehFavorito(item.id);
}
static #notificar() {
window.dispatchEvent(new CustomEvent('favoritos-atualizados', {
detail: { total: this.listar().length }
}));
}
}
// Botão de favoritar em cards e detalhe
function criarBotaoFavorito(item) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn-favorito';
const isFav = Favoritos.ehFavorito(item.id);
btn.innerHTML = isFav ? '❤️' : '🤍';
btn.setAttribute('aria-label',
isFav ? `Remover ${item.titulo} dos favoritos`
: `Adicionar ${item.titulo} aos favoritos`
);
btn.setAttribute('aria-pressed', isFav ? 'true' : 'false');
btn.addEventListener('click', (e) => {
e.stopPropagation(); // evita navegar para o detalhe ao clicar
const novoEstado = Favoritos.toggle(item);
btn.innerHTML = novoEstado ? '❤️' : '🤍';
btn.setAttribute('aria-pressed', novoEstado ? 'true' : 'false');
btn.setAttribute('aria-label',
novoEstado ? `Remover ${item.titulo} dos favoritos`
: `Adicionar ${item.titulo} aos favoritos`
);
});
return btn;
}
// Atualizar badge de favoritos na navbar
window.addEventListener('favoritos-atualizados', (e) => {
const badge = document.querySelector('#badge-favoritos');
if (badge) {
badge.textContent = e.detail.total || '';
badge.hidden = e.detail.total === 0;
}
});
18.4.3 — Toggle de tema com persistência¶
// js/app.js — inicializar tema ao carregar
function inicializarTema() {
const temaSalvo = localStorage.getItem('tema');
const prefereEscuro = window.matchMedia('(prefers-color-scheme: dark)').matches;
const temaInicial = temaSalvo || (prefereEscuro ? 'escuro' : 'claro');
document.documentElement.dataset.tema = temaInicial;
atualizarBotaoTema(temaInicial);
}
function atualizarBotaoTema(tema) {
const btn = document.getElementById('btn-tema');
if (!btn) return;
btn.textContent = tema === 'escuro' ? '☀️' : '🌙';
btn.setAttribute('aria-label',
tema === 'escuro' ? 'Mudar para tema claro' : 'Mudar para tema escuro'
);
btn.setAttribute('aria-pressed', tema === 'escuro' ? 'true' : 'false');
}
function alternarTema() {
const temaAtual = document.documentElement.dataset.tema;
const novoTema = temaAtual === 'escuro' ? 'claro' : 'escuro';
document.documentElement.dataset.tema = novoTema;
localStorage.setItem('tema', novoTema);
atualizarBotaoTema(novoTema);
}
// Inicializar na carga
inicializarTema();
document.getElementById('btn-tema')?.addEventListener('click', alternarTema);
18.4.4 — Revisão final de acessibilidade¶
// Checklist de acessibilidade para executar antes da entrega
// 1. Executar no console do DevTools para encontrar imagens sem alt
const imgsSemAlt = document.querySelectorAll('img:not([alt])');
console.log(`Imagens sem alt: ${imgsSemAlt.length}`, imgsSemAlt);
// 2. Verificar botões sem texto acessível
const botoesVazios = [...document.querySelectorAll('button')]
.filter(b => !b.textContent.trim() && !b.getAttribute('aria-label'));
console.log(`Botões sem texto acessível: ${botoesVazios.length}`, botoesVazios);
// 3. Verificar campos sem label
const camposSemLabel = [...document.querySelectorAll('input, select, textarea')]
.filter(campo => {
const id = campo.id;
if (!id) return true;
return !document.querySelector(`label[for="${id}"]`) &&
!campo.getAttribute('aria-label') &&
!campo.getAttribute('aria-labelledby');
});
console.log(`Campos sem label: ${camposSemLabel.length}`, camposSemLabel);
18.4.5 — Checkpoint Sprint 3: revisão de código¶
Ao final do Sprint 3, verificar:
- O formulário valida todos os tipos de campos implementados?
- A busca tem debounce e AbortController?
- Os favoritos persistem entre recarregamentos da página?
- O Lighthouse Accessibility está ≥ 80?
- O console do DevTools está limpo de erros?
- O código funciona sem conexão com a API (erro tratado graciosamente)?
18.5 — Entrega e apresentação¶
Vídeo curto explicativo (link será adicionado posteriormente)
18.5.1 — Checklist de entrega¶
Repositório Git:
□ Repositório público no GitHub com nome descritivo
□ README.md com: descrição do projeto, API usada,
instruções de configuração, screenshots e link do deploy
□ .gitignore incluindo config.js, node_modules, .env
□ Pelo menos 10 commits com mensagens descritivas
□ Último commit não quebra a aplicação
Código:
□ Nenhum console.log de debug no código final
□ Nenhuma chave de API exposta no código versionado
□ Código formatado consistentemente (recomendado: Prettier)
□ Todos os arquivos com encoding UTF-8
Funcionalidades:
□ Todas as funcionalidades obrigatórias implementadas e testadas
□ A aplicação funciona nos navegadores Chrome, Firefox e Edge
□ A aplicação funciona em viewport de 375px (iPhone SE)
□ Nenhum erro JavaScript no console em uso normal
Acessibilidade:
□ Lighthouse Accessibility ≥ 80 (print da pontuação no README)
□ WAVE sem erros críticos (vermelho)
□ Navegação por teclado funcional na listagem e detalhe
18.5.2 — Deploy no GitHub Pages¶
O GitHub Pages permite publicar a aplicação gratuitamente diretamente do repositório:
Passo 1 — Preparar para deploy:
# Verificar que não há caminhos absolutos no código
# Substituir '/api/' por caminhos relativos se necessário
# Verificar que os imports de módulos usam extensão .js explícita
Passo 2 — Configurar GitHub Pages:
1. Acesse o repositório no GitHub
2. Vá em Settings → Pages
3. Em Source, selecione Deploy from a branch
4. Selecione a branch main e a pasta / (root)
5. Clique em Save
Passo 3 — Verificar o deploy:
- Aguarde 2–5 minutos
- Acesse https://seuusuario.github.io/nome-do-repositorio
- Verifique se a aplicação carrega corretamente
- Teste a navegação entre telas
Problema comum: módulos ES6 (type="module") funcionam com http:// ou https://, mas não com file://. No GitHub Pages isso não é um problema — o servidor serve os arquivos via HTTPS.
Outro problema comum: se o repositório não estiver na raiz do GitHub Pages (ex.: usuario.github.io/projeto/), os caminhos de import precisam ser relativos:
// ✅ Correto — caminho relativo
import Router from './router.js';
// ❌ Pode quebrar no deploy
import Router from '/js/router.js';
18.5.3 — Critérios de avaliação detalhados¶
| Critério | Peso | Descrição |
|---|---|---|
| HTML semântico | 10% | Uso correto dos elementos semânticos, formulário com labels, atributos de acessibilidade |
| CSS e responsividade | 20% | Tokens CSS, design coerente, responsivo em 3 viewports, tema escuro |
| JavaScript e qualidade | 25% | Módulos, tratamento de erros, código limpo, sem var |
| Consumo de API | 20% | Fetch correto, normalização, estados de UI completos, debounce |
| Funcionalidades | 15% | Listagem, detalhe, formulário, roteamento, favoritos/localStorage |
| Acessibilidade | 10% | Lighthouse ≥ 80, navegação por teclado, ARIA |
Bônus (até +2 pontos): - Deploy no GitHub Pages funcionando: +0,5 - Tema escuro implementado corretamente: +0,5 - AbortController na busca: +0,5 - Componentes reutilizáveis bem documentados: +0,5
18.5.4 — Roteiro de apresentação¶
A apresentação tem duração de 5 minutos e deve cobrir:
[0:00 – 0:30] Introdução
→ Nome, qual API foi escolhida e por quê
→ Qual o "problema" que a aplicação resolve para o usuário
[0:30 – 2:30] Demonstração ao vivo
→ Mostrar a listagem com dados reais da API
→ Executar uma busca ou filtro
→ Navegar para o detalhe de um item
→ Demonstrar o formulário com validação (incluir um erro propositalmente)
→ Mostrar os favoritos sendo adicionados e persistindo
[2:30 – 3:30] Aspectos técnicos
→ Mostrar a estrutura de arquivos no VS Code
→ Mostrar um trecho de código que você considera bem resolvido
→ Mostrar o score do Lighthouse
[3:30 – 4:30] Desafios e aprendizados
→ Qual foi a parte mais difícil de implementar?
→ O que você faria diferente com mais tempo?
[4:30 – 5:00] Perguntas
18.5.5 — Retrospectiva: o que aprendemos no ano letivo¶
Ao longo de 40 semanas e 18 capítulos, percorremos uma jornada completa pelo desenvolvimento front-end moderno:
1º Bimestre — A fundação: Partimos dos fundamentos da Web — como um navegador interpreta HTML, o papel do HTTP, o conceito de DOM — e construímos páginas estruturadas com HTML semântico, acessível e bem formado. Aprendemos que a qualidade do HTML determina a qualidade de tudo que vem depois: CSS, JavaScript e acessibilidade dependem de uma marcação bem estruturada.
2º Bimestre — A apresentação: Com CSS moderno, Flexbox, Grid e design responsivo, aprendemos a criar interfaces que funcionam em qualquer dispositivo. O Design System nos ensinou que consistência visual não é um acidente — é resultado de decisões sistemáticas sobre cores, tipografia e espaçamento. O Tailwind CSS mostrou como um framework pode acelerar o desenvolvimento sem sacrificar o controle.
3º Bimestre — O comportamento: JavaScript trouxe vida às páginas. Aprendemos a linguagem em profundidade — closures, promises, async/await — e aplicamos esses conceitos na manipulação do DOM, no tratamento de eventos e na construção de componentes interativos. Os jogos mostraram que os mesmos conceitos de lógica, estado e renderização se aplicam em contextos bem diferentes.
4º Bimestre — A integração: APIs tornaram as aplicações dinâmicas e conectadas ao mundo real. Módulos ES6 organizaram o código em camadas. Roteamento, estado global e persistência com localStorage deram à SPA a sensação de uma aplicação completa. O Projeto Final sintetizou tudo isso em algo que pode ser mostrado, usado e evoluído.
O que aprendemos que transcende as tecnologias: - Separação de responsabilidades é um investimento que se paga a cada mudança futura - Acessibilidade não é opcional — ela é parte da qualidade do software - Código legível é mais valioso do que código "inteligente" - Todo comportamento visual inesperado tem uma explicação técnica precisa - A melhor forma de aprender é construir
A jornada do front-end não termina aqui. Frameworks como React, Vue e Angular, TypeScript, testes automatizados, performance e segurança web são capítulos futuros. Mas todos eles serão mais fáceis de compreender porque você entende o que acontece por baixo.
Referências finais e recursos para continuar: - MDN Web Docs — referência técnica definitiva - web.dev — boas práticas de performance e acessibilidade (Google) - javascript.info — JavaScript em profundidade - CSS Tricks — técnicas avançadas de CSS - A11y Project — acessibilidade web - GitHub Student Developer Pack — ferramentas gratuitas para estudantes
Atividades — Capítulo 18¶
Não há quiz neste capítulo — o projeto final é a atividade avaliativa.
-
Entrega principal: repositório GitHub com o projeto completo, README com screenshots e link do deploy no GitHub Pages. (data definida pelo professor)
-
Apresentação: demonstração ao vivo de 5 minutos seguindo o roteiro da seção 18.5.4. (data definida pelo professor)
:material-arrow-left: Voltar ao Capítulo 17 — Integração Frontend + API
Programação Web 1 — IFAL — Bacharelado em Sistemas de Informação Material didático desenvolvido para o ano letivo 2026
Appendix – Reference Material¶
Use this section for:
- Cheatsheets
- Command summaries
- Reference tables
- Links to external resources
(Write your actual reference material here.)
Contracapa¶
Programação Web 1
Este livro faz parte de uma iniciativa de educação aberta.
Você pode:
- Lê-lo online no GitHub Pages ou impresso (PDF).
- Baixar ou clonar o repositório.
- Sugerir melhorias por meio de issues e pull requests.
Obrigado pela leitura!