HTML & CSS

Форми в HTML

Повний розбір HTML-форм за специфікацією HTML Living Standard: <form>, всі типи <input>, <label>, <select>, <textarea>, <button>, <fieldset>, валідація, доступність та найкращі практики.

Форми в HTML

Як форми з'єднують людей із сервером

2004 рік. Марк Цукерберг запускає Facebook. Перше, що бачить новий користувач — форма реєстрації: ім'я, прізвище, пошта, пароль. Чотири поля і кнопка «Sign Up». Саме ця форма, написана на HTML, стала точкою входу для мільярдів людей.

Форми — єдиний нативний механізм HTML для двостороннього обміну даними між користувачем і сервером. Без форм веб залишився б лише читанкою: ви б не могли ні замовити піцу, ні написати коментар, ні авторизуватися.

За специфікацією HTML Living Standard (WHATWG), форми — це складна екосистема взаємопов'язаних елементів. Розберемо кожен із них.


Елемент <form> — контейнер форми

<form> (form — форма) представляє гіпертекстову форму з полями, які користувач може заповнити і відправити.

За специфікацією WHATWG:

  • Content model: flow content, але без вкладеного <form>
  • Permitted parents: будь-який елемент, що приймає flow content

Атрибути <form>

action
string
URL, на який надсилаються дані форми. За замовчуванням — поточна URL сторінки. Може бути відносним або абсолютним.
method
string
HTTP-метод: get або post. За замовчуванням get.
enctype
string
Тип кодування для method="post". Значення: application/x-www-form-urlencoded (за замовч.), multipart/form-data (для файлів), text/plain (для debug).
novalidate
boolean
Відключає браузерну валідацію при відправці форми.
autocomplete
string
on або off — дозволяти браузеру автодоповнення полів. За замовч. on.
target
string
Де показати відповідь: _self, _blank, _parent, _top або назва фрейму.
name
string
Ім'я форми для звернення через document.forms. Не впливає на відправку.

GET vs POST — ключова різниця

<!-- Дані передаються в URL: /search?q=html+форми&lang=uk -->
<form action="/search" method="get">
    <label for="q">Пошук:</label>
    <input type="text" id="q" name="q" value="html форми" />

    <label for="lang">Мова:</label>
    <select id="lang" name="lang">
        <option value="uk">Українська</option>
        <option value="en">English</option>
    </select>

    <button type="submit">Знайти</button>
</form>

Коли GET:

  • Пошукові запити
  • Фільтрація та сортування
  • Будь-що, що можна закладати в браузер або ділитися лінком
  • Ідемпотентні операції (повторний запит = той самий результат)

Елемент <label> — підпис поля

<label> (label — підпис) пов'язує текстовий опис з елементом керування форми. Це критично для доступності: скринрідер оголошує підпис при фокусуванні на полі.

Два способи зв'язку

<!-- Спосіб 1: явне зв'язування через for/id (рекомендований) -->
<label for="username">Ім'я користувача</label>
<input type="text" id="username" name="username" />

<!-- Спосіб 2: неявне — <input> всередині <label> -->
<label>
    Ім'я користувача
    <input type="text" name="username" />
</label>
Перший спосіб (через for/id) — кращий: він дозволяє більшу гнучкість у верстці, бо <label> і <input> не зобов'язані бути поруч у DOM.

Клік на <label> фокусує поле

Це важлива UX-перевага: клікаючи на текст підпису, користувач переходить у поле. Особливо критично для чекбоксів та радіокнопок — їхні мітки є дуже маленькими за замовчуванням:

<!-- ✅ Клік на "Погоджуюсь з умовами" активує checkbox -->
<input type="checkbox" id="agree" name="agree" />
<label for="agree">Погоджуюсь з умовами використання</label>

<!-- ❌ Немає label — користувач мусить влучити точно в checkbox -->
<input type="checkbox" name="agree" />
Погоджуюсь з умовами використання

Елемент <input> — поле вводу

<input> (input — ввід) — void element, найбільш багатофункціональний елемент форми. Атрибут type визначає, яким є поле.

