Estrutura do documento
Todo documento HTML começa com a declaração <!DOCTYPE html>, que instrui o browser a renderizar no modo padrão (standards mode) e não no modo quirks herdado do IE. O atributo lang no elemento <html> é obrigatório para leitores de tela e motores de busca. O charset deve ser declarado antes de qualquer conteúdo de texto nos primeiros 1024 bytes.
<!DOCTYPE html>
<html lang="pt-BR" dir="ltr">
<head>
<meta charset="UTF-8" />
<!-- viewport garante renderização responsiva em mobile -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- preconnect ao CDN de fontes reduz latência -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<title>Título descritivo da página — Nome do Site</title>
<link rel="stylesheet" href="/styles/main.css" />
</head>
<body>
<!-- conteúdo -->
<script src="/scripts/app.js" defer></script>
</body>
</html>Atributos relevantes no <html>: lang (BCP 47, ex: pt-BR, en-US), dir (ltr ou rtl), data-theme (convenção para temas). No <head>: a ordem importa — charset e viewport primeiro, depois title, depois estilos, depois scripts.
Tags semânticas completas
Semântica é o significado do conteúdo, não sua aparência. Tags semânticas comunicam ao browser, leitores de tela e motores de busca o papel de cada bloco de conteúdo. Use <div> e <span> apenas quando nenhuma tag semântica se aplica.
<body>
<header>
<!-- cabeçalho do site (ou de uma section/article) -->
<nav aria-label="Navegação principal">
<ul>
<li><a href="/" aria-current="page">Início</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
</nav>
</header>
<main>
<!-- conteúdo principal — deve ser único por página -->
<article>
<!-- conteúdo autônomo e redistribuível (post, produto, card) -->
<header>
<h1>Título do artigo</h1>
<address>
Por <a href="/sobre">Rafael Marques</a>
</address>
<time datetime="2024-05-01T10:00:00-03:00">1 de maio de 2024</time>
</header>
<section>
<!-- agrupa conteúdo temático dentro de article ou main -->
<h2>Introdução</h2>
<p>Texto com <mark>trecho destacado</mark> por relevância.</p>
<figure>
<img src="grafico.png" alt="Gráfico mostrando crescimento de 40% em 2024" />
<figcaption>Crescimento anual 2023–2024</figcaption>
</figure>
</section>
<section>
<h2>Detalhes técnicos</h2>
<details>
<summary>Clique para expandir os detalhes</summary>
<p>Conteúdo colapsável nativo, sem JavaScript.</p>
</details>
</section>
</article>
<aside aria-label="Conteúdo relacionado">
<!-- conteúdo tangencialmente relacionado ao principal -->
<h2>Posts relacionados</h2>
</aside>
</main>
<footer>
<!-- rodapé do site ou da section/article -->
<p><small>© 2024 Rafael Marques. Todos os direitos reservados.</small></p>
</footer>
</body>Referência rápida de tags semânticas:
| Tag | Uso correto |
|---|---|
<header> | Cabeçalho do site ou de article/section |
<nav> | Bloco de links de navegação (principal, breadcrumb, paginação) |
<main> | Conteúdo principal — único por página, não pode estar dentro de article, aside, footer, header, nav |
<article> | Conteúdo independente e redistribuível |
<section> | Agrupa conteúdo com um tema em comum — sempre deve ter título |
<aside> | Conteúdo complementar (sidebar, notas, anúncios) |
<footer> | Rodapé do site ou da section mais próxima |
<figure> + <figcaption> | Imagem, diagrama, código ou mídia com legenda |
<time> | Data/hora legível por máquina via atributo datetime |
<address> | Informações de contato do autor ou dono do article |
<mark> | Trecho de texto destacado por relevância no contexto atual |
<details> + <summary> | Widget de disclosure nativo (acordeão) |
<dialog> | Diálogo modal ou não-modal nativo |
<search> | Região de busca (novo no HTML Living Standard) |
Headings e hierarquia
Os headings (<h1>–<h6>) definem o outline do documento — a estrutura hierárquica do conteúdo. Leitores de tela permitem navegar entre headings; motores de busca usam a hierarquia para entender relevância. A regra principal: nunca pule níveis (não vá de <h2> para <h4>). Em cada <article>, a hierarquia recomeça do <h1>.
<main>
<h1>Fundamentos de CSS</h1> <!-- um <h1> por page/article -->
<section>
<h2>Box Model</h2>
<section>
<h3>content-box vs border-box</h3>
</section>
<section>
<h3>Margin collapsing</h3>
</section>
</section>
<section>
<h2>Flexbox</h2>
<h3>Container properties</h3>
<h3>Item properties</h3>
</section>
</main>Regras de acessibilidade:
- Não use headings para estilização — use classes CSS
- Cada página deve ter exatamente um
<h1>com o tópico principal - A ordem deve ser lógica:
h1→h2→h3, sem pular - O texto do heading deve ser descritivo e único na página quando possível
Listas
Listas comunicam conjuntos de itens relacionados. <ul> para itens sem ordem relevante, <ol> para sequências ordenadas, <dl> para pares chave-valor (glossários, metadados).
<!-- Lista não-ordenada — items sem sequência -->
<ul>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
</ul>
<!-- Lista ordenada — sequência importa -->
<ol>
<li>Clone o repositório</li>
<li>Instale as dependências</li>
<li>Configure o .env</li>
<li>Inicie o servidor</li>
</ol>
<!-- ol com atributos especiais -->
<ol start="5" reversed type="I">
<!-- start: inicia em 5; reversed: contagem regressiva; type: I=romano maiúsculo -->
<li>Quinto passo</li>
<li>Sexto passo</li>
</ol>
<!-- Lista de definição — glossário, metadados -->
<dl>
<dt>HTML</dt>
<dd>HyperText Markup Language — linguagem de marcação da web</dd>
<dt>CSS</dt>
<dd>Cascading Style Sheets — linguagem de estilos</dd>
<!-- múltiplos dt para o mesmo dd -->
<dt>JS</dt>
<dt>JavaScript</dt>
<dd>Linguagem de programação interpretada da web</dd>
</dl>Links
O elemento <a> é o pilar da web. Além do href, os atributos rel e target têm impacto direto em segurança, SEO e experiência do usuário.
<!-- Link interno com fragmento -->
<a href="/sobre#contato">Seção de Contato</a>
<!-- Âncora na mesma página -->
<a href="#introducao">Ir para introdução</a>
<h2 id="introducao">Introdução</h2>
<!-- Link externo — noopener previne acesso ao window.opener (segurança) -->
<!-- noreferrer inclui noopener e omite o header Referer -->
<a href="https://github.com/rafaelmarques" target="_blank" rel="noopener noreferrer">
GitHub
</a>
<!-- nofollow — instrui crawlers a não seguir o link (links de anúncio, UGC) -->
<a href="https://patrocinador.com" rel="nofollow sponsored">Patrocinador</a>
<!-- Download de arquivo -->
<a href="/relatorio-2024.pdf" download="relatorio-anual-2024.pdf">
Baixar Relatório PDF
</a>
<!-- Protocolos especiais -->
<a href="tel:+5511999999999">Ligar agora</a>
<a href="mailto:contato@rafael.dev?subject=Olá&body=Mensagem">E-mail</a>
<a href="sms:+5511999999999">Enviar SMS</a>Valores de rel:
| Valor | Significado |
|---|---|
noopener | Impede acesso ao window.opener — sempre use com target="_blank" |
noreferrer | Omite Referer header + implica noopener |
nofollow | Instrui crawlers a não seguir (links pagos, UGC) |
sponsored | Marca link como patrocinado/afiliado |
ugc | Conteúdo gerado por usuário (fórum, comentário) |
canonical | Indica URL canônica (no <link>, não <a>) |
prefetch | Sugestão de pré-busca do recurso |
Imagens responsivas
Uma única imagem em tamanho grande desperdiça banda em mobile. srcset e sizes permitem ao browser escolher a imagem mais adequada ao dispositivo. <picture> permite formatos diferentes (WebP/AVIF com fallback para JPEG).
<!-- srcset com resolução — para imagens de tamanho fixo (logo, avatar) -->
<img
src="avatar-1x.jpg"
srcset="avatar-1x.jpg 1x, avatar-2x.jpg 2x, avatar-3x.jpg 3x"
alt="Foto de perfil de Rafael Marques"
width="64"
height="64"
/>
<!-- srcset com largura + sizes — para imagens de layout fluido -->
<img
src="hero-800.jpg"
srcset="
hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w,
hero-1600.jpg 1600w
"
sizes="
(max-width: 600px) 100vw,
(max-width: 1024px) 80vw,
1200px
"
alt="Paisagem do Vale do Silício ao amanhecer"
loading="lazy"
decoding="async"
fetchpriority="low"
/>
<!-- picture — formatos modernos com fallback -->
<picture>
<!-- AVIF: melhor compressão, suporte parcial -->
<source
type="image/avif"
srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w"
sizes="(max-width: 600px) 100vw, 80vw"
/>
<!-- WebP: boa compressão, amplo suporte -->
<source
type="image/webp"
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
sizes="(max-width: 600px) 100vw, 80vw"
/>
<!-- JPEG: fallback universal -->
<img
src="hero-800.jpg"
alt="Paisagem do Vale do Silício ao amanhecer"
width="1200"
height="600"
loading="lazy"
/>
</picture>Sempre defina width e height em imagens para evitar layout shift (CLS). Para a imagem LCP (maior da viewport), use loading="eager" e fetchpriority="high".
Vídeo e Áudio
O browser suporta múltiplos formatos via <source>. Legendas acessíveis são adicionadas com <track>. Nunca use autoplay sem muted — browsers bloqueiam autoplay com som.
<!-- Vídeo com múltiplos formatos e legendas -->
<video
controls
preload="metadata"
poster="thumbnail.jpg"
width="1280"
height="720"
aria-label="Tutorial de CSS Grid"
>
<source src="tutorial.av1.mp4" type="video/mp4; codecs=av01" />
<source src="tutorial.h264.mp4" type="video/mp4" />
<source src="tutorial.webm" type="video/webm" />
<!-- Legendas em português -->
<track
kind="subtitles"
src="legenda-pt.vtt"
srclang="pt"
label="Português"
default
/>
<!-- Legendas em inglês -->
<track
kind="subtitles"
src="legenda-en.vtt"
srclang="en"
label="English"
/>
<!-- Descrição para deficientes visuais -->
<track kind="descriptions" src="descricao-pt.vtt" srclang="pt" />
<p>Seu browser não suporta vídeo HTML5.
<a href="tutorial.h264.mp4">Baixar vídeo</a>
</p>
</video>
<!-- Vídeo de background — muted obrigatório para autoplay -->
<video autoplay muted loop playsinline aria-hidden="true">
<source src="bg-loop.mp4" type="video/mp4" />
</video>
<!-- Áudio -->
<audio controls preload="metadata">
<source src="podcast.opus" type="audio/ogg; codecs=opus" />
<source src="podcast.mp3" type="audio/mpeg" />
<a href="podcast.mp3">Baixar podcast</a>
</audio>Valores de preload: none (não carrega nada), metadata (carrega apenas duração/dimensões), auto (browser decide — pode baixar todo o arquivo).
Valores de <track kind>: subtitles (legendas), captions (legenda fechada com sons), descriptions (descrição de vídeo para deficientes visuais), chapters (capítulos), metadata (dados para scripts).
Tabelas
Tabelas são para dados tabulares — não para layout. Use <thead>, <tbody>, <tfoot> para estrutura, scope nos <th> para acessibilidade, e <caption> para descrever o propósito da tabela.
<table>
<caption>Vendas mensais por categoria — 2024</caption>
<!-- colgroup permite estilizar colunas inteiras -->
<colgroup>
<col span="1" />
<col span="3" style="background-color: #f8fafc;" />
</colgroup>
<thead>
<tr>
<!-- scope="col" associa header à coluna inteira -->
<th scope="col">Mês</th>
<th scope="col">Eletrônicos</th>
<th scope="col">Vestuário</th>
<th scope="col">Total</th>
</tr>
</thead>
<tbody>
<tr>
<!-- scope="row" associa header à linha inteira -->
<th scope="row">Janeiro</th>
<td>R$ 45.000</td>
<td>R$ 12.000</td>
<td>R$ 57.000</td>
</tr>
<tr>
<th scope="row">Fevereiro</th>
<td>R$ 38.000</td>
<td>R$ 15.000</td>
<td>R$ 53.000</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">Total</th>
<td>R$ 83.000</td>
<td>R$ 27.000</td>
<td>R$ 110.000</td>
</tr>
</tfoot>
</table>
<!-- Tabela com células mescladas -->
<table>
<thead>
<tr>
<th scope="col" rowspan="2">Produto</th>
<!-- colspan mescla colunas horizontalmente -->
<th scope="colgroup" colspan="2">Vendas</th>
</tr>
<tr>
<th scope="col">Q1</th>
<th scope="col">Q2</th>
</tr>
</thead>
</table>Formulários completos
Formulários HTML5 têm validação nativa e tipos de input que ativam teclados específicos em dispositivos móveis. O atributo autocomplete melhora a experiência ao preencher campos já conhecidos pelo browser.
<form
action="/api/cadastro"
method="POST"
enctype="multipart/form-data"
novalidate
id="form-cadastro"
>
<!-- Campos de texto -->
<label for="nome">Nome completo</label>
<input type="text" id="nome" name="nome"
autocomplete="name"
required minlength="3" maxlength="100"
placeholder="Rafael Marques" />
<label for="email">E-mail</label>
<input type="email" id="email" name="email"
autocomplete="email"
required
inputmode="email" />
<label for="senha">Senha</label>
<input type="password" id="senha" name="senha"
autocomplete="new-password"
required minlength="8"
pattern="^(?=.*[A-Z])(?=.*\d).{8,}$" />
<!-- Campos numéricos e de intervalo -->
<label for="idade">Idade</label>
<input type="number" id="idade" name="idade"
min="18" max="120" step="1"
inputmode="numeric" />
<label for="preco">Preço (R$)</label>
<input type="number" id="preco" name="preco"
min="0" max="99999" step="0.01"
inputmode="decimal" />
<label for="satisfacao">Satisfação: <output id="satisfacao-val">5</output>/10</label>
<input type="range" id="satisfacao" name="satisfacao"
min="1" max="10" step="1" value="5"
oninput="document.getElementById('satisfacao-val').value = this.value" />
<!-- Campos de data e hora -->
<label for="data-nasc">Data de nascimento</label>
<input type="date" id="data-nasc" name="nascimento"
autocomplete="bday"
min="1900-01-01" max="2010-12-31" />
<label for="hora-reuniao">Horário da reunião</label>
<input type="time" id="hora-reuniao" name="hora" step="1800" />
<label for="evento">Data e hora do evento</label>
<input type="datetime-local" id="evento" name="evento" />
<label for="mes-comp">Competência</label>
<input type="month" id="mes-comp" name="competencia" />
<!-- Campos especiais -->
<label for="site">Website</label>
<input type="url" id="site" name="url"
autocomplete="url"
placeholder="https://rafael.dev" />
<label for="telefone">Telefone</label>
<input type="tel" id="telefone" name="tel"
autocomplete="tel"
pattern="^\+?[0-9\s\-\(\)]{10,15}$"
inputmode="tel"
placeholder="+55 (11) 99999-9999" />
<label for="cor">Cor favorita</label>
<input type="color" id="cor" name="cor" value="#38bdf8" />
<!-- Upload de arquivo -->
<label for="avatar">Foto de perfil</label>
<input type="file" id="avatar" name="avatar"
accept="image/png, image/jpeg, image/webp"
multiple />
<!-- Datalist — autocompletar customizado -->
<label for="linguagem">Linguagem preferida</label>
<input type="text" id="linguagem" name="linguagem" list="linguagens" />
<datalist id="linguagens">
<option value="TypeScript" />
<option value="Python" />
<option value="Go" />
<option value="Rust" />
</datalist>
<!-- Select simples e múltiplo -->
<label for="pais">País</label>
<select id="pais" name="pais" autocomplete="country" required>
<option value="" disabled selected>Selecione...</option>
<optgroup label="América do Sul">
<option value="BR">Brasil</option>
<option value="AR">Argentina</option>
<option value="CO">Colômbia</option>
</optgroup>
<optgroup label="América do Norte">
<option value="US">Estados Unidos</option>
<option value="CA">Canadá</option>
</optgroup>
</select>
<label for="interesses">Interesses</label>
<select id="interesses" name="interesses" multiple size="4">
<option value="frontend">Frontend</option>
<option value="backend">Backend</option>
<option value="devops">DevOps</option>
<option value="dados">Dados</option>
</select>
<!-- Checkboxes e radios -->
<fieldset>
<legend>Plano</legend>
<label>
<input type="radio" name="plano" value="free" checked /> Gratuito
</label>
<label>
<input type="radio" name="plano" value="pro" /> Pro — R$ 29/mês
</label>
</fieldset>
<fieldset>
<legend>Notificações</legend>
<label>
<input type="checkbox" name="notif" value="email" checked /> E-mail
</label>
<label>
<input type="checkbox" name="notif" value="sms" /> SMS
</label>
</fieldset>
<!-- Textarea -->
<label for="bio">Biografia</label>
<textarea id="bio" name="bio"
rows="4" cols="50"
maxlength="500"
placeholder="Fale um pouco sobre você..."
autocomplete="off"></textarea>
<!-- Campos ocultos -->
<input type="hidden" name="csrf_token" value="abc123xyz" />
<input type="hidden" name="referrer" value="homepage" />
<!-- Meter — valor em intervalo conhecido -->
<label for="disco">Uso do disco:</label>
<meter id="disco" value="75" min="0" max="100" low="60" high="85" optimum="30">
75%
</meter>
<!-- Progress — progresso de tarefa -->
<label for="upload">Upload:</label>
<progress id="upload" value="45" max="100">45%</progress>
<!-- Output — resultado de cálculo -->
<output name="total" for="qtd preco">R$ 0,00</output>
<button type="submit">Cadastrar</button>
<button type="reset">Limpar</button>
</form>Atributos de formulário e autocomplete
O atributo autocomplete tem valores específicos que permitem ao browser preencher campos corretamente — essencial para experiência em mobile e acessibilidade.
<!-- Valores de autocomplete para dados pessoais -->
<input autocomplete="name" /> <!-- nome completo -->
<input autocomplete="given-name" /> <!-- primeiro nome -->
<input autocomplete="family-name" /> <!-- sobrenome -->
<input autocomplete="email" />
<input autocomplete="tel" />
<input autocomplete="bday" /> <!-- data de nascimento completa -->
<input autocomplete="bday-year" />
<!-- Endereço -->
<input autocomplete="street-address" />
<input autocomplete="address-line1" />
<input autocomplete="postal-code" />
<input autocomplete="country" />
<input autocomplete="country-name" />
<!-- Pagamento -->
<input autocomplete="cc-name" /> <!-- nome no cartão -->
<input autocomplete="cc-number" /> <!-- número do cartão -->
<input autocomplete="cc-exp" /> <!-- validade -->
<input autocomplete="cc-csc" /> <!-- CVV -->
<!-- Credenciais -->
<input autocomplete="username" />
<input autocomplete="current-password" />
<input autocomplete="new-password" />
<input autocomplete="one-time-code" /> <!-- OTP por SMS/email -->
<!-- Desligar autocomplete -->
<input autocomplete="off" />inputmode — controla o teclado virtual em dispositivos móveis:
| Valor | Teclado exibido |
|---|---|
text | Teclado padrão |
numeric | Apenas números (0–9) |
decimal | Números com vírgula/ponto decimal |
tel | Teclado de telefone |
email | Teclado com @, . |
url | Teclado com /, . |
search | Teclado com tecla “Buscar” |
none | Nenhum teclado |
Validação nativa — Constraint Validation API
O browser valida automaticamente com base nos atributos do campo. Use novalidate no form para assumir o controle com JavaScript, mantendo o benefício dos atributos para semântica.
<form id="meu-form" novalidate>
<input type="email" id="email" name="email" required />
<span id="email-erro" class="erro" aria-live="polite"></span>
<button type="submit">Enviar</button>
</form>
<script>
const form = document.getElementById('meu-form');
const emailInput = document.getElementById('email');
const emailErro = document.getElementById('email-erro');
// checkValidity() — retorna boolean sem exibir UI nativa
emailInput.addEventListener('blur', () => {
if (!emailInput.checkValidity()) {
// validity.valueMissing, validity.typeMismatch, validity.patternMismatch, etc.
if (emailInput.validity.valueMissing) {
emailErro.textContent = 'O e-mail é obrigatório.';
} else if (emailInput.validity.typeMismatch) {
emailErro.textContent = 'Digite um e-mail válido.';
}
emailInput.setAttribute('aria-invalid', 'true');
} else {
emailErro.textContent = '';
emailInput.removeAttribute('aria-invalid');
}
});
// setCustomValidity() — define mensagem de erro customizada
emailInput.addEventListener('input', () => {
if (emailInput.value.endsWith('@teste.com')) {
emailInput.setCustomValidity('E-mails @teste.com não são aceitos.');
} else {
emailInput.setCustomValidity(''); // limpa o erro customizado
}
});
form.addEventListener('submit', (e) => {
e.preventDefault();
// reportValidity() — exibe a UI nativa de validação
if (!form.reportValidity()) return;
// ... enviar formulário
});
</script>Propriedades do objeto validity:
| Propriedade | Quando é true |
|---|---|
valueMissing | Campo required vazio |
typeMismatch | Valor não corresponde ao type |
patternMismatch | Não satisfaz o pattern |
tooShort / tooLong | Viola minlength/maxlength |
rangeUnderflow / rangeOverflow | Viola min/max |
stepMismatch | Não é múltiplo do step |
customError | setCustomValidity() foi chamado com string não-vazia |
valid | Todos os constraints satisfeitos |
Acessibilidade ARIA completa
ARIA (Accessible Rich Internet Applications) complementa o HTML semântico para casos que o HTML não cobre. A primeira regra do ARIA: use HTML semântico nativo antes de recorrer ao ARIA.
<!-- Roles principais -->
<div role="alert">Mensagem de erro urgente (aria-live assertive implícito)</div>
<div role="status">Mensagem de status (aria-live polite implícito)</div>
<div role="dialog" aria-modal="true" aria-labelledby="modal-titulo">
<h2 id="modal-titulo">Confirmar exclusão</h2>
</div>
<nav role="navigation" aria-label="Breadcrumb">...</nav>
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel-1">Aba 1</button>
<button role="tab" aria-selected="false" aria-controls="panel-2">Aba 2</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">...</div>
<!-- Rótulos e descrições -->
<button aria-label="Fechar modal">✕</button>
<button aria-labelledby="titulo-secao">Ver todos</button>
<input id="senha" aria-describedby="senha-dica senha-erro" />
<span id="senha-dica">Mínimo 8 caracteres com letra maiúscula e número</span>
<span id="senha-erro" role="alert"></span>
<!-- Estados -->
<button aria-expanded="false" aria-controls="menu-dropdown">Menu</button>
<button aria-pressed="true">Negrito</button>
<input type="checkbox" aria-checked="mixed" /> <!-- indeterminado -->
<li aria-selected="true">Item selecionado na listbox</li>
<button aria-disabled="true">Desabilitado (focável)</button>
<!-- Ocultar do tree de acessibilidade -->
<svg aria-hidden="true" focusable="false">...</svg>
<span aria-hidden="true">•</span> <!-- separador visual -->
<!-- Live regions — anunciam mudanças dinâmicas -->
<!-- polite: anuncia quando o usuário parar de interagir -->
<div aria-live="polite" aria-atomic="true" id="notificacoes">
<!-- Conteúdo injetado via JS será anunciado -->
</div>
<!-- assertive: interrompe e anuncia imediatamente — use com parcimônia -->
<div aria-live="assertive" role="alert" id="erros-criticos">
</div>
<!-- aria-relevant: especifica o que anunciar (default: "additions text") -->
<div aria-live="polite" aria-relevant="additions removals">
</div>
<!-- Propriedades de relacionamento -->
<ul role="listbox" aria-owns="opcao-extra">
<li role="option">Opção 1</li>
</ul>
<li role="option" id="opcao-extra">Opção extra (fora do DOM pai)</li>Focus management e navegação por teclado
Navegação por teclado é fundamental para acessibilidade. Todos os elementos interativos devem ser acessíveis via teclado, com indicador visual de foco visível.
<!-- Skip link — pula navegação para o conteúdo principal -->
<a href="#main-content" class="skip-link">Pular para o conteúdo</a>
<nav>...</nav>
<main id="main-content">...</main>
<style>
.skip-link {
position: absolute;
top: -100%;
left: 0;
z-index: 9999;
padding: 0.5rem 1rem;
background: #0f172a;
color: #fff;
}
/* Aparece ao receber foco via teclado */
.skip-link:focus {
top: 0;
}
</style>
<!-- tabindex -->
<!-- tabindex="0" → entra na ordem natural de tabulação -->
<!-- tabindex="-1" → focável via script, mas não pelo Tab -->
<!-- tabindex="n" → ordem explícita (evitar — quebra fluxo natural) -->
<div role="button" tabindex="0"
onkeydown="if(e.key==='Enter'||e.key===' ')this.click()">
Elemento customizado clicável
</div>
<!-- :focus-visible — mostra outline apenas na navegação por teclado -->
<style>
/* Remove outline para mouse, mantém para teclado */
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 2px solid #38bdf8;
outline-offset: 2px;
border-radius: 2px;
}
</style>
<script>
// Prender foco dentro de um modal (focus trap)
function trapFocus(modalElement) {
const focusableSelectors = [
'a[href]', 'button:not([disabled])',
'input:not([disabled])', 'select', 'textarea',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
const focusableElements = [...modalElement.querySelectorAll(focusableSelectors)];
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
modalElement.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
lastFocusable.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastFocusable) {
firstFocusable.focus();
e.preventDefault();
}
}
});
}
</script>Meta tags completas
Meta tags controlam SEO, compartilhamento social, comportamento em mobile e configurações do browser.
<head>
<!-- Essenciais -->
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Título da Página — Máximo 60 caracteres — Nome do Site</title>
<!-- SEO básico -->
<meta name="description" content="Descrição da página entre 120 e 160 caracteres para aparecer corretamente no snippet dos buscadores." />
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large" />
<link rel="canonical" href="https://rafael.dev/sobre" />
<!-- Open Graph — WhatsApp, LinkedIn, Facebook, Slack -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://rafael.dev/sobre" />
<meta property="og:title" content="Sobre — Rafael Marques" />
<meta property="og:description" content="Desenvolvedor full-stack apaixonado por performance e UX." />
<meta property="og:image" content="https://rafael.dev/og/sobre.jpg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="pt_BR" />
<meta property="og:site_name" content="Rafael Marques" />
<!-- Twitter / X Cards -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@rafael_dev" />
<meta name="twitter:creator" content="@rafael_dev" />
<meta name="twitter:title" content="Sobre — Rafael Marques" />
<meta name="twitter:description" content="Desenvolvedor full-stack apaixonado por performance e UX." />
<meta name="twitter:image" content="https://rafael.dev/og/sobre.jpg" />
<!-- PWA e Mobile -->
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="manifest" href="/manifest.json" />
<!-- Favicons -->
<link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
</head>JSON-LD e Schema.org
Microdados estruturados ajudam o Google a entender o conteúdo e podem gerar rich snippets nos resultados de busca. JSON-LD no <script> é o método recomendado pelo Google.
<head>
<!-- Dados estruturados — artigo de blog -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Fundamentos de CSS Grid para devs modernos",
"description": "Aprenda CSS Grid do zero ao avançado com exemplos práticos.",
"image": "https://rafael.dev/blog/css-grid/cover.jpg",
"datePublished": "2024-05-01T10:00:00-03:00",
"dateModified": "2024-05-10T15:00:00-03:00",
"author": {
"@type": "Person",
"name": "Rafael Marques",
"url": "https://rafael.dev/sobre"
},
"publisher": {
"@type": "Organization",
"name": "Rafael Marques",
"logo": {
"@type": "ImageObject",
"url": "https://rafael.dev/logo.png"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://rafael.dev/blog/css-grid"
}
}
</script>
<!-- Breadcrumb -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Início", "item": "https://rafael.dev" },
{ "@type": "ListItem", "position": 2, "name": "Blog", "item": "https://rafael.dev/blog" },
{ "@type": "ListItem", "position": 3, "name": "CSS Grid" }
]
}
</script>
</head>Web Storage, IndexedDB e Clipboard
// localStorage — persiste entre sessões (mesmo após fechar o browser)
localStorage.setItem('tema', 'escuro');
const tema = localStorage.getItem('tema'); // "escuro"
localStorage.removeItem('tema');
localStorage.clear();
// sessionStorage — limpo ao fechar a aba
sessionStorage.setItem('passo-wizard', '3');
// Armazenar objetos (serialização necessária)
localStorage.setItem('usuario', JSON.stringify({ id: 1, nome: 'Rafael' }));
const usuario = JSON.parse(localStorage.getItem('usuario'));
// IndexedDB — banco de dados no browser, suporta objetos complexos
const request = indexedDB.open('meu-app', 1);
request.onupgradeneeded = (e) => {
const db = e.target.result;
const store = db.createObjectStore('produtos', { keyPath: 'id', autoIncrement: true });
store.createIndex('nome', 'nome', { unique: false });
};
request.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction('produtos', 'readwrite');
const store = tx.objectStore('produtos');
store.add({ nome: 'Notebook', preco: 3500 });
};
// Clipboard API
// Copiar texto
await navigator.clipboard.writeText('Texto copiado!');
// Copiar imagem
const blob = await fetch('/imagem.png').then(r => r.blob());
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
]);
// Ler da área de transferência (requer permissão)
const texto = await navigator.clipboard.readText();IntersectionObserver, ResizeObserver, MutationObserver
Observers permitem reagir a mudanças no DOM e no viewport sem polling. São a base para animações on-scroll, lazy loading, e detecção de mudanças de layout.
// IntersectionObserver — detecta quando elemento entra/sai da viewport
const io = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visivel');
// Parar de observar após animar (performance)
observer.unobserve(entry.target);
}
});
},
{
root: null, // null = viewport; ou um elemento scrollável
rootMargin: '0px 0px -100px 0px', // margem negativa no bottom = anima antes de chegar
threshold: [0, 0.25, 0.5, 0.75, 1], // callbacks em 0%, 25%, 50%, 75% e 100%
}
);
document.querySelectorAll('.animar-entrada').forEach(el => io.observe(el));
// Lazy loading manual para imagens sem suporte nativo
document.querySelectorAll('img[data-src]').forEach(img => {
io.observe(img);
});
// No callback:
// entry.intersectionRatio — porcentagem visível (0 a 1)
// entry.boundingClientRect — posição/tamanho do elemento
// entry.rootBounds — viewport bounds
// entry.isIntersecting — boolean
// ResizeObserver — detecta mudanças de tamanho de um elemento
const ro = new ResizeObserver(entries => {
for (const entry of entries) {
const { inlineSize, blockSize } = entry.contentBoxSize[0];
console.log(`Largura: ${inlineSize}px, Altura: ${blockSize}px`);
// borderBoxSize[0] inclui border e padding
// contentBoxSize[0] exclui border e padding
}
});
ro.observe(document.getElementById('sidebar'));
// MutationObserver — detecta mudanças no DOM
const mo = new MutationObserver(mutations => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
console.log('Filhos adicionados:', mutation.addedNodes);
console.log('Filhos removidos:', mutation.removedNodes);
}
if (mutation.type === 'attributes') {
console.log(`Atributo "${mutation.attributeName}" mudou`);
}
}
});
mo.observe(document.getElementById('lista-dinamica'), {
childList: true, // observa adição/remoção de filhos
subtree: true, // observa toda a subárvore
attributes: true, // observa mudanças de atributos
characterData: true, // observa mudanças em texto
attributeOldValue: true, // inclui valor anterior nos mutations
});
mo.disconnect(); // para de observarFetch API e AbortController
A Fetch API substitui o XMLHttpRequest com uma interface baseada em Promises. AbortController permite cancelar requisições (ex: ao navegar para outra página).
// Fetch básico com tratamento de erro
async function fetchDados(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// Fetch com todas as opções
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // timeout de 5s
try {
const response = await fetch('/api/usuarios', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
'X-Request-ID': crypto.randomUUID(),
},
body: JSON.stringify({ nome: 'Rafael', email: 'r@r.dev' }),
credentials: 'include', // envia cookies — 'omit' | 'same-origin' | 'include'
cache: 'no-cache', // 'default' | 'no-cache' | 'reload' | 'force-cache'
mode: 'cors', // 'cors' | 'same-origin' | 'no-cors'
redirect: 'follow', // 'follow' | 'manual' | 'error'
signal: controller.signal, // AbortController
});
clearTimeout(timeoutId);
// Lendo o body de formas diferentes
const json = await response.json();
const texto = await response.text();
const blob = await response.blob();
const buffer = await response.arrayBuffer();
const form = await response.formData();
// Streaming — lê o body em chunks (útil para SSE, downloads grandes)
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
console.log(chunk);
}
} catch (err) {
if (err.name === 'AbortError') {
console.log('Requisição cancelada (timeout ou navegação)');
} else {
throw err;
}
}
// Cancelar ao desmontar componente / navegar
window.addEventListener('beforeunload', () => controller.abort());Web Workers e Service Workers
Workers permitem executar JavaScript em threads separadas. Web Workers para cálculos pesados; Service Workers para cache offline e interceptação de requisições.
// Web Worker — cálculo pesado sem travar a UI
// main.js
const worker = new Worker('/workers/calculos.js');
worker.postMessage({ tipo: 'calcular-primos', limite: 1000000 });
worker.onmessage = (e) => {
console.log('Resultado:', e.data.resultado);
};
worker.onerror = (e) => {
console.error('Erro no worker:', e.message);
};
// Para encerrar
// worker.terminate();
// calculos.js (no worker)
self.onmessage = (e) => {
if (e.data.tipo === 'calcular-primos') {
const primos = calcularPrimos(e.data.limite);
self.postMessage({ resultado: primos });
}
};
// Service Worker básico — cache-first strategy
// Registrar no main
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
const reg = await navigator.serviceWorker.register('/sw.js');
console.log('SW registrado:', reg.scope);
});
}
// sw.js
const CACHE_NAME = 'meu-app-v1';
const ASSETS = ['/', '/styles/main.css', '/scripts/app.js'];
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
);
self.skipWaiting();
});
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
)
);
self.clients.claim();
});
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then(cached => cached || fetch(e.request))
);
});WebSocket
WebSocket mantém uma conexão bidirecional persistente entre cliente e servidor — ideal para chat, notificações em tempo real, dashboards ao vivo.
const ws = new WebSocket('wss://api.rafael.dev/ws');
// Eventos do ciclo de vida
ws.onopen = (e) => {
console.log('Conectado');
ws.send(JSON.stringify({ tipo: 'autenticar', token: localStorage.getItem('token') }));
};
ws.onmessage = (e) => {
const dados = JSON.parse(e.data);
console.log('Mensagem recebida:', dados);
if (dados.tipo === 'nova-mensagem') {
exibirMensagem(dados.payload);
}
};
ws.onerror = (e) => {
console.error('Erro WebSocket:', e);
};
ws.onclose = (e) => {
console.log(`Conexão fechada. Código: ${e.code}, Motivo: ${e.reason}`);
// Reconectar com backoff exponencial
setTimeout(reconectar, 1000 * Math.min(reconnectAttempts++, 30));
};
// Verificar estado antes de enviar
// ws.readyState: CONNECTING(0), OPEN(1), CLOSING(2), CLOSED(3)
function enviarMensagem(texto) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ tipo: 'mensagem', texto }));
}
}
// Fechar graciosamente
ws.close(1000, 'Usuário deslogou');Performance: Resource Hints e atributos de carregamento
Resource hints informam ao browser para iniciar operações de rede antes que o recurso seja solicitado pelo HTML. Diminuem a latência percebida sem bloquear o rendering.
<head>
<!-- preconnect — abre conexão TCP+TLS antecipadamente para domínio externo -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- dns-prefetch — apenas resolução DNS (mais leve que preconnect) -->
<link rel="dns-prefetch" href="https://cdn.rafael.dev" />
<!-- preload — carrega recurso crítico antes do parser chegar nele -->
<!-- Usar para: fontes, CSS crítico, imagem LCP, scripts essenciais -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/images/hero.webp" as="image" fetchpriority="high" />
<link rel="preload" href="/scripts/lcp-component.js" as="script" />
<!-- prefetch — baixa recurso provável na próxima navegação (prioridade baixa) -->
<link rel="prefetch" href="/sobre" as="document" />
<link rel="prefetch" href="/images/sobre-hero.webp" as="image" />
<!-- modulepreload — preload específico para ES modules -->
<link rel="modulepreload" href="/scripts/app.js" />
</head>
<!-- defer vs async para scripts -->
<!-- defer: baixa em paralelo, executa após parsing do HTML (em ordem) — preferido -->
<script src="/scripts/analytics.js" defer></script>
<!-- async: baixa e executa assim que possível, sem garantia de ordem -->
<script src="/scripts/widget-independente.js" async></script>
<!-- type="module": implicitamente defer -->
<script type="module" src="/scripts/app.js"></script>
<!-- Imagens: loading e fetchpriority -->
<!-- loading="lazy": adia carregamento até próximo da viewport -->
<img src="foto.jpg" alt="..." loading="lazy" decoding="async" />
<!-- fetchpriority: controla prioridade de carregamento -->
<!-- "high" para imagem LCP, "low" para abaixo do fold -->
<img src="hero.webp" alt="..." fetchpriority="high" loading="eager" />Web Components
Web Components são elementos HTML customizados encapsulados. Compostos de três APIs: Custom Elements, Shadow DOM e HTML Templates.
// Custom Element — extend HTMLElement
class ToastNotification extends HTMLElement {
// Atributos a observar para chamadas de attributeChangedCallback
static observedAttributes = ['message', 'type', 'duration'];
constructor() {
super();
// Shadow DOM — encapsula estilos e DOM
this.shadow = this.attachShadow({ mode: 'open' });
}
// Ciclo de vida
connectedCallback() {
// Chamado quando o elemento é inserido no DOM
this.render();
const duration = parseInt(this.getAttribute('duration') || '3000');
setTimeout(() => this.remove(), duration);
}
disconnectedCallback() {
// Chamado quando o elemento é removido do DOM
clearTimeout(this._timer);
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) this.render();
}
render() {
const message = this.getAttribute('message') || '';
const type = this.getAttribute('type') || 'info';
this.shadow.innerHTML = `
<style>
/* Estilos encapsulados — não vazam para fora */
:host {
display: block;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
font-family: system-ui, sans-serif;
}
:host([type="error"]) { background: #fee2e2; color: #991b1b; }
:host([type="success"]) { background: #dcfce7; color: #166534; }
:host([type="info"]) { background: #dbeafe; color: #1d4ed8; }
</style>
<p>${message}</p>
`;
}
}
customElements.define('toast-notification', ToastNotification);
// Uso no HTML
// <toast-notification message="Salvo com sucesso!" type="success" duration="4000"></toast-notification>
// HTML Template — define estrutura reutilizável sem renderizar
// <template id="card-template">
// <div class="card">
// <slot name="titulo">Título padrão</slot>
// <slot>Conteúdo padrão</slot>
// </div>
// </template>
// Clonar e instanciar o template
const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);
document.body.appendChild(clone);Dialog e Popover API
<dialog> é o elemento nativo para modais e diálogos. A Popover API (nova, 2023+) permite tooltips, dropdowns e painéis flutuantes sem JavaScript para posicionamento.
<!-- Dialog modal nativo -->
<dialog id="modal-confirmar">
<h2>Confirmar exclusão</h2>
<p>Esta ação não pode ser desfeita. Deseja continuar?</p>
<form method="dialog"> <!-- method="dialog" fecha o dialog sem submit -->
<button value="cancelar">Cancelar</button>
<button value="confirmar">Confirmar</button>
</form>
</dialog>
<button onclick="document.getElementById('modal-confirmar').showModal()">
Abrir modal
</button>
<script>
const dialog = document.getElementById('modal-confirmar');
// showModal() — abre como modal (com backdrop, foco preso, fecha com Esc)
// show() — abre como não-modal
dialog.showModal();
// Lendo o valor retornado pelo form[method="dialog"]
dialog.addEventListener('close', () => {
if (dialog.returnValue === 'confirmar') {
executarExclusao();
}
});
// Fechar programaticamente
dialog.close('cancelar');
</script>
<!-- Estilizando o backdrop -->
<style>
dialog::backdrop {
background: rgb(0 0 0 / 0.5);
backdrop-filter: blur(4px);
}
</style>
<!-- Popover API — sem JavaScript para abertura/fechamento básico -->
<button popovertarget="meu-menu">Abrir menu</button>
<div id="meu-menu" popover>
<!-- popover="auto": fecha ao clicar fora ou pressionar Esc -->
<!-- popover="manual": só fecha via JavaScript -->
<ul>
<li><a href="/perfil">Perfil</a></li>
<li><a href="/config">Configurações</a></li>
<li><button popovertarget="meu-menu" popovertargetaction="hide">Fechar</button></li>
</ul>
</div>
<script>
// Controle programático do popover
const popover = document.getElementById('meu-menu');
popover.showPopover();
popover.hidePopover();
popover.togglePopover();
</script>Versões — HTML 4.01 ao Living Standard
| Versão | Ano | Principais novidades |
|---|---|---|
| HTML 4.01 | 1999 | Padrão dominante por anos; <table> para layout; <font>, <center>, <frame> em uso; separação incipiente de conteúdo e estilo |
| XHTML 1.0 | 2000 | HTML reescrito com regras XML (tags fechadas, lowercase, aspas obrigatórias); transitório, nunca amplamente adotado |
| XHTML 1.1 | 2001 | Modularização do XHTML; strict, sem elementos de apresentação |
| HTML5 (W3C draft) | 2008–2012 | Tags semânticas (header, main, article, section, aside, nav, footer); <canvas>, <video>, <audio>; formulários melhorados (tipos de input, validação nativa); APIs nativas (localStorage, Geolocation, WebSocket, Web Worker, drag-and-drop) |
| HTML5 (W3C Rec.) | 2014 | Especificação oficialmente recomendada; remoção de <frame>, <font>, <center>, <big>, <strike> |
| HTML 5.1 | 2016 | <picture>, <details>, <summary>, atributos sizes e srcset; pequenas melhorias |
| HTML 5.2 | 2017 | <dialog> nativo; Payment Request API mencionada; melhorias em acessibilidade |
| HTML Living Standard | 2019–hoje | Especificação contínua mantida pelo WHATWG (sem versões numeradas); novidades recentes: <search>, Popover API, fetchpriority, loading="lazy" para iframes, Declarative Shadow DOM, blockquote[cite] semântica refinada |