Programování

Formuláře a validace v HTML a JavaScriptu bez zbytečných knihoven

Nativní HTML validace, Constraint Validation API, vlastní chybové hlášky, přístupné chyby a proč musíte vždy validovat i na serveru.

12. 10. 2026 · 5 min čtení

Dobře udělaný formulář uživatele neotravuje, srozumitelně řekne, co je špatně, a funguje i na mobilu. Velkou část práce přitom zvládne samotné HTML. JavaScript přidáte jen tam, kde nativní chování nestačí.

Začněte správnými typy polí

<form id="signup" novalidate>
  <label for="email">E-mail</label>
  <input id="email" name="email" type="email" autocomplete="email" required>

  <label for="phone">Telefon</label>
  <input id="phone" name="phone" type="tel" autocomplete="tel"
         pattern="(\+420)? ?[0-9]{3} ?[0-9]{3} ?[0-9]{3}">

  <label for="age">Věk</label>
  <input id="age" name="age" type="number" min="15" max="120" inputmode="numeric">

  <label for="password">Heslo</label>
  <input id="password" name="password" type="password"
         autocomplete="new-password" minlength="10" required>

  <button type="submit">Registrovat</button>
</form>

Co tím získáte zadarmo:

  • type="email" / tel / number – mobil ukáže vhodnou klávesnici,
  • autocomplete – prohlížeč a správce hesel vyplní údaje (seznam hodnot je ve specifikaci HTML: email, tel, given-name, postal-code, new-password, one-time-code…),
  • required, minlength, min, max, pattern – validace bez řádku JavaScriptu.

Každé pole potřebuje <label> propojený přes for/id. Placeholder label nenahrazuje – zmizí při psaní a čtečky obrazovky ho nemusí přečíst.

Proč novalidate?

Nativní bubliny s chybami vypadají v každém prohlížeči jinak a nedají se stylovat. Atribut novalidate vypne jejich automatické zobrazení, ale pravidla zůstávají aktivní – můžeme se jich ptát z JavaScriptu přes Constraint Validation API a chyby zobrazit po svém.

Constraint Validation API

Každé pole má vlastnost validity s přehledem, co je špatně:

Vlastnost Kdy je true
valueMissing prázdné povinné pole
typeMismatch neplatný e-mail nebo URL
patternMismatch neodpovídá pattern
tooShort / tooLong porušení minlength / maxlength
rangeUnderflow / rangeOverflow menší než min / větší než max
customError nastaveno přes setCustomValidity()
valid vše v pořádku

Vlastní hlášky v češtině

const form = document.querySelector('#signup');

const messages = {
  valueMissing: 'Toto pole je povinné.',
  typeMismatch: 'Zadejte platný e-mail.',
  patternMismatch: 'Zadejte telefon ve tvaru 777 123 456.',
  tooShort: f => `Zadejte alespoň ${f.minLength} znaků.`,
  rangeUnderflow: f => `Minimální hodnota je ${f.min}.`,
  rangeOverflow: f => `Maximální hodnota je ${f.max}.`,
};

function getMessage(field) {
  for (const key in messages) {
    if (field.validity[key]) {
      const m = messages[key];
      return typeof m === 'function' ? m(field) : m;
    }
  }
  return field.validationMessage;   // záložní text prohlížeče
}

function showError(field) {
  const errorId = `${field.id}-error`;
  let error = document.getElementById(errorId);
  if (!error) {
    error = document.createElement('p');
    error.id = errorId;
    error.className = 'field-error';
    field.insertAdjacentElement('afterend', error);
  }

  if (field.validity.valid) {
    error.textContent = '';
    field.removeAttribute('aria-invalid');
    field.removeAttribute('aria-describedby');
  } else {
    error.textContent = getMessage(field);
    field.setAttribute('aria-invalid', 'true');
    field.setAttribute('aria-describedby', errorId);
  }
}

Atributy aria-invalid a aria-describedby zajistí, že čtečka obrazovky u pole přečte i chybovou hlášku. Text chyby vkládejte přes textContent, ne innerHTML.

Kdy validovat

Validace při každém stisku klávesy je otravná – uživatel ještě nedopsal e-mail a už vidí chybu. Osvědčený postup:

  1. první kontrola při opuštění pole (blur),
  2. jakmile pole jednou ukázalo chybu, kontrolovat průběžně při psaní, aby chyba zmizela hned po opravě,
  3. kontrola všeho při odeslání.
form.addEventListener('blur', e => {
  if (e.target.matches('input, select, textarea')) {
    e.target.dataset.touched = 'true';
    showError(e.target);
  }
}, true);   // blur nebublá, proto capture fáze

form.addEventListener('input', e => {
  if (e.target.dataset.touched) showError(e.target);
});

form.addEventListener('submit', async e => {
  e.preventDefault();
  const fields = [...form.elements].filter(el => el.willValidate);
  fields.forEach(showError);

  const firstInvalid = fields.find(f => !f.validity.valid);
  if (firstInvalid) {
    firstInvalid.focus();
    return;
  }
  await submit(new FormData(form));
});

Po neúspěšném odeslání přesuňte fokus na první chybné pole – uživatel s klávesnicí nebo čtečkou jinak neví, kde je problém.

Vlastní pravidla: potvrzení hesla

const confirm = document.querySelector('#password-confirm');
const password = document.querySelector('#password');

function checkMatch() {
  confirm.setCustomValidity(
    confirm.value && confirm.value !== password.value ? 'Hesla se neshodují.' : ''
  );
}
password.addEventListener('input', checkMatch);
confirm.addEventListener('input', checkMatch);

Nezapomeňte nastavit prázdný řetězec, když je hodnota v pořádku – jinak pole zůstane navždy nevalidní. Do getMessage pak přidejte i customError: f => f.validationMessage.

Stylování podle stavu

input:user-invalid {
  border-color: #c0392b;
}
.field-error {
  color: #c0392b;
  font-size: 0.875rem;
  min-height: 1.25em;   /* rezervuje místo, formulář neposkakuje */
}

Pseudotřída :user-invalid (podporovaná v aktuálních prohlížečích) se na rozdíl od :invalid aktivuje až po interakci uživatele, takže prázdný formulář nesvítí červeně hned po načtení.

Odeslání a ochrana proti dvojkliku

async function submit(data) {
  const button = form.querySelector('[type="submit"]');
  button.disabled = true;
  try {
    const res = await fetch('/api/signup', { method: 'POST', body: data });
    if (!res.ok) throw new Error();
    form.reset();
  } catch {
    alert('Odeslání se nepovedlo, zkuste to znovu.');
  } finally {
    button.disabled = false;
  }
}

Server validuje vždy

Validace v prohlížeči je pohodlí pro uživatele, ne zabezpečení. Kdokoli ji obejde pomocí DevTools nebo pošle požadavek přímo přes curl. Server musí stejná pravidla zkontrolovat znovu a chyby vrátit ve strukturované podobě (např. { "errors": { "email": "Tento e-mail už je registrovaný." } }), aby je frontend mohl zobrazit u správných polí.

Ve větších React aplikacích se osvědčuje kombinace React Hook Form a schématu v knihovně Zod – stejné schéma můžete použít na klientu i na serveru.