A maioria dos formulários HTML que encontro em produção usa três tipos de input: text, password e submit. Só isso. Enquanto isso, o HTML tem tipos de input ricos, validação de restrições e recursos de acessibilidade há anos — tudo disponível sem JavaScript e sem npm install. Vamos usá-los.

Tipos de Input que Vale Conhecer

Cada tipo de input faz trabalho real para você: aciona o teclado mobile correto, fornece validação integrada e comunica a intenção para tecnologias assistivas. Aqui estão os que aparecem constantemente em projetos reais:

html
<!-- Email: validates format, shows email keyboard on mobile -->
<input type="email" name="email" autocomplete="email">

<!-- Phone: shows numeric keypad on mobile -->
<input type="tel" name="phone" autocomplete="tel" pattern="[0-9]{10,15}">

<!-- Number: spin buttons, min/max/step validation -->
<input type="number" name="quantity" min="1" max="99" step="1" value="1">

<!-- Date: native date picker (no library needed) -->
<input type="date" name="birthdate" min="1900-01-01" max="2026-12-31">

<!-- Range: slider with min/max/step -->
<input type="range" name="volume" min="0" max="100" step="5" value="50">

<!-- Color: native color picker -->
<input type="color" name="theme_color" value="#0066cc">

<!-- File: single or multiple file upload -->
<input type="file" name="resume" accept=".pdf,.doc,.docx">
<input type="file" name="photos" accept="image/*" multiple>
Dica útil: type="date" retorna o valor no formato YYYY-MM-DD independentemente do locale do usuário — que é exatamente o que você quer ao enviar para um servidor. Nunca faça parse de datas de inputs type="text" se puder evitar.

Associação de Labels — Faça Corretamente

Labels não são decoração opcional. São o principal meio pelo qual leitores de tela identificam inputs, e clicar em um label deve focar o input associado. Existem duas abordagens válidas:

html
<!-- Method 1: for/id association (most common, most flexible) -->
<label for="email">Email address</label>
<input type="email" id="email" name="email">

<!-- Method 2: wrapping label (no id needed) -->
<label>
  Email address
  <input type="email" name="email">
</label>

<!-- Wrong: placeholder is NOT a label -->
<input type="email" name="email" placeholder="Email address">
<!-- placeholder disappears when the user starts typing — terrible UX for screen readers -->

A abordagem for/id geralmente é preferível porque mantém labels e inputs desacoplados no DOM, o que é mais fácil de estilizar. Nunca confie no placeholder como substituto de label — falha para usuários com deficiências cognitivas e desaparece no momento em que alguém começa a digitar. O tutorial de labels W3C WAI e a orientação de controles de formulário do WebAIM cobrem a justificativa de acessibilidade com mais profundidade.

Validação de Restrições Integrada

A validação de restrições integrada do HTML é executada antes do envio do formulário e é genuinamente poderosa. Você obtém muito comportamento útil de graça:

html
<form>
  <!-- required: field must have a value -->
  <label for="fullname">Full name</label>
  <input type="text" id="fullname" name="fullname" required>

  <!-- minlength/maxlength: character count constraints -->
  <label for="username">Username (3–20 chars)</label>
  <input type="text" id="username" name="username"
         minlength="3" maxlength="20" required>

  <!-- pattern: regex-based validation -->
  <label for="postcode">UK Postcode</label>
  <input type="text" id="postcode" name="postcode"
         pattern="[A-Z]{1,2}[0-9][0-9A-Z]?s?[0-9][A-Z]{2}"
         title="Enter a valid UK postcode (e.g. SW1A 1AA)"
         required>

  <!-- min/max on date -->
  <label for="checkin">Check-in date</label>
  <input type="date" id="checkin" name="checkin"
         min="2026-04-16" required>

  <button type="submit">Submit</button>
