Większość formularzy HTML, które napotykam w produkcji, używa trzech typów pól: text, password i submit. To wszystko. Tymczasem HTML od lat posiada bogate wbudowane typy pól, walidację ograniczeń i funkcje dostępności — wszystkie dostępne bez JavaScript i bez npm install. Zacznijmy ich używać.
Typy Pól Warte Poznania
Każdy typ pola wykonuje za ciebie prawdziwą pracę: uruchamia odpowiednią klawiaturę mobilną, zapewnia wbudowaną walidację i komunikuje zamiar technologiom wspomagającym. Oto te, które regularnie pojawiają się w prawdziwych projektach:
<!-- 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" zwraca wartość w formacie YYYY-MM-DD niezależnie od ustawień regionalnych użytkownika — co jest dokładnie tym, czego chcesz przy wysyłaniu danych na serwer. Nigdy nie parsuj dat z pól type="text", jeśli możesz tego uniknąć.Powiązanie Etykiet — Zrób To Poprawnie
Etykiety nie są opcjonalną dekoracją. To główny sposób, w jaki czytniki ekranu identyfikują pola, a kliknięcie etykiety powinno ustawić fokus na powiązanym polu. Istnieją dwa poprawne podejścia:
<!-- 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 -->Podejście for/id jest zazwyczaj preferowane, ponieważ utrzymuje etykiety i pola rozdzielone w DOM, co jest łatwiejsze do stylizowania. Nigdy nie polegaj na placeholder jako zastępniku etykiety — zawodzi użytkowników z niepełnosprawnościami poznawczymi i znika w momencie, gdy ktoś zaczyna pisać. Samouczek etykiet W3C WAI i wskazówki WebAIM dotyczące kontrolek formularzy omawiają uzasadnienie dostępności bardziej szczegółowo.
Wbudowana Walidacja Ograniczeń
Wbudowana walidacja ograniczeń HTML uruchamia się przed wysłaniem formularza i jest naprawdę potężna. Dostajesz wiele przydatnych zachowań za darmo:
<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. Pole musi być niepuste. W przypadku pól wyboru musi być zaznaczone.pattern. Wyrażenie regularne, któremu wartość musi odpowiadać. Zawsze dodawaj atrybuttitle— staje się tekstem podpowiedzi walidacji i daje użytkownikom wskazówkę dotyczącą oczekiwanego formatu.minlength/maxlength. Liczba znaków dla pól tekstowych.maxlengthdyskretnie obcina wprowadzane dane;minlengthwaliduje tylko przy wysyłaniu.min/max. Granice numeryczne lub daty. Działa na polachnumber,date,rangeitime.step. Definiuje prawidłowe przyrosty.step="0.01"w polu waluty pozwala na grosze.step="any"całkowicie wyłącza sprawdzanie kroku.
fieldset i legend do Grupowania
Gdy masz grupę powiązanych pól — zwłaszcza przycisków radiowych lub pól wyboru — <fieldset> i <legend> grupują je semantycznie. Czytniki ekranu czytają legendę przed każdym polem w grupie, dzięki czemu użytkownicy zawsze wiedzą, na jakie pytanie odpowiada przycisk radiowy.
<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>API Walidacji Ograniczeń
API Walidacji Ograniczeń daje ci dostęp JavaScript do tej samej logiki walidacji, której przeglądarka używa natywnie. Pozwala to programowo uruchamiać walidację i ustawiać niestandardowe komunikaty o błędach bez blokowania wysyłania:
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.');
});Zwróć uwagę na kluczowy szczegół: gdy wywołasz setCustomValidity() z niepustym ciągiem znaków, pole jest trwale nieprawidłowe, dopóki nie wyczyścisz go przez wywołanie setCustomValidity(''). Zapomnienie o kroku czyszczenia to najczęstszy błąd przy używaniu tego API.
novalidate dla Niestandardowej Walidacji JS
Jeśli budujesz niestandardowy interfejs walidacji ze stylizowanymi komunikatami o błędach (zamiast natywnych tooltipów przeglądarki), dodaj novalidate do formularza. Wyłącza to natywną walidację przeglądarki, jednocześnie udostępniając API Walidacji Ograniczeń dla twoich własnych sprawdzeń:
<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 i aria-describedby
Dwa atrybuty, które znacznie poprawiają UX formularzy i łatwo je przeoczyć:
<!-- 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>Atrybut autocomplete używa standaryzowanych wartości tokenów zdefiniowanych przez specyfikację WHATWG. Gdy używasz prawidłowego tokenu, przeglądarki i menedżery haseł mogą niezawodnie automatycznie wypełniać pola. aria-describedby powiązuje wskazówkę lub komunikat o błędzie z polem — czytniki ekranu odczytują opis po etykiecie pola, co sprawia, że ograniczenia są słyszalne, zanim użytkownik zacznie pisać.
Kompletny Dostępny Formularz Logowania
Oto wszystko razem — formularz logowania, który działa dla użytkowników klawiatury, czytników ekranu i użytkowników mobilnych, używając tylko HTML i niewielkiej ilości 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>Kluczowe punkty: aria-live="polite" na elementach błędów oznacza, że czytniki ekranu ogłoszą błędy po ich pojawieniu się bez przerywania tego, czego użytkownik aktualnie słucha. role="alert" wzmacnia to dla starszych czytników ekranu. Wartości autocomplete odpowiadają temu, czego oczekują menedżery haseł, dzięki czemu autouzupełnianie działa poprawnie za pierwszym razem.
Przydatne Narzędzia
Podczas budowania i testowania formularzy HTML, Walidator HTML wykrywa błędy strukturalne, takie jak brakujące etykiety i zduplikowane identyfikatory, zanim zrobią to użytkownicy. Do ogólnego czyszczenia HTML, Formater HTML utrzymuje czytelność kodu. Dokumentacja elementu input w MDN jest najbardziej kompletną dokumentacją dla wszystkich typów pól i ich atrybutów.
Podsumowanie
Formularze HTML mają o wiele więcej wbudowanych możliwości, niż większość projektów wykorzystuje. Wybór właściwego typu pola daje ci mobilne klawiatury, walidację i znaczenie semantyczne za darmo. Właściwe powiązanie etykiet i aria-describedby sprawia, że formularze są nawigowalne bez myszy. API Walidacji Ograniczeń daje ci hooki JavaScript do natywnej walidacji bez walki z przeglądarką. Połącz te elementy, a uzyskasz formularze, które działają dla każdego — zanim napiszesz jedną linię niestandardowej logiki walidacji.