Frontend

HTML5

Referência completa de HTML5 — semântica, formulários, validação nativa, ARIA, Web APIs, Web Components, performance e meta tags

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:

TagUso 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: h1h2h3, 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>

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:

ValorSignificado
noopenerImpede acesso ao window.opener — sempre use com target="_blank"
noreferrerOmite Referer header + implica noopener
nofollowInstrui crawlers a não seguir (links pagos, UGC)
sponsoredMarca link como patrocinado/afiliado
ugcConteúdo gerado por usuário (fórum, comentário)
canonicalIndica URL canônica (no <link>, não <a>)
prefetchSugestã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:

ValorTeclado exibido
textTeclado padrão
numericApenas números (0–9)
decimalNúmeros com vírgula/ponto decimal
telTeclado de telefone
emailTeclado com @, .
urlTeclado com /, .
searchTeclado com tecla “Buscar”
noneNenhum 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:

PropriedadeQuando é true
valueMissingCampo required vazio
typeMismatchValor não corresponde ao type
patternMismatchNão satisfaz o pattern
tooShort / tooLongViola minlength/maxlength
rangeUnderflow / rangeOverflowViola min/max
stepMismatchNão é múltiplo do step
customErrorsetCustomValidity() foi chamado com string não-vazia
validTodos 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 observar

Fetch 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ãoAnoPrincipais novidades
HTML 4.011999Padrão dominante por anos; <table> para layout; <font>, <center>, <frame> em uso; separação incipiente de conteúdo e estilo
XHTML 1.02000HTML reescrito com regras XML (tags fechadas, lowercase, aspas obrigatórias); transitório, nunca amplamente adotado
XHTML 1.12001Modularização do XHTML; strict, sem elementos de apresentação
HTML5 (W3C draft)2008–2012Tags 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.)2014Especificação oficialmente recomendada; remoção de <frame>, <font>, <center>, <big>, <strike>
HTML 5.12016<picture>, <details>, <summary>, atributos sizes e srcset; pequenas melhorias
HTML 5.22017<dialog> nativo; Payment Request API mencionada; melhorias em acessibilidade
HTML Living Standard2019–hojeEspecificaçã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