</form>
  • required. O campo deve ter um valor. Em checkboxes, deve estar marcado.
  • pattern. Uma regex que o valor deve corresponder. Sempre adicione um atributo title — ele se torna o texto do tooltip de validação e dá aos usuários uma dica sobre o formato esperado.
  • minlength / maxlength. Contagem de caracteres para inputs de texto. maxlength trunca silenciosamente o input; minlength só valida no envio.
  • min / max. Limites numéricos ou de data. Funciona em inputs number, date, range e time.
  • step. Define incrementos válidos. step="0.01" em um campo de moeda permite centavos. step="any" desativa completamente a verificação de passo.

fieldset e legend para Agrupamento

Quando você tem um grupo de inputs relacionados — especialmente botões de rádio ou checkboxes — <fieldset> e <legend> os agrupam semanticamente. Leitores de tela leem a legenda antes de cada input no grupo, para que os usuários sempre saibam a qual pergunta um botão de rádio responde.

html
<form>
  <fieldset>
    <legend>Preferred contact method</legend>

    <label>
      <input type="radio" name="contact" value="email" checked>
      Email
    </label>
    <label>
      <input type="radio" name="contact" value="phone">
      Phone
    </label>
    <label>
      <input type="radio" name="contact" value="post">
      Post
    </label>
  </fieldset>

  <fieldset>
    <legend>Notification preferences</legend>

    <label>
      <input type="checkbox" name="notify_new_posts" value="1">
      New articles
    </label>
    <label>
      <input type="checkbox" name="notify_replies" value="1">
      Replies to my comments
    </label>
  </fieldset>
</form>

A API de Validação de Restrições

A API de Validação de Restrições fornece acesso JavaScript à mesma lógica de validação que o navegador usa nativamente. Isso permite acionar a validação programaticamente e definir mensagens de erro personalizadas sem bloquear o envio:

js
const usernameInput = document.getElementById('username');

// Check if a single field is valid
console.log(usernameInput.checkValidity()); // true or false
console.log(usernameInput.validity.tooShort); // true if below minlength
console.log(usernameInput.validity.patternMismatch); // true if pattern fails

// Set a custom error message (shows in the browser's native validation tooltip)
usernameInput.setCustomValidity('That username is already taken.');
usernameInput.reportValidity(); // triggers the tooltip immediately

// Clear a custom error (important — once set, it sticks until cleared)
usernameInput.setCustomValidity('');

// Validate on the fly as the user types (async example — username availability)
usernameInput.addEventListener('input', async () => {
  const value = usernameInput.value;
  if (value.length < 3) return; // let minlength handle this

  const response = await fetch(`/api/check-username?q=${encodeURIComponent(value)}`);
  const { available } = await response.json();

  usernameInput.setCustomValidity(available ? '' : 'Username is already taken.');
});

Observe o detalhe crítico: uma vez que você chama setCustomValidity() com uma string não vazia, o campo fica permanentemente inválido até que você o limpe chamando setCustomValidity(''). Esquecer o passo de limpeza é o bug mais comum ao usar esta API.

novalidate para Validação JS Personalizada

Se você está construindo uma UI de validação personalizada com mensagens de erro estilizadas (não os tooltips nativos do navegador), adicione novalidate ao formulário. Isso desativa a validação nativa do navegador enquanto mantém a API de Validação de Restrições disponível para suas próprias verificações:

html
<form id="signup-form" novalidate>
  <div class="field">
    <label for="signup-email">Email</label>
    <input type="email" id="signup-email" name="email" required>
    <span class="error" aria-live="polite"></span>
  </div>

  <div class="field">
    <label for="signup-password">Password</label>
    <input type="password" id="signup-password" name="password" minlength="8" required>
    <span class="error" aria-live="polite"></span>
  </div>

  <button type="submit">Create account</button>
</form>
js
const form = document.getElementById('signup-form');

