Модуль 6: Формы и валидация
Научимся создавать доступные, удобные и безопасные формы с правильной валидацией.
Основы HTML-форм
Структура формы
<form action="/submit" method="POST">
<!-- Группировка полей -->
<fieldset>
<legend>Контактная информация</legend>
<!-- Поле с label -->
<div class="form-group">
<label for="name">Имя</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
</fieldset>
<button type="submit">Отправить</button>
</form>
Атрибуты form
| Атрибут | Описание | Значения |
|---|---|---|
action |
URL для отправки | URL или пустая строка |
method |
HTTP метод | GET, POST |
enctype |
Кодировка данных | application/x-www-form-urlencoded, multipart/form-data, text/plain |
novalidate |
Отключить браузерную валидацию | Boolean |
autocomplete |
Автозаполнение | on, off |
<!-- Форма с загрузкой файлов -->
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/*">
<button type="submit">Загрузить</button>
</form>
<!-- Форма без валидации (для кастомной) -->
<form novalidate>
<!-- ... -->
</form>
Типы input
Текстовые типы
<!-- Базовый текст -->
<input type="text" placeholder="Введите текст">
<!-- Email с валидацией -->
<input type="email" placeholder="user@example.com">
<!-- Пароль (скрытые символы) -->
<input type="password" minlength="8">
<!-- URL -->
<input type="url" placeholder="https://...">
<!-- Телефон (нет валидации, но правильная клавиатура на мобильных) -->
<input type="tel" placeholder="+7 (999) 123-45-67">
<!-- Поиск (с кнопкой очистки) -->
<input type="search" placeholder="Поиск...">
Числовые типы
<!-- Число -->
<input type="number" min="0" max="100" step="1">
<!-- Диапазон (слайдер) -->
<input type="range" min="0" max="100" value="50">
<!-- Дата -->
<input type="date" min="2020-01-01" max="2030-12-31">
<!-- Время -->
<input type="time" step="900"> <!-- Шаг 15 минут -->
<!-- Дата и время -->
<input type="datetime-local">
<!-- Месяц -->
<input type="month">
<!-- Неделя -->
<input type="week">
Выбор
<!-- Чекбокс -->
<label>
<input type="checkbox" name="agree" required>
Согласен с условиями
</label>
<!-- Группа чекбоксов -->
<fieldset>
<legend>Интересы</legend>
<label><input type="checkbox" name="interests" value="coding"> Программирование</label>
<label><input type="checkbox" name="interests" value="design"> Дизайн</label>
<label><input type="checkbox" name="interests" value="music"> Музыка</label>
</fieldset>
<!-- Радиокнопки (только один выбор) -->
<fieldset>
<legend>Пол</legend>
<label><input type="radio" name="gender" value="male"> Мужской</label>
<label><input type="radio" name="gender" value="female"> Женский</label>
</fieldset>
Специальные типы
<!-- Файл -->
<input type="file" accept=".pdf,.doc,.docx">
<input type="file" accept="image/*" multiple>
<!-- Цвет -->
<input type="color" value="#6c63ff">
<!-- Скрытое поле -->
<input type="hidden" name="token" value="abc123">
Атрибуты input
| Атрибут | Описание |
|---|---|
required |
Обязательное поле |
disabled |
Отключено (не отправляется) |
readonly |
Только чтение (отправляется) |
placeholder |
Подсказка |
value |
Значение по умолчанию |
name |
Имя для отправки |
id |
Идентификатор для label |
autocomplete |
Тип автозаполнения |
autofocus |
Автофокус при загрузке |
Autocomplete
<!-- Помогает браузеру правильно автозаполнять -->
<input type="text" name="name" autocomplete="name">
<input type="email" autocomplete="email">
<input type="tel" autocomplete="tel">
<input type="text" autocomplete="address-line1">
<input type="text" autocomplete="postal-code">
<input type="text" autocomplete="cc-number"> <!-- Номер карты -->
<input type="password" autocomplete="new-password"> <!-- Новый пароль -->
<input type="password" autocomplete="current-password"> <!-- Текущий пароль -->
<input type="text" autocomplete="one-time-code"> <!-- OTP код -->
Элементы форм
textarea
<label for="message">Сообщение</label>
<textarea
id="message"
name="message"
rows="5"
cols="40"
maxlength="500"
placeholder="Введите сообщение..."
></textarea>
select
<!-- Простой select -->
<label for="country">Страна</label>
<select id="country" name="country" required>
<option value="">Выберите страну</option>
<option value="ru">Россия</option>
<option value="us">США</option>
<option value="de">Германия</option>
</select>
<!-- С группами -->
<select name="car">
<optgroup label="Немецкие">
<option value="bmw">BMW</option>
<option value="mercedes">Mercedes</option>
</optgroup>
<optgroup label="Японские">
<option value="toyota">Toyota</option>
<option value="honda">Honda</option>
</optgroup>
</select>
<!-- Множественный выбор -->
<select name="skills" multiple size="5">
<option value="html">HTML</option>
<option value="css">CSS</option>
<option value="js">JavaScript</option>
</select>
datalist (автодополнение)
<label for="browser">Браузер</label>
<input list="browsers" id="browser" name="browser">
<datalist id="browsers">
<option value="Chrome">
<option value="Firefox">
<option value="Safari">
<option value="Edge">
</datalist>
output
<form oninput="result.value = parseInt(a.value) + parseInt(b.value)">
<input type="range" id="a" value="50"> +
<input type="number" id="b" value="50"> =
<output name="result" for="a b">100</output>
</form>
progress и meter
<!-- Прогресс загрузки -->
<label for="file-progress">Загрузка:</label>
<progress id="file-progress" value="70" max="100">70%</progress>
<!-- Индикатор значения -->
<label for="disk">Использовано диска:</label>
<meter id="disk" value="0.7" min="0" max="1" low="0.3" high="0.7" optimum="0.5">70%</meter>
Нативная HTML5 валидация
Атрибуты валидации
<!-- Обязательное поле -->
<input type="text" required>
<!-- Минимальная/максимальная длина -->
<input type="text" minlength="3" maxlength="50">
<!-- Диапазон чисел -->
<input type="number" min="18" max="100">
<!-- Шаг -->
<input type="number" step="0.01"> <!-- Для цен -->
<!-- Паттерн (регулярное выражение) -->
<input type="text" pattern="[A-Za-z]{3,}" title="Минимум 3 буквы">
<!-- Телефон с паттерном -->
<input type="tel" pattern="\+7\s?\(\d{3}\)\s?\d{3}-?\d{2}-?\d{2}"
title="+7 (999) 123-45-67">
Популярные паттерны
<!-- Только буквы -->
<input pattern="[A-Za-zА-Яа-яЁё\s]+">
<!-- Только цифры -->
<input pattern="[0-9]+">
<!-- Имя пользователя (буквы, цифры, подчёркивание) -->
<input pattern="[a-zA-Z0-9_]{3,20}">
<!-- Пароль (мин 8 символов, буквы и цифры) -->
<input type="password" pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
title="Минимум 8 символов, включая цифры, строчные и заглавные буквы">
<!-- Почтовый индекс РФ -->
<input pattern="[0-9]{6}">
<!-- ИНН (10 или 12 цифр) -->
<input pattern="[0-9]{10}|[0-9]{12}">
Важно
Нативная валидация — это первая линия защиты. Всегда валидируйте данные также на сервере!
CSS для валидации
Псевдоклассы валидации
/* Валидное поле */
input:valid {
border-color: #22c55e;
}
/* Невалидное поле */
input:invalid {
border-color: #ef4444;
}
/* Обязательное поле */
input:required {
border-left: 3px solid var(--primary);
}
/* Необязательное поле */
input:optional {
border-left: 3px solid #ccc;
}
/* Поле в фокусе */
input:focus {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
/* Заполнен плейсхолдер */
input:placeholder-shown {
/* Пока placeholder виден */
}
/* Отключенное поле */
input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Только для чтения */
input:read-only {
background: #f5f5f5;
}
Проблема :invalid при загрузке
По умолчанию :invalid срабатывает сразу. Решение — показывать ошибку только после взаимодействия:
/* Вариант 1: Через :not(:placeholder-shown) */
input:not(:placeholder-shown):invalid {
border-color: #ef4444;
}
/* Вариант 2: Через :focus */
input:focus:invalid {
border-color: #ef4444;
}
/* Вариант 3: Через data-атрибут (добавляется JS после blur) */
input[data-touched]:invalid {
border-color: #ef4444;
}
/* Вариант 4: Современный :user-invalid (Chrome 119+) */
input:user-invalid {
border-color: #ef4444;
}
Стилизация сообщений об ошибках
.form-group {
position: relative;
margin-bottom: 1.5rem;
}
.form-group input:user-invalid {
border-color: #ef4444;
background: #fef2f2;
}
.form-group input:user-invalid + .error-message {
display: block;
}
.error-message {
display: none;
color: #ef4444;
font-size: 0.875rem;
margin-top: 0.25rem;
}
/* Иконки валидации */
.form-group input:user-valid {
background-image: url("data:image/svg+xml,...");
background-repeat: no-repeat;
background-position: right 0.75rem center;
padding-right: 2.5rem;
}
JavaScript валидация
Базовая валидация
const form = document.querySelector('form');
const emailInput = document.getElementById('email');
form.addEventListener('submit', (e) => {
// Собственная валидация
if (!validateEmail(emailInput.value)) {
e.preventDefault();
showError(emailInput, 'Введите корректный email');
return;
}
// Если всё ок, форма отправится
});
function validateEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
function showError(input, message) {
const errorEl = input.parentElement.querySelector('.error-message');
errorEl.textContent = message;
input.classList.add('invalid');
}
Валидация в реальном времени
const inputs = document.querySelectorAll('input[required]');
inputs.forEach(input => {
// Валидация при потере фокуса
input.addEventListener('blur', () => {
validateInput(input);
input.dataset.touched = 'true';
});
// Валидация при вводе (после первого blur)
input.addEventListener('input', () => {
if (input.dataset.touched) {
validateInput(input);
}
});
});
function validateInput(input) {
const isValid = input.checkValidity();
const errorEl = input.parentElement.querySelector('.error-message');
if (!isValid) {
input.classList.add('invalid');
input.classList.remove('valid');
errorEl.textContent = getErrorMessage(input);
} else {
input.classList.remove('invalid');
input.classList.add('valid');
errorEl.textContent = '';
}
}
function getErrorMessage(input) {
if (input.validity.valueMissing) {
return 'Это поле обязательно';
}
if (input.validity.typeMismatch) {
return 'Неверный формат';
}
if (input.validity.tooShort) {
return `Минимум ${input.minLength} символов`;
}
if (input.validity.tooLong) {
return `Максимум ${input.maxLength} символов`;
}
if (input.validity.patternMismatch) {
return input.title || 'Неверный формат';
}
return 'Некорректное значение';
}
Валидация паролей
const password = document.getElementById('password');
const confirmPassword = document.getElementById('confirm-password');
const requirements = document.querySelectorAll('.requirement');
password.addEventListener('input', () => {
const value = password.value;
// Проверка требований
updateRequirement('length', value.length >= 8);
updateRequirement('lowercase', /[a-z]/.test(value));
updateRequirement('uppercase', /[A-Z]/.test(value));
updateRequirement('number', /\d/.test(value));
updateRequirement('special', /[!@#$%^&*]/.test(value));
});
function updateRequirement(name, isValid) {
const el = document.querySelector(`[data-requirement="${name}"]`);
el.classList.toggle('valid', isValid);
el.classList.toggle('invalid', !isValid);
}
// Проверка совпадения паролей
confirmPassword.addEventListener('input', () => {
if (confirmPassword.value !== password.value) {
confirmPassword.setCustomValidity('Пароли не совпадают');
} else {
confirmPassword.setCustomValidity('');
}
});
<!-- HTML для требований к паролю -->
<div class="password-requirements">
<p class="requirement" data-requirement="length">
<span class="icon"></span> Минимум 8 символов
</p>
<p class="requirement" data-requirement="lowercase">
<span class="icon"></span> Строчная буква
</p>
<p class="requirement" data-requirement="uppercase">
<span class="icon"></span> Заглавная буква
</p>
<p class="requirement" data-requirement="number">
<span class="icon"></span> Цифра
</p>
<p class="requirement" data-requirement="special">
<span class="icon"></span> Спецсимвол (!@#$%^&*)
</p>
</div>
Constraint Validation API
Свойства validity
const input = document.querySelector('input');
// Объект ValidityState
const validity = input.validity;
validity.valid; // true если всё ок
validity.valueMissing; // required не заполнен
validity.typeMismatch; // неверный тип (email, url)
validity.patternMismatch;// не соответствует pattern
validity.tooLong; // превышает maxlength
validity.tooShort; // меньше minlength
validity.rangeOverflow; // больше max
validity.rangeUnderflow; // меньше min
validity.stepMismatch; // не соответствует step
validity.badInput; // невозможно преобразовать
validity.customError; // установлена своя ошибка
Методы
const input = document.querySelector('input');
const form = document.querySelector('form');
// Проверить валидность
input.checkValidity(); // true/false, триггерит 'invalid' event
input.reportValidity(); // true/false, показывает браузерную подсказку
// Проверить всю форму
form.checkValidity();
form.reportValidity();
// Установить свою ошибку
input.setCustomValidity('Пользователь с таким email уже существует');
// Сбросить свою ошибку
input.setCustomValidity('');
// Получить сообщение об ошибке
input.validationMessage; // Текст ошибки
Кастомные сообщения об ошибках
const form = document.querySelector('form');
form.addEventListener('submit', (e) => {
const inputs = form.querySelectorAll('input, select, textarea');
let firstInvalid = null;
inputs.forEach(input => {
// Сбрасываем кастомные ошибки
input.setCustomValidity('');
if (!input.validity.valid) {
if (!firstInvalid) firstInvalid = input;
// Устанавливаем кастомное сообщение
if (input.validity.valueMissing) {
input.setCustomValidity(`Пожалуйста, заполните поле "${input.labels[0]?.textContent || input.name}"`);
}
}
});
if (firstInvalid) {
e.preventDefault();
firstInvalid.reportValidity();
firstInvalid.focus();
}
});
Событие invalid
const input = document.querySelector('input');
input.addEventListener('invalid', (e) => {
// Отменить браузерное всплывающее окно
e.preventDefault();
// Своя логика показа ошибки
showCustomError(input, input.validationMessage);
});
function showCustomError(input, message) {
const errorEl = input.nextElementSibling;
errorEl.textContent = message;
errorEl.hidden = false;
input.setAttribute('aria-invalid', 'true');
input.setAttribute('aria-describedby', errorEl.id);
}
Доступность форм (a11y)
Label — обязательно!
<!-- Правильно: явная связь -->
<label for="email">Email</label>
<input type="email" id="email" name="email">
<!-- Правильно: вложенный input -->
<label>
Email
<input type="email" name="email">
</label>
<!-- Неправильно: placeholder вместо label -->
<input type="email" placeholder="Email">
<!-- Визуально скрытый label -->
<label for="search" class="visually-hidden">Поиск</label>
<input type="search" id="search" placeholder="Поиск...">
/* Визуально скрытый, но доступный для скринридеров */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
ARIA атрибуты
<!-- Обязательное поле -->
<input type="email" required aria-required="true">
<!-- Невалидное поле -->
<input type="email" aria-invalid="true" aria-describedby="email-error">
<span id="email-error" role="alert">Введите корректный email</span>
<!-- Подсказка -->
<input type="password" aria-describedby="password-hint">
<span id="password-hint">Минимум 8 символов</span>
<!-- Автозаполнение для скринридеров -->
<input type="text" aria-autocomplete="list" aria-controls="suggestions">
<!-- Отключенная секция -->
<fieldset disabled aria-disabled="true">
<legend>Недоступно</legend>
</fieldset>
Live regions для ошибок
<!-- Объявление ошибок скринридеру -->
<div aria-live="polite" aria-atomic="true" class="error-summary">
<!-- JS добавит сюда список ошибок -->
</div>
<!-- role="alert" для немедленного объявления -->
<span role="alert" class="error">Пароли не совпадают</span>
Фокус и навигация
/* Видимый фокус */
input:focus,
select:focus,
textarea:focus,
button:focus {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
/* Не убирайте outline! Если нужен кастомный, замените */
input:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.3);
}
// Фокус на первую ошибку после submit
form.addEventListener('submit', (e) => {
const firstInvalid = form.querySelector(':invalid');
if (firstInvalid) {
e.preventDefault();
firstInvalid.focus();
// Объявить ошибку скринридеру
announceError(firstInvalid);
}
});
function announceError(input) {
const liveRegion = document.querySelector('[aria-live]');
liveRegion.textContent = `Ошибка в поле ${input.labels[0].textContent}: ${input.validationMessage}`;
}
Чек-лист доступности форм
- У каждого поля есть label
- Ошибки объявляются скринридерам (aria-live, role="alert")
- Видимый фокус на всех элементах
- Форма работает с клавиатуры
- Достаточный контраст текста
- Ошибки описаны текстом, не только цветом
- Touch targets минимум 44x44px
UX паттерны форм
Inline валидация
Показывайте ошибки рядом с полем, сразу после взаимодействия:
// Валидация на blur (потеря фокуса)
input.addEventListener('blur', validate);
// НЕ валидируйте при каждом keyup — это раздражает
// Валидация на input — только после первой ошибки
let hasError = false;
input.addEventListener('input', () => {
if (hasError) validate();
});
function validate() {
hasError = !input.checkValidity();
// показать/скрыть ошибку
}
Floating labels
<div class="floating-label">
<input type="email" id="email" placeholder=" " required>
<label for="email">Email</label>
</div>
.floating-label {
position: relative;
}
.floating-label input {
padding: 1.5rem 1rem 0.5rem;
}
.floating-label label {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
transition: all 0.2s;
pointer-events: none;
color: #666;
}
/* Когда есть фокус или значение */
.floating-label input:focus + label,
.floating-label input:not(:placeholder-shown) + label {
top: 0.5rem;
transform: translateY(0);
font-size: 0.75rem;
color: var(--primary);
}
Show/Hide пароль
<div class="password-field">
<input type="password" id="password">
<button type="button" class="toggle-password" aria-label="Показать пароль">
<svg class="icon-eye">...</svg>
<svg class="icon-eye-off" hidden>...</svg>
</button>
</div>
document.querySelectorAll('.toggle-password').forEach(btn => {
btn.addEventListener('click', () => {
const input = btn.previousElementSibling;
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
btn.setAttribute('aria-label', isPassword ? 'Скрыть пароль' : 'Показать пароль');
btn.querySelector('.icon-eye').hidden = !isPassword;
btn.querySelector('.icon-eye-off').hidden = isPassword;
});
});
Автосохранение
const form = document.querySelector('form');
const STORAGE_KEY = 'form_draft';
// Загрузка сохранённых данных
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const data = JSON.parse(saved);
Object.entries(data).forEach(([name, value]) => {
const input = form.querySelector(`[name="${name}"]`);
if (input) input.value = value;
});
}
// Автосохранение при изменении
form.addEventListener('input', debounce(() => {
const formData = new FormData(form);
const data = Object.fromEntries(formData);
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}, 500));
// Очистка после успешной отправки
form.addEventListener('submit', () => {
localStorage.removeItem(STORAGE_KEY);
});
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
Многошаговые формы
const steps = document.querySelectorAll('.form-step');
const nextBtns = document.querySelectorAll('[data-next]');
const prevBtns = document.querySelectorAll('[data-prev]');
let currentStep = 0;
function showStep(index) {
steps.forEach((step, i) => {
step.hidden = i !== index;
step.setAttribute('aria-hidden', i !== index);
});
// Обновить индикатор прогресса
updateProgress(index);
}
nextBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Валидация текущего шага
const currentFields = steps[currentStep].querySelectorAll('input, select');
const isValid = [...currentFields].every(f => f.checkValidity());
if (isValid && currentStep < steps.length - 1) {
currentStep++;
showStep(currentStep);
}
});
});
prevBtns.forEach(btn => {
btn.addEventListener('click', () => {
if (currentStep > 0) {
currentStep--;
showStep(currentStep);
}
});
});
Безопасность форм
CSRF защита
<!-- Токен от сервера -->
<form method="POST" action="/submit">
<input type="hidden" name="_csrf" value="abc123xyz">
<!-- остальные поля -->
</form>
Санитизация ввода
// Опасно: XSS уязвимость
element.innerHTML = userInput;
// Безопасно
element.textContent = userInput;
// Если нужен HTML — санитизация
function sanitize(html) {
const temp = document.createElement('div');
temp.textContent = html;
return temp.innerHTML;
}
// Или используйте DOMPurify
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);
Защита от автозаполнения паролей
<!-- Для полей с чувствительными данными -->
<input type="text" autocomplete="off">
<!-- Для нового пароля (не автозаполнять старый) -->
<input type="password" autocomplete="new-password">
Honeypot против ботов
<!-- Скрытое поле, которое бот заполнит -->
<div class="honey" aria-hidden="true">
<label for="website">Website</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
.honey {
position: absolute;
left: -9999px;
}
// На сервере проверяем
if (formData.website) {
// Это бот — отклонить
return res.status(400).json({ error: 'Bot detected' });
}
Rate limiting
// Простой rate limiter на клиенте
const submitBtn = document.querySelector('button[type="submit"]');
let lastSubmit = 0;
const COOLDOWN = 3000; // 3 секунды
form.addEventListener('submit', (e) => {
const now = Date.now();
if (now - lastSubmit < COOLDOWN) {
e.preventDefault();
alert('Подождите перед повторной отправкой');
return;
}
lastSubmit = now;
submitBtn.disabled = true;
});
Реальные примеры форм
Форма регистрации
<form id="register-form" novalidate>
<div class="form-group">
<label for="reg-name">Имя</label>
<input
type="text"
id="reg-name"
name="name"
required
minlength="2"
autocomplete="name"
>
<span class="error-message" id="name-error"></span>
</div>
<div class="form-group">
<label for="reg-email">Email</label>
<input
type="email"
id="reg-email"
name="email"
required
autocomplete="email"
>
<span class="error-message" id="email-error"></span>
</div>
<div class="form-group">
<label for="reg-password">Пароль</label>
<div class="password-field">
<input
type="password"
id="reg-password"
name="password"
required
minlength="8"
autocomplete="new-password"
>
<button type="button" class="toggle-password">👁</button>
</div>
<div class="password-strength"></div>
<span class="error-message" id="password-error"></span>
</div>
<div class="form-group">
<label for="reg-confirm">Подтвердите пароль</label>
<input
type="password"
id="reg-confirm"
name="confirm"
required
autocomplete="new-password"
>
<span class="error-message" id="confirm-error"></span>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" name="agree" required>
Согласен с <a href="/terms">условиями</a>
</label>
</div>
<button type="submit" class="btn-primary">Зарегистрироваться</button>
</form>
Форма оформления заказа
<form id="checkout-form">
<fieldset>
<legend>Контактные данные</legend>
<div class="form-row">
<div class="form-group">
<label for="first-name">Имя</label>
<input type="text" id="first-name" name="firstName" required autocomplete="given-name">
</div>
<div class="form-group">
<label for="last-name">Фамилия</label>
<input type="text" id="last-name" name="lastName" required autocomplete="family-name">
</div>
</div>
<div class="form-group">
<label for="phone">Телефон</label>
<input type="tel" id="phone" name="phone" required autocomplete="tel"
pattern="\+7\d{10}" placeholder="+79991234567">
</div>
</fieldset>
<fieldset>
<legend>Адрес доставки</legend>
<div class="form-group">
<label for="city">Город</label>
<input type="text" id="city" name="city" required autocomplete="address-level2">
</div>
<div class="form-group">
<label for="address">Адрес</label>
<input type="text" id="address" name="address" required autocomplete="street-address">
</div>
<div class="form-row">
<div class="form-group">
<label for="postal">Индекс</label>
<input type="text" id="postal" name="postal" pattern="[0-9]{6}" autocomplete="postal-code">
</div>
</div>
</fieldset>
<button type="submit">Оформить заказ</button>
</form>
Итоговый проект
Создайте полнофункциональную форму регистрации с:
- Валидацией в реальном времени
- Индикатором силы пароля
- Проверкой совпадения паролей
- Доступностью (label, aria, focus)
- Автосохранением черновика
- Красивыми стилями и анимациями