De meeste HTML-formulieren die ik in productie tegenkom, gebruiken drie invoertypen: text, password en submit. Dat is alles. Ondertussen heeft HTML al jaren uitgebreide ingebouwde invoertypen, beperkingsvalidatie en toegankelijkheidsfuncties — allemaal beschikbaar zonder JavaScript en zonder npm install. Laten we ze gebruiken.
Invoertypen die het Waard Zijn te Kennen
Elk invoertype doet echt werk voor je: het activeert het juiste mobiele toetsenbord, biedt ingebouwde validatie en communiceert de intentie naar hulptechnologie. Dit zijn de typen die constant voorkomen in echte projecten:
<!-- 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>type="date" geeft de waarde terug in het formaat YYYY-MM-DD ongeacht de taal van de gebruiker — precies wat je wilt bij het versturen naar een server. Verwerk nooit datums van type="text"-invoervelden als je het kunt vermijden.Labelkoppeling — Doe Het Goed
Labels zijn geen optionele decoratie. Ze zijn de primaire manier waarop schermlezers invoervelden identificeren, en klikken op een label zou focus op het gekoppelde invoerveld moeten plaatsen. Er zijn twee geldige aanpakken:
<!-- 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 -->De for/id-aanpak is gewoonlijk te verkiezen omdat het labels en invoervelden losstaand houdt in de DOM, wat gemakkelijker te stylen is. Vertrouw nooit op placeholder als labelvervanging — het laat gebruikers met cognitieve beperkingen in de steek en verdwijnt zodra iemand begint te typen. De W3C WAI-labelhandleiding en de WebAIM-formulierbesturingsgids behandelen beide de toegankelijkheidsreden uitgebreider.
Ingebouwde Beperkingsvalidatie
De ingebouwde beperkingsvalidatie van HTML wordt uitgevoerd vóór het indienen van het formulier en is echt krachtig. Je krijgt veel nuttig gedrag gratis:
<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. Veld moet een waarde hebben. Bij checkboxes moet het aangevinkt zijn.pattern. Een regex waaraan de waarde moet voldoen. Voeg altijd eentitle-attribuut toe — dit wordt de tekst van de validatietooltip en geeft gebruikers een hint over het verwachte formaat.minlength/maxlength. Tekenaantal voor tekstinvoervelden.maxlengthknipt invoer stilletjes af;minlengthvalideert alleen bij indienen.min/max. Numerieke of datumgrenzen. Werkt opnumber-,date-,range- entime-invoervelden.step. Definieert geldige stappen.step="0.01"op een valutaveld staat centen toe.step="any"schakelt de stapcontrole volledig uit.
fieldset en legend voor Groepering
Wanneer je een groep gerelateerde invoervelden hebt — vooral keuzerondjes of checkboxes — groeperen <fieldset> en <legend> ze semantisch. Schermlezers lezen de legenda voor elk invoerveld in de groep, zodat gebruikers altijd weten op welke vraag een keuzerondje antwoord geeft.
<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>De Constraint Validation API
De Constraint Validation API geeft je JavaScript-toegang tot dezelfde validatielogica die de browser native gebruikt. Dit laat je validatie programmatisch activeren en aangepaste foutmeldingen instellen zonder het indienen te blokkeren:
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.');
});Let op het kritieke detail: zodra je setCustomValidity() aanroept met een niet-lege string, is het veld permanent ongeldig totdat je het wist door setCustomValidity('') aan te roepen. De wisstap vergeten is de meest voorkomende bug bij het gebruik van deze API.
novalidate voor Aangepaste JS-validatie
Als je een aangepaste validatie-UI bouwt met gestylede foutmeldingen (niet de native tooltips van de browser), voeg dan novalidate toe aan het formulier. Dit schakelt de native browservalidatie uit terwijl de Constraint Validation API beschikbaar blijft voor je eigen controles:
<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>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 en aria-describedby
Twee attributen die de UX van formulieren aanzienlijk verbeteren en gemakkelijk over het hoofd worden gezien:
<!-- 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>Het autocomplete-attribuut gebruikt gestandaardiseerde tokenwaarden die zijn gedefinieerd door de WHATWG-specificatie. Door het juiste token te gebruiken, kunnen browsers en wachtwoordmanagers velden betrouwbaar automatisch invullen. aria-describedby koppelt een hint of foutbericht aan een invoerveld — schermlezers lezen de beschrijving na het veldlabel, waardoor beperkingen hoorbaar zijn voordat de gebruiker begint te typen.
Een Compleet Toegankelijk Inlogformulier
Hier is alles gecombineerd — een inlogformulier dat werkt voor toetsenbordgebruikers, schermlezers gebruikers en mobiele gebruikers, met alleen HTML en een kleine hoeveelheid JavaScript:
<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>Kernpunten: aria-live="polite" op fout-spans betekent dat schermlezers fouten aankondigen wanneer ze verschijnen zonder te onderbreken wat de gebruiker op dat moment hoort. role="alert" versterkt dit voor oudere schermlezers. De autocomplete-waarden komen overeen met wat wachtwoordmanagers verwachten, zodat automatisch invullen correct werkt bij de eerste poging.
Handige Tools
Bij het bouwen en testen van HTML-formulieren vangt HTML Validator structurele fouten op zoals ontbrekende labels en dubbele ID's voordat gebruikers ze tegenkomen. Voor algemene HTML-opschoning houdt HTML Formatter je markup leesbaar. De MDN input-elementreferentie is de meest volledige documentatie voor alle invoertypen en hun attributen.
Samenvatting
HTML-formulieren hebben veel meer ingebouwde mogelijkheden dan de meeste projecten gebruiken. Het juiste invoertype kiezen geeft je mobiele toetsenborden, validatie en semantische betekenis gratis. Correcte labelkoppeling en aria-describedby maken formulieren navigeerbaar zonder muis. De Constraint Validation API geeft je JavaScript-hooks in native validatie zonder te vechten tegen de browser. Zet deze stukken bij elkaar en je krijgt formulieren die voor iedereen werken — voordat je ook maar één regel aangepaste validatielogica schrijft.