Типи <input>

Текстові поля

<!-- Базовий текстовий ввід -->
<label for="name">Повне ім'я</label>
<input type="text" id="name" name="name" placeholder="Іван Петренко" autocomplete="name" maxlength="100" required />

Базовий однорядковий текстовий ввід. Найуніверсальніший тип.

Числові та діапазон

<!-- number: стрілки вгору/вниз, валідація числа -->
<label for="quantity">Кількість (1–99)</label>
<input type="number" id="quantity" name="quantity" min="1" max="99" step="1" value="1" />

<!-- range: слайдер -->
<label for="volume">Гучність: <output id="vol-output">50</output>%</label>
<input type="range" id="volume" name="volume" min="0" max="100" step="5" value="50" />

Дата та час

<!-- date: вибір дати (рік-місяць-день) -->
<label for="birthday">Дата народження</label>
<input type="date" id="birthday" name="birthday" min="1900-01-01" max="2010-12-31" />

<!-- time: вибір часу -->
<label for="appointment">Час запису</label>
<input type="time" id="appointment" name="appointment" min="09:00" max="18:00" step="1800" />

<!-- datetime-local: дата + час без часового поясу -->
<label for="event-start">Початок події</label>
<input type="datetime-local" id="event-start" name="event_start" min="2024-01-01T00:00" />

<!-- month: місяць та рік -->
<label for="exp-date">Термін дії картки</label>
<input type="month" id="exp-date" name="exp_date" />

<!-- week: номер тижня -->
<label for="delivery-week">Тиждень доставки</label>
<input type="week" id="delivery-week" name="delivery_week" />

Checkbox та Radio

<!-- CHECKBOX: незалежний вибір (можна вибрати кілька) -->
<fieldset>
    <legend>Улюблені мови програмування</legend>

    <input type="checkbox" id="lang-js" name="languages" value="javascript" />
    <label for="lang-js">JavaScript</label>

    <input type="checkbox" id="lang-ts" name="languages" value="typescript" />
    <label for="lang-ts">TypeScript</label>

    <input type="checkbox" id="lang-py" name="languages" value="python" checked />
    <label for="lang-py">Python</label>

    <input type="checkbox" id="lang-go" name="languages" value="go" />
    <label for="lang-go">Go</label>
</fieldset>

<!-- RADIO: взаємовиключний вибір (тільки один з групи) -->
<!-- Група radio визначається однаковим name -->
<fieldset>
    <legend>Рівень кваліфікації</legend>

    <input type="radio" id="level-junior" name="level" value="junior" />
    <label for="level-junior">Junior</label>

    <input type="radio" id="level-mid" name="level" value="mid" checked />
    <label for="level-mid">Middle</label>

    <input type="radio" id="level-senior" name="level" value="senior" />
    <label for="level-senior">Senior</label>
</fieldset>
fieldset {
    border: 1px solid #e2e8f0;
    border-radius: 8px;
    padding: 1rem 1.5rem;
    margin-bottom: 1rem;
}
legend {
    font-weight: 600;
    padding: 0 0.5rem;
    color: #1e293b;
}
input[type='checkbox'],
input[type='radio'] {
    margin-right: 0.4rem;
    accent-color: #3b82f6;
}
label {
    margin-right: 1rem;
    cursor: pointer;
}
Preview
Однакове name у <input type="radio"> — це те, що робить їх групою. Браузер автоматично знімає вибір попередньої кнопки при виборі нової.

Колір та файл

<!-- color: колірний пікер -->
<label for="brand-color">Колір бренду</label>
<input type="color" id="brand-color" name="brand_color" value="#3b82f6" />

<!-- file: вибір файлів -->
<label for="documents">Документи</label>
<input type="file" id="documents" name="documents" accept=".pdf,.doc,.docx" multiple />

<!-- multiple: дозволяє вибрати кілька файлів -->
<label for="photos">Фотографії (до 5 штук)</label>
<input type="file" id="photos" name="photos" accept="image/*" multiple />

Приховані та кнопкові

