Вёрстка: Доступная HTML-форма обратной связи с CSS

Готовая разметка формы обратной связи с полями имени, e-mail, сообщения, загрузкой файла и согласием на обработку персональных данных. Все элементы имеют связанные подписи, заметный клавиатурный фокус и понятные ограничения.

Когда не подходит: это пример клиентской части. Он не отправляет письма самостоятельно и не заменяет серверный обработчик, проверку данных, CSRF-защиту, антиспам и безопасную обработку файлов.

Готовое решение

HTML-разметка

<form
	method="post"
	action="/feedback/"
	class="feedbackForm"
	enctype="multipart/form-data"
>
	<fieldset class="formFieldset">
		<legend class="formLegend">Отправить сообщение</legend>

		<p class="formHint">
			Поля со знаком <span aria-hidden="true">*</span> обязательны.
		</p>

		<div class="formField">
			<label for="feedbackName">
				Имя <span aria-hidden="true">*</span>
			</label>

			<input
				class="formControl"
				type="text"
				id="feedbackName"
				name="userName"
				autocomplete="name"
				minlength="2"
				maxlength="100"
				required
			>
		</div>

		<div class="formField">
			<label for="feedbackEmail">
				E-mail <span aria-hidden="true">*</span>
			</label>

			<input
				class="formControl"
				type="email"
				id="feedbackEmail"
				name="userEmail"
				autocomplete="email"
				inputmode="email"
				maxlength="254"
				required
			>
		</div>

		<div class="formField">
			<label for="feedbackMessage">
				Сообщение <span aria-hidden="true">*</span>
			</label>

			<textarea
				class="formControl"
				id="feedbackMessage"
				name="userMessage"
				rows="7"
				minlength="10"
				maxlength="5000"
				required
			></textarea>

			<p class="fieldHint" id="feedbackMessageHint">
				От 10 до 5000 символов.
			</p>
		</div>

		<div class="formField">
			<label for="feedbackFile">Прикрепить файл</label>

			<input
				class="fileControl"
				type="file"
				id="feedbackFile"
				name="attachment"
				accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png"
				aria-describedby="feedbackFileHint"
			>

			<p class="fieldHint" id="feedbackFileHint">
				PDF, JPG или PNG, не более 5 МБ.
			</p>
		</div>

		<div class="checkboxField">
			<input
				type="checkbox"
				id="privacyConsent"
				name="privacyConsent"
				value="1"
				required
			>

			<label for="privacyConsent">
				Я согласен с
				<a href="/policy/">политикой обработки персональных данных</a>.
				<span aria-hidden="true">*</span>
			</label>
		</div>

		<button class="submitButton" type="submit">
			Отправить
		</button>
	</fieldset>
</form>

Атрибут enctype="multipart/form-data" нужен только потому, что форма содержит input type="file". Если загрузка файлов не требуется, поле файла и этот атрибут можно удалить.

CSS-стили

:root {
	--formPrimary: #2563eb;
	--formPrimaryHover: #1d4ed8;
	--formBorder: #9ca3af;
	--formText: #1f2937;
	--formMuted: #6b7280;
	--formBackground: #ffffff;
	--formError: #b91c1c;
}

.feedbackForm,
.feedbackForm * {
	box-sizing: border-box;
}

.feedbackForm {
	width: min(100%, 640px);
	margin-inline: auto;
	padding: 24px;
	color: var(--formText);
	background: var(--formBackground);
	border: 1px solid #e5e7eb;
	border-radius: 8px;
	box-shadow: 0 8px 24px rgb(15 23 42 / 8%);
}

.formFieldset {
	min-width: 0;
	margin: 0;
	padding: 0;
	border: 0;
}

.formLegend {
	margin-bottom: 8px;
	font-size: 1.5rem;
	font-weight: 700;
}

.formHint,
.fieldHint {
	margin: 0;
	color: var(--formMuted);
	font-size: 0.875rem;
	line-height: 1.5;
}

.formHint {
	margin-bottom: 24px;
}

.formField {
	display: grid;
	gap: 8px;
	margin-bottom: 20px;
}

.formField label,
.checkboxField label {
	font-weight: 600;
}

.formControl {
	width: 100%;
	min-height: 44px;
	padding: 10px 12px;
	color: var(--formText);
	background: #fff;
	border: 1px solid var(--formBorder);
	border-radius: 6px;
	font: inherit;
}