form.addEventListener('submit', (e) => {
  e.preventDefault();

  let isValid = true;

  form.querySelectorAll('input').forEach((input) => {
    const errorEl = input.nextElementSibling;

    if (!input.checkValidity()) {
      isValid = false;
      errorEl.textContent = input.validationMessage;
      input.setAttribute('aria-invalid', 'true');
    } else {
      errorEl.textContent = '';
      input.removeAttribute('aria-invalid');
    }
  });

  if (isValid) {
    form.submit();
  }
});

autocomplete e aria-describedby

Dois atributos que melhoram significativamente a UX do formulário e são fáceis de ignorar:

html
<!-- autocomplete: tells browsers/password managers what the field is for -->
<input type="text"     name="fname"    autocomplete="given-name">
<input type="text"     name="lname"    autocomplete="family-name">
<input type="email"    name="email"    autocomplete="email">
<input type="tel"      name="phone"    autocomplete="tel">
<input type="password" name="password" autocomplete="current-password">
<input type="password" name="new_pass" autocomplete="new-password">
<input type="text"     name="cc"       autocomplete="cc-number">

<!-- aria-describedby: links an input to a hint or error message -->
<label for="pass">Password</label>
<input
  type="password"
  id="pass"
  name="password"
  minlength="8"
  aria-describedby="pass-hint"
  required
>
<span id="pass-hint">Must be at least 8 characters.</span>

O atributo autocomplete usa valores de token padronizados definidos pela especificação WHATWG. Quando você usa o token correto, navegadores e gerenciadores de senha podem preencher campos automaticamente de forma confiável. aria-describedby associa uma dica ou mensagem de erro a um input — leitores de tela leem a descrição após o label do campo, tornando as restrições audíveis antes mesmo de o usuário começar a digitar.

Um Formulário de Login Acessível Completo

Aqui está tudo combinado — um formulário de login que funciona para usuários de teclado, leitores de tela e usuários mobile, usando apenas HTML e uma pequena quantidade de JavaScript:

html
<form id="login-form" novalidate>
  <h2>Sign in</h2>

  <div class="field">
    <label for="login-email">Email address</label>
    <input
      type="email"
      id="login-email"
      name="email"
      autocomplete="email"
      aria-describedby="email-error"
      required
    >
    <span id="email-error" class="error" aria-live="polite" role="alert"></span>
  </div>

  <div class="field">
    <label for="login-password">Password</label>
    <input
      type="password"
      id="login-password"
      name="password"
      autocomplete="current-password"
      aria-describedby="password-error"
      required
    >
    <span id="password-error" class="error" aria-live="polite" role="alert"></span>
    <a href="/forgot-password">Forgot password?</a>
  </div>

  <label class="inline">
    <input type="checkbox" name="remember" value="1">
    Keep me signed in for 30 days
  </label>

  <button type="submit">Sign in</button>
</form>

Pontos-chave: aria-live="polite" nos spans de erro significa que leitores de tela anunciarão erros quando aparecerem sem interromper o que o usuário está ouvindo. role="alert" reforça isso para leitores de tela mais antigos. Os valores de autocomplete correspondem ao que os gerenciadores de senha esperam, então o preenchimento automático funciona corretamente na primeira tentativa.

Ferramentas para Ajudar

Ao construir e testar formulários HTML, o Validador HTML captura erros estruturais como labels ausentes e IDs duplicados antes que os usuários o façam. Para limpeza geral de HTML, o Formatador HTML mantém sua marcação legível. A referência do elemento input no MDN é a documentação mais completa para todos os tipos de input e seus atributos.

Conclusão

Formulários HTML têm muito mais capacidade integrada do que a maioria dos projetos usa. Escolher o tipo de input correto dá a você teclados mobile, validação e significado semântico de graça. Associação adequada de labels e aria-describedby tornam os formulários navegáveis sem mouse. A API de Validação de Restrições fornece ganchos JavaScript na validação nativa sem lutar com o navegador. Junte essas peças e você terá formulários que funcionam para todos — antes de escrever uma única linha de lógica de validação personalizada.