<!-- hidden: дані, які відправляються разом з формою, але невидимі -->
<input type="hidden" name="csrf_token" value="abc123xyz" />
<input type="hidden" name="product_id" value="42" />

<!-- submit: кнопка відправки -->
<input type="submit" value="Зареєструватися" />

<!-- reset: скидання всіх полів до значень за замовч. -->
<input type="reset" value="Очистити форму" />

<!-- button: кнопка без дефолтної дії -->
<input type="button" value="Перевірити" id="check-btn" />

<!-- image: кнопка-зображення (відправляє координати кліку) -->
<input type="image" src="/images/submit-arrow.png" alt="Відправити" width="40" height="40" />

Спільні атрибути <input>

АтрибутТипОпис
namestringІм'я поля — ключ у парі ключ=значення при відправці
idstringУнікальний ідентифікатор для <label for>
valuestringПочаткове значення (або відправлюване для checkbox/radio/hidden)
placeholderstringПідказка всередині поля (зникає при введенні)
requiredbooleanПоле обов'язкове для заповнення
disabledbooleanПоле заблоковане, не відправляється з формою
readonlybooleanПоле лише для читання, але відправляється з формою
autofocusbooleanАвтоматичний фокус при завантаженні сторінки
autocompletestringПідказка браузеру для автодоповнення (значень дуже багато)
min / maxvariesМінімальне/максимальне значення для number, date, range
minlength / maxlengthnumberМінімальна/максимальна довжина тексту
stepnumber/stringКрок для number, range, date, time
patternstringРегулярний вираз для валідації значення
multiplebooleanДозволяє кілька значень (email, file)
acceptstringДозволені типи файлів для type="file"

Елемент <textarea> — багаторядковий текст

<textarea> (text area — текстова область) — поле для багаторядкового тексту. На відміну від <input>, має закриваючий тег і може містити текст за замовчуванням:

<label for="message">Ваше повідомлення</label>
<textarea
    id="message"
    name="message"
    rows="6"
    cols="40"
    placeholder="Опишіть ваш запит детально..."
    maxlength="1000"
    required
>
Тут може бути початковий текст</textarea
>
rows
number
Кількість видимих рядків (висота). За замовч. 2. Рекомендовано вказувати явно.
cols
number
Кількість видимих символів у рядку (ширина). За замовч. 20. На практиці краще контролювати через CSS.
wrap
string
soft (за замовч.) — перенос лише для відображення; hard — перенос вставляється в значення при відправці.
resize
string
Не є HTML-атрибутом — контролюється через CSS-властивість resize: both, horizontal, vertical, none.

Елемент <select> — випадаючий список

<label for="country">Країна</label>
<select id="country" name="country" required>
    <!-- Перший варіант — підказка (не реальний вибір) -->
    <option value="" disabled selected>Оберіть країну</option>

    <!-- optgroup для групування -->
    <optgroup label="Східна Європа">
        <option value="UA">🇺🇦 Україна</option>
        <option value="PL">🇵🇱 Польща</option>
        <option value="CZ">🇨🇿 Чехія</option>
    </optgroup>

    <optgroup label="Захід Європи">
        <option value="DE">🇩🇪 Німеччина</option>
        <option value="FR">🇫🇷 Франція</option>
        <option value="NL">🇳🇱 Нідерланди</option>
    </optgroup>
</select>

<!-- multiple: список з множинним вибором -->
<label for="skills">Ваші навички</label>
<select id="skills" name="skills" multiple size="5">
    <option value="html">HTML</option>
    <option value="css">CSS</option>
    <option value="js" selected>JavaScript</option>
    <option value="ts" selected>TypeScript</option>
    <option value="react">React</option>
    <option value="vue">Vue.js</option>
</select>
multiple
boolean
Дозволяє вибір кількох <option> (Ctrl+Click або Shift+Click).
size
number
Кількість видимих рядків. При multiple рекомендовано вказувати.

Елемент <button> — кнопка

<button> — більш потужна альтернатива <input type="submit">. На відміну від <input>, <button> може містити будь-який phrasing content — текст, зображення, іконки:

<!-- type="submit" — відправляє форму (за замовчуванням всередині <form>) -->
<button type="submit">
    <img src="/icons/send.svg" alt="" width="16" height="16" />
    Надіслати заявку
</button>

<!-- type="reset" — скидає всі поля до початкових значень -->
<button type="reset">Очистити</button>

<!-- type="button" — без дефолтної дії, лише для JS -->
<button type="button" id="preview-btn">Попередній перегляд</button>
<button> без атрибута type всередині <form> має тип submit за замовчуванням — це може призвести до ненавмисного відправлення форми при кліку. Завжди вказуйте type="button" для кнопок без функції відправки.

<fieldset> та <legend> — групування полів

<fieldset> (field set — набір полів) групує пов'язані елементи форми. <legend> (legend — легенда) — підпис групи, перший дочірній елемент <fieldset>:

<form action="/register" method="post">
    <fieldset>
        <legend>Особисті дані</legend>

        <label for="first-name">Ім'я</label>
        <input type="text" id="first-name" name="first_name" required autocomplete="given-name" />

        <label for="last-name">Прізвище</label>
        <input type="text" id="last-name" name="last_name" required autocomplete="family-name" />

        <label for="birth-date">Дата народження</label>
        <input type="date" id="birth-date" name="birth_date" autocomplete="bday" />
    </fieldset>

    <fieldset>
        <legend>Контактна інформація</legend>

        <label for="reg-email">Email</label>
        <input type="email" id="reg-email" name="email" required autocomplete="email" />

        <label for="reg-phone">Телефон</label>
        <input type="tel" id="reg-phone" name="phone" autocomplete="tel" />
    </fieldset>

    <fieldset>
        <legend>Налаштування сповіщень</legend>

        <input type="checkbox" id="notify-email" name="notify" value="email" checked />
        <label for="notify-email">На Email</label>

        <input type="checkbox" id="notify-sms" name="notify" value="sms" />
        <label for="notify-sms">SMS</label>

        <input type="checkbox" id="notify-push" name="notify" value="push" />
        <label for="notify-push">Push-сповіщення</label>
    </fieldset>

    <!-- disabled fieldset — відключає всі поля всередині -->
    <fieldset disabled>
        <legend>Платіжна інформація (недоступно на цьому кроці)</legend>
        <label for="card">Номер картки</label>
        <input type="text" id="card" name="card_number" />
    </fieldset>

    <button type="submit">Зареєструватися</button>
</form>
<fieldset disabled> — елегантний спосіб заблокувати одразу всю групу полів. Браузер ігнорує всі елементи всередині при відправці форми.

<datalist> — список підказок

<datalist> надає попередньо визначені варіанти для <input>, не обмежуючи введення користувача (на відміну від <select>):

<label for="framework">Фреймворк</label>
<input type="text" id="framework" name="framework" list="frameworks-list" placeholder="Введіть або оберіть..." />

<datalist id="frameworks-list">
    <option value="React"></option>
    <option value="Vue.js"></option>
    <option value="Angular"></option>
    <option value="Svelte"></option>
    <option value="Solid.js"></option>
    <option value="Nuxt.js"></option>
    <option value="Next.js"></option>
</datalist>

<!-- datalist працює і для числового вводу -->
<label for="font-size">Розмір шрифту (px)</label>
<input type="number" id="font-size" name="font_size" list="font-sizes" min="8" max="72" />
<datalist id="font-sizes">
    <option value="12"></option>
    <option value="14"></option>
    <option value="16"></option>
    <option value="18"></option>
    <option value="24"></option>
    <option value="32"></option>
</datalist>

<output> — результат обчислення

<output> (output — результат) представляє результат обчислення. Семантично пов'язується з полями через атрибут for:

<form oninput="result.value = parseInt(a.value) + parseInt(b.value)">
    <label for="a">Перше число:</label>
    <input type="number" id="a" name="a" value="0" />

    <label for="b">Друге число:</label>
    <input type="number" id="b" name="b" value="0" />

    <label for="result">Сума:</label>
    <!-- for вказує на id полів, які є джерелом -->
    <output id="result" name="result" for="a b">0</output>