textarea.formControl {
	min-height: 140px;
	resize: vertical;
}

.formControl:focus,
.fileControl:focus-visible,
.checkboxField input:focus-visible,
.submitButton:focus-visible,
.checkboxField a:focus-visible {
	outline: 3px solid rgb(37 99 235 / 35%);
	outline-offset: 2px;
}

.formControl:focus {
	border-color: var(--formPrimary);
}

.formControl[aria-invalid="true"] {
	border-color: var(--formError);
}

.fieldError {
	margin: 0;
	color: var(--formError);
	font-size: 0.875rem;
	font-weight: 600;
}

.fileControl {
	max-width: 100%;
	font: inherit;
}

.fileControl::file-selector-button {
	margin-right: 12px;
	padding: 8px 12px;
	color: var(--formText);
	background: #f3f4f6;
	border: 1px solid var(--formBorder);
	border-radius: 6px;
	cursor: pointer;
}

.fileControl::file-selector-button:hover {
	background: #e5e7eb;
}

.checkboxField {
	display: grid;
	grid-template-columns: auto 1fr;
	align-items: start;
	gap: 10px;
	margin-bottom: 24px;
}

.checkboxField input {
	width: 20px;
	height: 20px;
	margin: 2px 0 0;
	accent-color: var(--formPrimary);
}

.checkboxField label {
	line-height: 1.5;
}

.submitButton {
	min-height: 44px;
	padding: 10px 20px;
	color: #fff;
	background: var(--formPrimary);
	border: 0;
	border-radius: 6px;
	font: inherit;
	font-weight: 700;
	cursor: pointer;
}

.submitButton:hover {
	background: var(--formPrimaryHover);
}

.submitButton:disabled {
	cursor: not-allowed;
	opacity: 0.65;
}

@media (max-width: 480px) {
	.feedbackForm {
		padding: 16px;
	}
}

@media (prefers-reduced-motion: reduce) {
	.feedbackForm *,
	.feedbackForm *::before,
	.feedbackForm *::after {
		scroll-behavior: auto;
		transition-duration: 0.01ms;
	}
}

@media (forced-colors: active) {
	.formControl:focus,
	.fileControl:focus-visible,
	.checkboxField input:focus-visible,
	.submitButton:focus-visible,
	.checkboxField a:focus-visible {
		outline: 2px solid CanvasText;
	}
}

Нативный чекбокс не скрывается и не заменяется псевдоэлементами. Свойство accent-color меняет его основной цвет, сохраняя привычное поведение в браузере и работу вспомогательных технологий.

Что отправится на сервер

  • userName — имя пользователя;
  • userEmail — адрес электронной почты;
  • userMessage — текст сообщения;
  • attachment — загруженный файл, если он выбран;
  • privacyConsent — значение 1, если пользователь установил флажок.

Если чекбокс не установлен, браузер не отправляет его имя и значение. Сервер должен самостоятельно проверить наличие privacyConsent.

Вывод ошибки рядом с полем

После неудачной серверной проверки верните форму с введёнными безопасно экранированными значениями, добавьте полю aria-invalid="true" и свяжите его с текстом ошибки через aria-describedby:

<div class="formField">
	<label for="feedbackEmail">
		E-mail <span aria-hidden="true">*</span>
	</label>

	<input
		class="formControl"
		type="email"
		id="feedbackEmail"
		name="userEmail"
		value="user@example"
		autocomplete="email"
		aria-invalid="true"
		aria-describedby="feedbackEmailError"
		required
	>

	<p class="fieldError" id="feedbackEmailError">
		Укажите корректный адрес электронной почты.
	</p>
</div>

Одного красного цвета недостаточно: причина ошибки должна быть написана текстом.

CSRF-защита

Если отправка формы изменяет данные, работает в авторизованной области или использует сессию, добавьте одноразовый CSRF-токен. Его генерирует сервер или фреймворк — статическое значение использовать нельзя.

<input
	type="hidden"
	name="csrfToken"
	value="{{ csrfToken }}"
>

При обработке запроса сервер должен сравнить токен с сохранённым значением безопасным способом и отклонить запрос при несовпадении.

