Формы

Модуль 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)
  • Автосохранением черновика
  • Красивыми стилями и анимациями

Настройки

Цветовая схема

Тема