</form>

<progress> та <meter> — прогрес і вимірювання

<!-- progress: прогрес виконання завдання -->
<label for="upload-progress">Завантаження файлу:</label>
<progress id="upload-progress" value="65" max="100">65%</progress>

<!-- Невизначений прогрес (без value) — анімований індикатор -->
<progress>Завантаження...</progress>

<!-- meter: вимірювання в межах діапазону -->
<!-- Семантика: низький/нормальний/високий рівень -->
<label for="disk-usage">Використання диску:</label>
<meter id="disk-usage" value="75" min="0" max="100" low="50" high="80" optimum="30">75%</meter>
<!-- value < low = добре (зелений), low..high = нормально (жовтий), >high = погано (червоний) -->
label {
    display: block;
    margin-bottom: 0.3rem;
    font-weight: 500;
    font-size: 0.9rem;
}
progress, meter {
    width: 100%;
    height: 1.25rem;
    margin-bottom: 1.5rem;
}
Preview

Вбудована валідація HTML5

Браузери виконують базову валідацію до відправки форми. Атрибути, що беруть участь у валідації:

Атрибути валідації

<form action="/register" method="post">
    <!-- required: поле не може бути порожнім -->
    <input type="text" name="name" required />

    <!-- minlength / maxlength: довжина тексту -->
    <input type="text" name="username" minlength="3" maxlength="20" required />

    <!-- min / max: числовий діапазон або дати -->
    <input type="number" name="age" min="18" max="120" required />

    <!-- pattern: регулярний вираз -->
    <!-- \+? — опціональний +, [0-9] — цифри, {11,13} — від 11 до 13 символів -->
    <input
        type="tel"
        name="phone"
        pattern="\+?[0-9]{11,13}"
        title="Номер телефону: 11-13 цифр, опціонально з +"
        required
    />

    <!-- type="email" автоматично перевіряє формат -->
    <input type="email" name="email" required />

    <!-- type="url" перевіряє формат URL -->
    <input type="url" name="website" />

    <button type="submit">Відправити</button>
</form>

novalidate — відключення валідації

<!-- Відключає браузерну валідацію (для власної JS-валідації) -->
<form action="/submit" method="post" novalidate>
    <input type="email" name="email" required />
    <button type="submit">Відправити</button>
</form>

CSS-псевдокласи валідації

<form novalidate>
    <label for="demo-email">Email (спробуйте ввести неправильний):</label>
    <input type="email" id="demo-email" value="not-an-email" required />

    <label for="demo-name">Ім'я (обов'язкове, порожнє):</label>
    <input type="text" id="demo-name" required />

    <label for="demo-age">Вік (18–120):</label>
    <input type="number" id="demo-age" value="25" min="18" max="120" />
</form>
form {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}
label {
    font-size: 0.875rem;
    font-weight: 500;
    color: #374151;
}
input {
    display: block;
    width: 100%;
    padding: 0.5rem 0.75rem;
    border: 1px solid #d1d5db;
    border-radius: 6px;
    font-size: 1rem;
    outline: none;
    transition: border-color 0.2s;
}

/* Поле валідне (заповнено правильно) */
input:valid {
    border-color: #10b981;
    background: #f0fdf4;
}

/* Поле невалідне */
input:invalid {
    border-color: #ef4444;
    background: #fef2f2;
}

/* Обов'язкове поле */
input:required {
    border-left: 3px solid #f59e0b;
}

/* Поле в межах min/max */
input:in-range {
    border-color: #10b981;
}

/* Поле поза межами min/max */
input:out-of-range {
    border-color: #ef4444;
    background: #fef2f2;
}
Preview
Браузерна валідація HTML5 — лише перша лінія захисту для UX. Вона не замінює серверну валідацію. Будь-який користувач може відключити required через DevTools або надіслати запит напряму, оминаючи форму.

Autocomplete — підказки браузеру

Атрибут autocomplete стандартизований: правильні значення дозволяють браузеру та менеджерам паролів точно знати, яке поле для чого:

<form autocomplete="on">
    <!-- Особисті дані -->
    <input autocomplete="name" name="name" />
    <!-- Повне ім'я -->
    <input autocomplete="given-name" name="first" />
    <!-- Ім'я -->
    <input autocomplete="family-name" name="last" />
    <!-- Прізвище -->
    <input autocomplete="email" name="email" />
    <!-- Email -->
    <input autocomplete="tel" name="phone" />
    <!-- Телефон -->

    <!-- Адреса -->
    <input autocomplete="street-address" name="street" />
    <input autocomplete="postal-code" name="zip" />
    <input autocomplete="country-name" name="country" />

    <!-- Платіж -->
    <input autocomplete="cc-number" name="card" />
    <!-- Номер картки -->
    <input autocomplete="cc-exp" name="expiry" />
    <!-- Термін -->
    <input autocomplete="cc-csc" name="cvv" />
    <!-- CVV -->

    <!-- Авторизація -->
    <input autocomplete="username" name="login" />
    <input autocomplete="current-password" name="password" />
    <input autocomplete="new-password" name="new_pass" />
</form>

Практичні завдання


Комплексний приклад від А до Я

Тема: Форма заявки на вакансію — Full Stack Developer у стартапі TechUA.

<!DOCTYPE html>
<html lang="uk">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta name="description" content="Залишити заявку на вакансію Full Stack Developer у компанії TechUA." />
        <title>Заявка на вакансію — TechUA</title>
    </head>
    <body>
        <header>
            <h1>TechUA — Заявка на вакансію</h1>
            <nav aria-label="Кроки заявки">
                <ol>
                    <li><a href="#personal">Особисті дані</a></li>
                    <li><a href="#experience">Досвід</a></li>
                    <li><a href="#skills">Навички</a></li>
                    <li><a href="#documents">Документи</a></li>
                </ol>
            </nav>
        </header>

        <main>
            <form action="/careers/apply" method="post" enctype="multipart/form-data" novalidate id="job-application">
                <!-- CSRF-захист (прихований токен) -->
                <input type="hidden" name="csrf_token" value="a1b2c3d4e5f6" />
                <input type="hidden" name="vacancy_id" value="fsd-2024-11" />

                <!-- ===== КРОК 1: ОСОБИСТІ ДАНІ ===== -->
                <fieldset id="personal">
                    <legend>Крок 1: Особисті дані</legend>

                    <div>
                        <label for="first-name">Ім'я <abbr title="обов'язкове">*</abbr></label>
                        <input
                            type="text"
                            id="first-name"
                            name="first_name"
                            autocomplete="given-name"
                            required
                            minlength="2"
                            maxlength="50"
                            placeholder="Іван"
                        />
                    </div>

                    <div>
                        <label for="last-name">Прізвище <abbr title="обов'язкове">*</abbr></label>
                        <input
                            type="text"
                            id="last-name"
                            name="last_name"
                            autocomplete="family-name"
                            required
                            minlength="2"
                            maxlength="50"
                            placeholder="Коваленко"
                        />
                    </div>

                    <div>
                        <label for="email">Email <abbr title="обов'язкове">*</abbr></label>
                        <input
                            type="email"
                            id="email"
                            name="email"
                            autocomplete="email"
                            required
                            placeholder="ivan@example.com"
                        />
                    </div>

                    <div>
                        <label for="phone">Телефон</label>
                        <input
                            type="tel"
                            id="phone"
                            name="phone"
                            autocomplete="tel"
                            pattern="\+380[0-9]{9}"
                            title="Формат: +380XXXXXXXXX"
                            placeholder="+380671234567"
                        />
                    </div>

                    <div>
                        <label for="city">Місто</label>
                        <input
                            type="text"
                            id="city"
                            name="city"
                            list="ua-cities"
                            autocomplete="address-level2"
                            placeholder="Оберіть або введіть місто"
                        />
                        <datalist id="ua-cities">
                            <option value="Київ"></option>
                            <option value="Харків"></option>
                            <option value="Одеса"></option>
                            <option value="Дніпро"></option>
                            <option value="Запоріжжя"></option>
                            <option value="Львів"></option>
                        </datalist>
                    </div>

                    <div>
                        <label for="website">Ваш GitHub / LinkedIn / Portfolio</label>
                        <input
                            type="url"
                            id="website"
                            name="website"
                            autocomplete="url"
                            placeholder="https://github.com/username"
                        />
                    </div>
                </fieldset>

                <!-- ===== КРОК 2: ДОСВІД ===== -->
                <fieldset id="experience">
                    <legend>Крок 2: Досвід роботи</legend>

                    <!-- Radio group: рівень -->
                    <fieldset>
                        <legend>Рівень <abbr title="обов'язкове">*</abbr></legend>

                        <input type="radio" id="level-junior" name="level" value="junior" />
                        <label for="level-junior">Junior (до 1 року)</label>

                        <input type="radio" id="level-middle" name="level" value="middle" />
                        <label for="level-middle">Middle (1–3 роки)</label>

                        <input type="radio" id="level-senior" name="level" value="senior" checked />
                        <label for="level-senior">Senior (3+ роки)</label>

                        <input type="radio" id="level-lead" name="level" value="lead" />
                        <label for="level-lead">Tech Lead / Principal</label>
                    </fieldset>

                    <div>
                        <label for="years-exp">Роки досвіду в розробці</label>
                        <input
                            type="number"
                            id="years-exp"
                            name="years_experience"
                            min="0"
                            max="40"
                            step="1"
                            value="0"
                        />
                    </div>

                    <div>
                        <label for="salary">Очікувана зарплата (USD/місяць)</label>
                        <input
                            type="range"
                            id="salary"
                            name="expected_salary"
                            min="500"
                            max="10000"
                            step="500"
                            value="3000"
                        />
                        <output for="salary" id="salary-output">3000</output> USD
                    </div>

                    <div>
                        <label for="availability">Дата готовності до початку роботи</label>
                        <input type="date" id="availability" name="available_from" min="2024-12-01" />
                    </div>

                    <div>
                        <label for="work-format">Формат роботи</label>
                        <select id="work-format" name="work_format" required>
                            <option value="" disabled selected>Оберіть формат</option>
                            <optgroup label="Дистанційно">
                                <option value="remote-full">Повністю дистанційно</option>
                                <option value="remote-partial">Гібрид (1–2 дні в офісі)</option>
                            </optgroup>
                            <optgroup label="В офісі">
                                <option value="office-kyiv">Офіс Київ</option>
                                <option value="office-lviv">Офіс Львів</option>
                            </optgroup>
                        </select>
                    </div>
                </fieldset>

                <!-- ===== КРОК 3: НАВИЧКИ ===== -->
                <fieldset id="skills">
                    <legend>Крок 3: Технічні навички</legend>

                    <!-- Checkbox group: технології -->
                    <fieldset>
                        <legend>Frontend (оберіть всі, якими володієте)</legend>

                        <input type="checkbox" id="skill-html" name="skills" value="html" checked />
                        <label for="skill-html">HTML</label>

                        <input type="checkbox" id="skill-css" name="skills" value="css" checked />
                        <label for="skill-css">CSS</label>

                        <input type="checkbox" id="skill-js" name="skills" value="javascript" checked />
                        <label for="skill-js">JavaScript</label>

                        <input type="checkbox" id="skill-ts" name="skills" value="typescript" />
                        <label for="skill-ts">TypeScript</label>

                        <input type="checkbox" id="skill-react" name="skills" value="react" />
                        <label for="skill-react">React</label>

                        <input type="checkbox" id="skill-vue" name="skills" value="vue" />
                        <label for="skill-vue">Vue.js</label>
                    </fieldset>

                    <fieldset>
                        <legend>Backend</legend>

                        <input type="checkbox" id="skill-node" name="skills" value="nodejs" />
                        <label for="skill-node">Node.js</label>

                        <input type="checkbox" id="skill-python" name="skills" value="python" />
                        <label for="skill-python">Python</label>

                        <input type="checkbox" id="skill-go" name="skills" value="go" />
                        <label for="skill-go">Go</label>
                    </fieldset>

                    <div>
                        <label for="cover-letter">
                            Супровідний лист
                            <small>(розкажіть чому хочете until to TechUA)</small>
                        </label>
                        <textarea
                            id="cover-letter"
                            name="cover_letter"
                            rows="8"
                            maxlength="3000"
                            placeholder="Чому мене зацікавила ця позиція..."
                        ></textarea>
                        <!-- output показує кількість символів -->
                        <output for="cover-letter">0</output> / 3000 символів
                    </div>
                </fieldset>

                <!-- ===== КРОК 4: ДОКУМЕНТИ ===== -->
                <fieldset id="documents">
                    <legend>Крок 4: Документи</legend>

                    <div>
                        <label for="resume">
                            Резюме <abbr title="обов'язкове">*</abbr>
                            <small>(PDF або DOCX, до 5 МБ)</small>
                        </label>
                        <input type="file" id="resume" name="resume" accept=".pdf,.doc,.docx" required />
                    </div>

                    <div>
                        <label for="portfolio">
                            Портфоліо / Приклади роботи
                            <small>(зображення або архів, до 20 МБ)</small>
                        </label>
                        <input type="file" id="portfolio" name="portfolio_files" accept="image/*,.zip,.pdf" multiple />
                    </div>

                    <div>
                        <label for="linkedin-color">Колір LinkedIn-банера (для профілю)</label>
                        <input type="color" id="linkedin-color" name="banner_color" value="#0077b5" />
                    </div>
                </fieldset>

                <!-- ===== ПРОГРЕС ЗАВЕРШЕНОСТІ ===== -->
                <div aria-live="polite">
                    <label for="completion">Заповненість форми:</label>
                    <progress id="completion" value="75" max="100">75%</progress>
                    <meter value="75" min="0" max="100" low="30" high="80" optimum="100">75%</meter>
                </div>

                <!-- ===== ЗГОДИ ===== -->
                <fieldset>
                    <legend>Згоди</legend>

                    <input type="checkbox" id="privacy" name="privacy_policy" value="agreed" required />
                    <label for="privacy">
                        Погоджуюсь з
                        <a href="/privacy" target="_blank" rel="noopener noreferrer"> Політикою конфіденційності </a>
                        <abbr title="обов'язкове">*</abbr>
                    </label>

                    <input type="checkbox" id="newsletter" name="newsletter" value="subscribed" />
                    <label for="newsletter">
                        Хочу отримувати розсилку з новинами TechUA та відкритими вакансіями
                    </label>
                </fieldset>

                <!-- ===== КНОПКИ ===== -->
                <div>
                    <button type="submit">Надіслати заявку →</button>
                    <button type="reset">Очистити форму</button>
                    <button type="button" id="save-draft">Зберегти чернетку</button>
                </div>
            </form>
        </main>

        <footer>
            <address>
                <p>Питання щодо вакансії: <a href="mailto:hr@techua.com">hr@techua.com</a></p>
                <p>HR-відділ TechUA: <a href="tel:+380441234567">+38 (044) 123-45-67</a></p>
            </address>
        </footer>
    </body>
</html>

Що демонструє цей приклад:

🏗️ Структура форми

<form> з method="post", enctype="multipart/form-data", novalidate. Прихований CSRF-токен. Чотири <fieldset> з <legend> для логічного групування.

📝 Поля вводу

Всі типи <input>: text, email, tel з pattern, url, number, range + <output>, date, radio (група), checkbox (кілька груп), file (одиночний та multiple), color, hidden.

🎛️ Складні елементи

<select> з <optgroup>, <datalist> для міст, <textarea> з maxlength, <output> для лічильника, <progress> + <meter> для прогресу.

🌐 Доступність

Всі поля мають <label for>. Групи <radio> і <checkbox> загорнуті у вкладені <fieldset>. autocomplete атрибути. <abbr title> для позначення обов'язкових полів. aria-live для динамічного прогресу.

Корисні посилання

Copyright © 2026