Краевые случаи

  • Атрибут accept только подсказывает браузеру допустимые типы файлов. Пользователь может обойти это ограничение.
  • HTML не ограничивает размер файла. Лимит 5 МБ необходимо проверить на сервере и согласовать с настройками веб-сервера и PHP.
  • Значение maxlength улучшает интерфейс, но сервер всё равно должен проверять фактическую длину строки.
  • Поля с required проходят встроенную браузерную проверку, однако HTTP-запрос можно отправить в обход формы.
  • Не используйте placeholder вместо label: подсказка исчезает после начала ввода и не заменяет постоянное название поля.
  • После серверной ошибки сохраняйте введённые имя, e-mail и сообщение, но никогда не пытайтесь восстановить выбранный файл.
  • Кнопку можно временно отключить после отправки через JavaScript, однако сервер обязан самостоятельно защищаться от повторных запросов.

Безопасность

  • Проверяйте все поля на сервере: обязательность, длину, формат и допустимые значения.
  • Экранируйте данные в момент вывода в конкретный контекст. Для HTML-текста обычно используют экранирование специальных символов.
  • Не подставляйте пользовательские значения напрямую в SQL, команды оболочки, HTTP-заголовки или шаблон письма.
  • Для файлов проверяйте расширение, MIME-тип и содержимое; задавайте случайное имя и храните загрузки вне публичного web-root.
  • Ограничьте размер файла, количество загрузок и частоту отправки формы.
  • Не разрешайте исполняемые расширения: .php, .phar, .phtml, .cgi, .sh и аналогичные.
  • Для публичной формы добавьте rate limit, журналирование и антиспам. CAPTCHA должна быть дополнительным уровнем, а не единственной защитой.
  • Не отправляйте пользовательский e-mail напрямую в заголовок From. Используйте адрес своего домена, а пользовательский адрес помещайте в проверенный Reply-To.

Доступность

  • Каждое поле связано с подписью через одинаковые значения for и id.
  • Обязательные поля отмечены визуально и имеют атрибут required.
  • Подсказки связаны с полями через aria-describedby.
  • Фокус не скрывается: при навигации клавишей Tab активный элемент хорошо заметен.
  • Минимальная высота текстовых полей и кнопки составляет 44 пикселя.
  • Пользователь может увеличить высоту textarea по вертикали.

Производительность

Разметка и стили не требуют JavaScript и сторонних библиотек. Основная нагрузка появляется на сервере при отправке писем, проверке антивирусом и обработке файлов. Ограничивайте размер загрузки до начала ресурсоёмких операций.

Совместимость

Форма использует стандартные элементы HTML и работает в актуальных версиях Chrome, Edge, Firefox и Safari. Если браузер не поддерживает accent-color, чекбокс сохранит стандартный внешний вид. Псевдоэлемент ::file-selector-button влияет только на оформление кнопки выбора файла и не мешает работе самого поля.

Как проверить форму

  1. Откройте страницу и последовательно нажимайте Tab. Фокус должен быть виден на каждом поле, ссылке и кнопке.
  2. Нажмите «Отправить» с пустыми обязательными полями и проверьте встроенные сообщения браузера.
  3. Введите неправильный e-mail и убедитесь, что форма не отправляется.
  4. Проверьте минимальную и максимальную длину сообщения.
  5. Попробуйте выбрать разрешённый и запрещённый тип файла.
  6. Отправьте запрос напрямую через DevTools или HTTP-клиент и убедитесь, что сервер повторяет все проверки.
  7. Проверьте файл больше установленного лимита.
  8. Проверьте форму при масштабе страницы 200% и на экране шириной около 320 пикселей.
  9. После серверной ошибки убедитесь, что сообщение понятно, связано с нужным полем и не определяется только цветом.

Альтернативы

  • Убрать загрузку файлов и оставить более простую форму имени, e-mail и сообщения.
  • Отправлять данные через fetch(), если нужен интерфейс без перезагрузки. Серверные проверки при этом остаются обязательными.
  • Использовать готовый компонент формы из дизайн-системы, если в проекте много однотипных форм.
  • Для MODX Revolution подключить серверный обработчик через FormIt с собственными хуками проверки, антиспама и загрузки файлов.

Документация

up: Просмотров: 1.1k
Материал помог? Оценка помогает находить устаревшие и спорные решения.

Комментарии

0

Нашли ошибку или хотите дополнить материал?

Комментариев пока нет.Можно первым дополнить материал или сообщить об ошибке.
Telegram