Тренажёр HTML-форм и валидации

Почему браузер не принимает значение? Чем отличаются disabled и readonly? Как связать подпись с полем и какие данные попадут в форму? Соберите учебную форму, исследуйте её поведение и закрепите знания в заданиях.

Как пользоваться

В режиме «Исследовать» начните с одного поля, регистрации, заказа или обратной связи. Добавляйте поля и меняйте их порядок, выбирайте настраиваемое поле, задавайте атрибуты и вводите пробные значения в превью. Настройки типов и ограничений применяются к HTML, а проверку выполняет браузер.

Кнопка «Проверить форму» показывает ошибки и переводит фокус на первое ошибочное поле. «Пробная отправка» внутри превью демонстрирует встроенную проверку перед событием submit; отправка перехватывается локально. Сводка ошибок содержит ссылки на поля. Пройдите форму клавишами Tab и Shift+Tab, попробуйте нажимать подписи.

Откройте инспектор, чтобы увидеть флаги ValidityState, сообщение браузера и связь label с полем. Таблица FormData показывает текущие пары name — value. Разбор «Поле → данные формы» объясняет, почему каждое поле включено или исключено; нажмите его подпись, чтобы перейти к полю. «Очистить ввод» возвращает начальные значения; «Сбросить настройки» возвращает исходную форму. Ввод не сохраняется между посещениями.

В режиме «Практиковаться» исправьте форму по условию и нажмите «Проверить». Подсказки и разбор не засчитывают решение. Галочки и прогресс сохраняются в браузере; сброс удаляет только отметки этого тренажёра. Ссылка на выбранное задание содержит его ID, без введённых значений.

В заданиях «Исправь форму» сначала воспроизведите описанную неисправность в превью. Затем измените настройки, повторите действие и проверьте решение. «Сбросить настройки» позволяет снова увидеть исходную ошибку, сохраняя отметки решённых заданий.

Во вкладке «Практика создания HTML-форм» исправляйте разметку в редакторе с подсветкой. Нажмите «Обновить превью», попробуйте заполнить форму и затем «Проверить решение». До обновления проверка недоступна. «Вернуть исходный HTML» начинает текущую задачу заново, сохраняя прогресс. В итоговых заданиях регистрации, оформления заказа и обратной связи начните с пустой формы, выполните список требований и раскрывайте подсказки постепенно; просмотр подсказок не засчитывает решение. Неподдерживаемая разметка отмечается в редакторе; последний результат остаётся в превью.

Можно добавить оформление полей, изменить акцент, скругление, отступы и число колонок. HTML, CSS и JavaScript доступны с подсветкой и копированием; скачивание сохраняет работающий учебный пример целиком.

В HTML-практике можно скачать самостоятельный HTML-файл с оформлением и перехватом отправки. Введённые в превью значения в файл не попадают. Блок «Браузер и сервер» имитирует задержку, отказ и успешный ответ: попробуйте отмену и повторную попытку. Реальных запросов нет. Подробная проверка раскрывается при ошибках и сворачивается после успешного решения.

Что важно понимать о проверке форм
  • required запрещает отсутствие значения. Тип email и pattern сами по себе не делают поле обязательным.
  • pattern проверяет всё значение. В лаборатории доступны готовые учебные шаблоны; они не являются универсальными правилами для имён, телефонов или почтовых адресов.
  • minlength и maxlength зависят от пользовательского ввода. Программно заданное начальное значение может не давать ошибку длины; браузер обычно не позволяет набрать больше maxlength.
  • disabled исключает поле из проверки и данных формы. readonly для поддерживаемых типов исключает поле из проверки, но сохраняет значение в FormData.
  • novalidate пропускает автоматическую проверку при отправке, но не отключает ограничения или явный вызов проверки. Непустое сообщение setCustomValidity сохраняет ошибку, пока его не очистят.
  • Связка label[for] и id даёт полю подпись. Подсказки и ошибки подключаются через aria-describedby; aria-invalid появляется при показе ошибки.

Лаборатория работает с текстом, почтой, URL, телефоном, паролем, числом, датой, checkbox, radio, select, textarea, range и файловыми полями. В форме до восьми полей. Для файлов показываются только локальные сведения: имя, размер и MIME-тип. Содержимое не читается и не отправляется; сетевых запросов нет. На реальном сайте проверка в браузере дополняет обязательную проверку на сервере.

Для вашего удобства рекомендую использовать полноэкранный режим.

Формы и валидация

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

Начать с формы:

Вводите учебные данные в превью. Они не отправляются и не сохраняются. Экспорт содержит настройки и начальные значения, а не текущий ввод.

Результат проверки поля

Все флаги ValidityState

    Проверяется фактический ввод браузера. Minlength срабатывает при пользовательском вводе; программное начальное значение не всегда даёт tooShort. Maxlength обычно не позволяет ввести лишние символы.

    Поля формы

    До восьми полей. «Выше» и «Ниже» меняют также порядок обхода клавишей Tab.

    Настройки поля
    Ограничения и состояние
    Справочник: min
    Справочник: max
    Справочник: maxlength

    Пустое ограничение удаляет атрибут. Пользовательская ошибка блокирует поле, пока вы не очистите сообщение setCustomValidity. Неактуальные для выбранного типа атрибуты не попадают в HTML.

    Accept — подсказка диалогу выбора, а не проверка содержимого. Расширение и MIME-тип могут быть неверными или отсутствовать. Required проверяет только наличие файла. На сервере проверяют содержимое, размер и допустимый формат.

    Оформление и проверка при отправке

    Novalidate влияет на пробную отправку, но не отключает ValidityState и явную проверку кнопкой «Проверить форму».

    Какие данные попадут в FormData
    Текущие пары name — value
    NameValue

    Почему поле входит в данные? Выберите поле в таблице, чтобы перейти к нему в превью. Проверка значения и включение в FormData — разные вещи: ошибочное поле тоже может присутствовать в данных.

    Поле → данные формы
    ПолеРезультатПричина

    Повторяющиеся name создают отдельные пары. Кнопки отправки не включаются в показанный FormData(form): для их данных нужен конкретный submitter.

    Отключённые поля, поля без name и неотмеченные checkbox/radio не включаются. Readonly включается. Повторяющиеся имена сохраняются отдельными строками. Пароли в этой таблице скрыты.

    HTML, CSS и JavaScript формы
    HTML
    <form id="learning-form">
    <div id="form-errors" role="alert" tabindex="-1" hidden></div>
    <div class="form-field" data-field="field-1">
    <label for="field-1">Электронная почта (обязательно)</label>
    <input id="field-1" type="email" name="email" autocomplete="email" required aria-describedby="field-1-hint">
    <small id="field-1-hint">Например, reader@example.com</small>
    <p id="field-1-error" class="field-error" hidden></p>
    </div>
    <button type="submit">Пробная отправка</button>
    <button type="reset">Очистить ввод</button>
    <p id="form-result" role="status"></p>
    </form>
    CSS
    :root { color-scheme: light dark; }
    body { margin: 16px; font: 16px/1.5 system-ui, sans-serif; }
    form { max-width: 680px; }
    .form-field { margin-block: 12px; min-width: 0; }
    label, small { display: block; }
    input:not([type="checkbox"]):not([type="radio"]), select, textarea { max-width: 100%; box-sizing: border-box; }
    input[type="checkbox"] + label, input[type="radio"] + label { display: inline; }
    fieldset { min-width: 0; }
    .field-error { color: light-dark(#a12622, #ffb0aa); }
    #form-errors { border: 2px solid light-dark(#a12622, #ffb0aa); padding: 12px; margin-bottom: 16px; }
    [hidden] { display: none !important; }
    :focus-visible { outline: 3px solid light-dark(#1759a2, #8ecaff); outline-offset: 3px; }
    JavaScript
    // Учебная отправка перехватывается: сервер не используется.
    function bindLearningForm(doc, customErrors = {}, changed = () => void 0) {
    	const form = doc.querySelector("#learning-form");
    	const summary = doc.querySelector("#form-errors");
    	const status = doc.querySelector("#form-result");
    	const touched = /* @__PURE__ */ new Set();
    	let submitted = false;
    	let requested = false;
    	const controls = () => [...doc.querySelectorAll("input, select, textarea")];
    	function message(control) {
    		const v = control.validity;
    		if (!control.willValidate || v.valid) return "";
    		if (v.customError) return control.validationMessage;
    		if (v.valueMissing) return "Заполните поле или выберите вариант.";
    		if (v.typeMismatch) return control.type === "email" ? "Введите адрес электронной почты, например reader@example.com." : "Введите полный URL, например https://example.com.";
    		if (v.patternMismatch) return "Значение не соответствует шаблону pattern.";
    		if (v.tooShort) return "Введите не менее " + control.getAttribute("minlength") + " символов.";
    		if (v.tooLong) return "Введите не более " + control.getAttribute("maxlength") + " символов.";
    		if (v.rangeUnderflow) return "Значение должно быть не меньше " + control.getAttribute("min") + ".";
    		if (v.rangeOverflow) return "Значение должно быть не больше " + control.getAttribute("max") + ".";
    		if (v.stepMismatch) return "Значение не попадает в шаг step от базы (min, начальное value или 0).";
    		if (v.badInput) return "Браузер не может преобразовать ввод в значение этого типа.";
    		return control.validationMessage;
    	}
    	function render(focus = false) {
    		for (const input of doc.querySelectorAll("input[type=\"file\"]")) {
    			const list = input.closest("[data-field]")?.querySelector("[data-file-list]");
    			if (!list) continue;
    			const files = [...input.files ?? []];
    			const signature = files.map((file) => [
    				file.name,
    				file.size,
    				file.type,
    				file.lastModified
    			].join(":")).join("|");
    			if (list.dataset.files === signature) continue;
    			list.dataset.files = signature;
    			list.replaceChildren();
    			if (!files.length) {
    				list.textContent = "Файлы не выбраны.";
    				continue;
    			}
    			const heading = doc.createElement("p");
    			heading.textContent = "Выбрано файлов: " + files.length;
    			const items = doc.createElement("ul");
    			for (const file of files) {
    				const item = doc.createElement("li");
    				item.textContent = file.name + " · " + file.size + " байт · " + (file.type || "MIME-тип не указан");
    				items.append(item);
    			}
    			list.append(heading, items);
    		}
    		const invalid = controls().filter((control) => control.willValidate && !control.validity.valid);
    		for (const wrapper of doc.querySelectorAll("[data-field]")) {
    			const local = [...wrapper.querySelectorAll("input,select,textarea")];
    			const failed = local.find((control) => control.willValidate && !control.validity.valid);
    			const show = !!failed && (submitted || local.some((control) => touched.has(control.id)));
    			const error = wrapper.querySelector(".field-error");
    			error.textContent = show ? message(failed) : "";
    			error.hidden = !show;
    			for (const control of local) {
    				if (show) control.setAttribute("aria-invalid", "true");
    				else control.removeAttribute("aria-invalid");
    				const ids = [wrapper.querySelector("small")?.id, show ? error.id : ""].filter(Boolean).join(" ");
    				if (ids) control.setAttribute("aria-describedby", ids);
    				else control.removeAttribute("aria-describedby");
    			}
    		}
    		summary.replaceChildren();
    		summary.hidden = !submitted || invalid.length === 0;
    		if (!summary.hidden) {
    			const heading = doc.createElement("strong");
    			heading.textContent = "Исправьте ошибки:";
    			summary.append(heading);
    			const list = doc.createElement("ul");
    			const seen = /* @__PURE__ */ new Set();
    			for (const control of invalid) {
    				const wrapper = control.closest("[data-field]");
    				if (seen.has(wrapper.dataset.field)) continue;
    				seen.add(wrapper.dataset.field);
    				const item = doc.createElement("li"), link = doc.createElement("a");
    				link.href = "#" + control.id;
    				link.textContent = (wrapper.querySelector("legend,label")?.textContent ?? control.name) + ": " + message(control);
    				link.addEventListener("click", (event) => {
    					event.preventDefault();
    					control.focus();
    				});
    				item.append(link);
    				list.append(item);
    			}
    			summary.append(list);
    		}
    		if (focus && invalid.length) invalid[0].focus();
    		changed();
    		return invalid.length === 0;
    	}
    	function setErrors(errors) {
    		customErrors = errors;
    		for (const control of controls()) {
    			const id = control.closest("[data-field]").dataset.field;
    			control.setCustomValidity(customErrors[id] ?? "");
    		}
    	}
    	EventTarget.prototype.addEventListener.call(form, "input", (event) => {
    		const control = event.target;
    		if (![
    			"INPUT",
    			"SELECT",
    			"TEXTAREA"
    		].includes(control.tagName)) return;
    		touched.add(control.id);
    		status.textContent = "";
    		render();
    	});
    	EventTarget.prototype.addEventListener.call(form, "change", () => render());
    	EventTarget.prototype.addEventListener.call(form, "click", (event) => {
    		if (event.target.closest("button[type=\"submit\"]")) requested = true;
    	});
    	EventTarget.prototype.addEventListener.call(form, "keydown", (event) => {
    		if (event.key === "Enter" && event.target.tagName === "INPUT") requested = true;
    	});
    	EventTarget.prototype.addEventListener.call(form, "invalid", (event) => {
    		event.preventDefault();
    		if (requested) {
    			submitted = true;
    			queueMicrotask(() => {
    				render(true);
    				requested = false;
    				status.textContent = "Отправка остановлена: исправьте отмеченные поля.";
    			});
    		}
    	}, true);
    	EventTarget.prototype.addEventListener.call(form, "submit", (event) => {
    		event.preventDefault();
    		requested = false;
    		submitted = true;
    		const valid = render();
    		status.textContent = valid ? "Проверка пройдена. Учебная отправка перехвачена: данные никуда не отправлены." : "Novalidate пропустил встроенную проверку при отправке. Ограничения полей по-прежнему нарушены; данные никуда не отправлены.";
    		changed();
    	});
    	EventTarget.prototype.addEventListener.call(form, "reset", () => {
    		setTimeout(() => {
    			touched.clear();
    			submitted = false;
    			requested = false;
    			setErrors(customErrors);
    			status.textContent = "Ввод сброшен к начальным значениям.";
    			render();
    		});
    	});
    	setErrors(customErrors);
    	return {
    		controls,
    		message,
    		refresh: () => {
    			setErrors(customErrors);
    			render();
    		},
    		setErrors,
    		check: () => {
    			submitted = true;
    			const valid = render(true);
    			status.textContent = valid ? "Все проверяемые поля корректны." : "Есть ошибки. Первое ошибочное поле получило фокус.";
    			return valid;
    		},
    		clear: () => {
    			submitted = false;
    			touched.clear();
    			status.textContent = "";
    			render();
    		}
    	};
    }
    
    (bindLearningForm)(document, {});

    Начните с настройки отдельных полей или перейдите к заданиям «Исправь форму» : воспроизведите ошибку и найдите причину.

    1 из 18

    Обязательная почта

    Настройте обязательное поле электронной почты. Пустая строка и hello должны отклоняться, reader@example.com — приниматься. Поле должно оставаться доступным для ввода.

    Объяснение в учебнике и справочнике

    Подсказка

    Выберите email и включите required. Сам email не делает поле обязательным.

    Показать разбор

    Type=email проверяет формат непустого значения; required запрещает пустое. Проверка формата не подтверждает существование почтового ящика.

    Просмотр разбора не засчитывает решение. Примените настройки и проверьте результат.

    Вводите учебные данные в превью. Они не отправляются и не сохраняются. Экспорт содержит настройки и начальные значения, а не текущий ввод.

    Результат проверки поля

    Все флаги ValidityState

      Проверяется фактический ввод браузера. Minlength срабатывает при пользовательском вводе; программное начальное значение не всегда даёт tooShort. Maxlength обычно не позволяет ввести лишние символы.

      Поля формы

      До восьми полей. «Выше» и «Ниже» меняют также порядок обхода клавишей Tab.

      Настройки поля
      Ограничения и состояние
      Справочник: min
      Справочник: max
      Справочник: maxlength

      Пустое ограничение удаляет атрибут. Пользовательская ошибка блокирует поле, пока вы не очистите сообщение setCustomValidity. Неактуальные для выбранного типа атрибуты не попадают в HTML.

      Accept — подсказка диалогу выбора, а не проверка содержимого. Расширение и MIME-тип могут быть неверными или отсутствовать. Required проверяет только наличие файла. На сервере проверяют содержимое, размер и допустимый формат.

      Оформление и проверка при отправке

      Novalidate влияет на пробную отправку, но не отключает ValidityState и явную проверку кнопкой «Проверить форму».

        Какие данные попадут в FormData
        Текущие пары name — value
        NameValue

        Почему поле входит в данные? Выберите поле в таблице, чтобы перейти к нему в превью. Проверка значения и включение в FormData — разные вещи: ошибочное поле тоже может присутствовать в данных.

        Поле → данные формы
        ПолеРезультатПричина

        Повторяющиеся name создают отдельные пары. Кнопки отправки не включаются в показанный FormData(form): для их данных нужен конкретный submitter.

        Отключённые поля, поля без name и неотмеченные checkbox/radio не включаются. Readonly включается. Повторяющиеся имена сохраняются отдельными строками. Пароли в этой таблице скрыты.

        HTML, CSS и JavaScript формы
        HTML
        <form id="learning-form">
        <div id="form-errors" role="alert" tabindex="-1" hidden></div>
        <div class="form-field" data-field="field-1">
        <label for="field-1">Почта</label>
        <input id="field-1" type="text" name="email">
        <p id="field-1-error" class="field-error" hidden></p>
        </div>
        <button type="submit">Пробная отправка</button>
        <button type="reset">Очистить ввод</button>
        <p id="form-result" role="status"></p>
        </form>
        CSS
        :root { color-scheme: light dark; }
        body { margin: 16px; font: 16px/1.5 system-ui, sans-serif; }
        form { max-width: 680px; }
        .form-field { margin-block: 12px; min-width: 0; }
        label, small { display: block; }
        input:not([type="checkbox"]):not([type="radio"]), select, textarea { max-width: 100%; box-sizing: border-box; }
        input[type="checkbox"] + label, input[type="radio"] + label { display: inline; }
        fieldset { min-width: 0; }
        .field-error { color: light-dark(#a12622, #ffb0aa); }
        #form-errors { border: 2px solid light-dark(#a12622, #ffb0aa); padding: 12px; margin-bottom: 16px; }
        [hidden] { display: none !important; }
        :focus-visible { outline: 3px solid light-dark(#1759a2, #8ecaff); outline-offset: 3px; }
        JavaScript
        // Учебная отправка перехватывается: сервер не используется.
        function bindLearningForm(doc, customErrors = {}, changed = () => void 0) {
        	const form = doc.querySelector("#learning-form");
        	const summary = doc.querySelector("#form-errors");
        	const status = doc.querySelector("#form-result");
        	const touched = /* @__PURE__ */ new Set();
        	let submitted = false;
        	let requested = false;
        	const controls = () => [...doc.querySelectorAll("input, select, textarea")];
        	function message(control) {
        		const v = control.validity;
        		if (!control.willValidate || v.valid) return "";
        		if (v.customError) return control.validationMessage;
        		if (v.valueMissing) return "Заполните поле или выберите вариант.";
        		if (v.typeMismatch) return control.type === "email" ? "Введите адрес электронной почты, например reader@example.com." : "Введите полный URL, например https://example.com.";
        		if (v.patternMismatch) return "Значение не соответствует шаблону pattern.";
        		if (v.tooShort) return "Введите не менее " + control.getAttribute("minlength") + " символов.";
        		if (v.tooLong) return "Введите не более " + control.getAttribute("maxlength") + " символов.";
        		if (v.rangeUnderflow) return "Значение должно быть не меньше " + control.getAttribute("min") + ".";
        		if (v.rangeOverflow) return "Значение должно быть не больше " + control.getAttribute("max") + ".";
        		if (v.stepMismatch) return "Значение не попадает в шаг step от базы (min, начальное value или 0).";
        		if (v.badInput) return "Браузер не может преобразовать ввод в значение этого типа.";
        		return control.validationMessage;
        	}
        	function render(focus = false) {
        		for (const input of doc.querySelectorAll("input[type=\"file\"]")) {
        			const list = input.closest("[data-field]")?.querySelector("[data-file-list]");
        			if (!list) continue;
        			const files = [...input.files ?? []];
        			const signature = files.map((file) => [
        				file.name,
        				file.size,
        				file.type,
        				file.lastModified
        			].join(":")).join("|");
        			if (list.dataset.files === signature) continue;
        			list.dataset.files = signature;
        			list.replaceChildren();
        			if (!files.length) {
        				list.textContent = "Файлы не выбраны.";
        				continue;
        			}
        			const heading = doc.createElement("p");
        			heading.textContent = "Выбрано файлов: " + files.length;
        			const items = doc.createElement("ul");
        			for (const file of files) {
        				const item = doc.createElement("li");
        				item.textContent = file.name + " · " + file.size + " байт · " + (file.type || "MIME-тип не указан");
        				items.append(item);
        			}
        			list.append(heading, items);
        		}
        		const invalid = controls().filter((control) => control.willValidate && !control.validity.valid);
        		for (const wrapper of doc.querySelectorAll("[data-field]")) {
        			const local = [...wrapper.querySelectorAll("input,select,textarea")];
        			const failed = local.find((control) => control.willValidate && !control.validity.valid);
        			const show = !!failed && (submitted || local.some((control) => touched.has(control.id)));
        			const error = wrapper.querySelector(".field-error");
        			error.textContent = show ? message(failed) : "";
        			error.hidden = !show;
        			for (const control of local) {
        				if (show) control.setAttribute("aria-invalid", "true");
        				else control.removeAttribute("aria-invalid");
        				const ids = [wrapper.querySelector("small")?.id, show ? error.id : ""].filter(Boolean).join(" ");
        				if (ids) control.setAttribute("aria-describedby", ids);
        				else control.removeAttribute("aria-describedby");
        			}
        		}
        		summary.replaceChildren();
        		summary.hidden = !submitted || invalid.length === 0;
        		if (!summary.hidden) {
        			const heading = doc.createElement("strong");
        			heading.textContent = "Исправьте ошибки:";
        			summary.append(heading);
        			const list = doc.createElement("ul");
        			const seen = /* @__PURE__ */ new Set();
        			for (const control of invalid) {
        				const wrapper = control.closest("[data-field]");
        				if (seen.has(wrapper.dataset.field)) continue;
        				seen.add(wrapper.dataset.field);
        				const item = doc.createElement("li"), link = doc.createElement("a");
        				link.href = "#" + control.id;
        				link.textContent = (wrapper.querySelector("legend,label")?.textContent ?? control.name) + ": " + message(control);
        				link.addEventListener("click", (event) => {
        					event.preventDefault();
        					control.focus();
        				});
        				item.append(link);
        				list.append(item);
        			}
        			summary.append(list);
        		}
        		if (focus && invalid.length) invalid[0].focus();
        		changed();
        		return invalid.length === 0;
        	}
        	function setErrors(errors) {
        		customErrors = errors;
        		for (const control of controls()) {
        			const id = control.closest("[data-field]").dataset.field;
        			control.setCustomValidity(customErrors[id] ?? "");
        		}
        	}
        	EventTarget.prototype.addEventListener.call(form, "input", (event) => {
        		const control = event.target;
        		if (![
        			"INPUT",
        			"SELECT",
        			"TEXTAREA"
        		].includes(control.tagName)) return;
        		touched.add(control.id);
        		status.textContent = "";
        		render();
        	});
        	EventTarget.prototype.addEventListener.call(form, "change", () => render());
        	EventTarget.prototype.addEventListener.call(form, "click", (event) => {
        		if (event.target.closest("button[type=\"submit\"]")) requested = true;
        	});
        	EventTarget.prototype.addEventListener.call(form, "keydown", (event) => {
        		if (event.key === "Enter" && event.target.tagName === "INPUT") requested = true;
        	});
        	EventTarget.prototype.addEventListener.call(form, "invalid", (event) => {
        		event.preventDefault();
        		if (requested) {
        			submitted = true;
        			queueMicrotask(() => {
        				render(true);
        				requested = false;
        				status.textContent = "Отправка остановлена: исправьте отмеченные поля.";
        			});
        		}
        	}, true);
        	EventTarget.prototype.addEventListener.call(form, "submit", (event) => {
        		event.preventDefault();
        		requested = false;
        		submitted = true;
        		const valid = render();
        		status.textContent = valid ? "Проверка пройдена. Учебная отправка перехвачена: данные никуда не отправлены." : "Novalidate пропустил встроенную проверку при отправке. Ограничения полей по-прежнему нарушены; данные никуда не отправлены.";
        		changed();
        	});
        	EventTarget.prototype.addEventListener.call(form, "reset", () => {
        		setTimeout(() => {
        			touched.clear();
        			submitted = false;
        			requested = false;
        			setErrors(customErrors);
        			status.textContent = "Ввод сброшен к начальным значениям.";
        			render();
        		});
        	});
        	setErrors(customErrors);
        	return {
        		controls,
        		message,
        		refresh: () => {
        			setErrors(customErrors);
        			render();
        		},
        		setErrors,
        		check: () => {
        			submitted = true;
        			const valid = render(true);
        			status.textContent = valid ? "Все проверяемые поля корректны." : "Есть ошибки. Первое ошибочное поле получило фокус.";
        			return valid;
        		},
        		clear: () => {
        			submitted = false;
        			touched.clear();
        			status.textContent = "";
        			render();
        		}
        	};
        }
        
        (bindLearningForm)(document, {});

        Исправьте HTML или создайте форму по требованиям, обновите превью и проверьте решение. Здесь можно писать разметку формы; CSS, JavaScript и внешние ресурсы не используются. Код и введённые данные остаются только на этой странице, сохраняются лишь отметки решений.

        1 из 10

        Свяжите подпись с полем

        Исправьте подпись «Имя»: нажатие на неё должно переводить фокус в доступное текстовое поле с name="name". Можно использовать for/id или вложить input в label.

        Объяснение в учебнике и справочнике
        Подсказка

        For должен совпадать с id поля. Другой вариант — обернуть поле элементом label.

        Показать разбор

        Оба способа связывают подпись с полем. Name определяет ключ данных, а id связывает элементы документа.

        Редактор загружается…

        HTML формы

        <form>
          <label for="user">Имя</label>
          <input id="name" name="name">
          <button>Отправить</button>
        </form>

        Результат

        Данные FormData

        Почему поле входит в данные? Выберите поле в таблице, чтобы перейти к нему в превью. Проверка значения и включение в FormData — разные вещи: ошибочное поле тоже может присутствовать в данных.

        Поле → данные формы
        ПолеРезультатПричина

        Повторяющиеся name создают отдельные пары. Кнопки отправки не включаются в показанный FormData(form): для их данных нужен конкретный submitter.

        Пары имя — значение текущего превью. Пароли скрыты. Данные никуда не отправляются.

        Браузер и сервер: пробная отправка

        Даже корректную форму сервер может отклонить. Выберите учебный ответ и отправьте текущее превью. Ответ появится с задержкой; это локальная имитация без сети. Изменение формы отменяет устаревший ответ.

        Оформление range и select: браузерные псевдоэлементы

        Сравните отдельные примеры. Поля остаются нативными, связаны с подписями и имеют видимый фокус. Системные цвета поддерживают светлую, тёмную тему и режим высокой контрастности.

        WebKit/Blink использует ::-webkit-slider-runnable-track и ::-webkit-slider-thumb; Gecko — ::-moz-range-track, ::-moz-range-thumb и ::-moz-range-progress. Это нестандартные расширения. Правила разделены, чтобы неподдерживаемый селектор не отменил остальные. Tab переводит фокус, стрелки меняют значение; границы min/max и шаг step остаются нативными.

        Range: дорожка и бегунок

        input[type="range"] {
          width: 100%; min-height: 44px; margin: 0;
          accent-color: #287047;
        }
        @supports selector(input::-webkit-slider-thumb) {
          input[type="range"] { appearance: none; background: transparent; }
          input[type="range"]::-webkit-slider-runnable-track {
            height: 8px; border-radius: 4px; background: CanvasText;
          }
          input[type="range"]::-webkit-slider-thumb {
            appearance: none; margin-top: -8px;
            width: 24px; height: 24px; border: 2px solid CanvasText;
            border-radius: 50%; background: Canvas;
          }
        }
        @supports selector(input::-moz-range-thumb) {
          input[type="range"] { appearance: none; background: transparent; }
          input[type="range"]::-moz-range-track {
            height: 8px; border-radius: 4px; background: CanvasText;
          }
          input[type="range"]::-moz-range-progress {
            height: 8px; background: Highlight;
          }
          input[type="range"]::-moz-range-thumb {
            width: 24px; height: 24px; box-sizing: border-box;
            border: 2px solid CanvasText; border-radius: 50%; background: Canvas;
          }
        }
        input:focus-visible { outline: 3px solid Highlight; outline-offset: 4px; }
        input:disabled { opacity: .6; cursor: not-allowed; }

        ::picker(select) оформляет раскрывающийся список, ::picker-icon — стрелку, option::checkmark — отметку выбранного пункта. Appearance: base-select включается только при поддержке. Иначе работает обычный select. Подпись, фокус и управление с клавиатуры сохраняются; конкретные клавиши открытия зависят от браузера и ОС. В режиме base-select мобильный системный диалог заменяется списком внутри страницы.

        Select: список, стрелка и отметка выбора

        select {
          font: inherit; max-width: 100%; min-height: 44px;
          padding: 8px; border: 1px solid CanvasText;
          border-radius: 8px; background: Canvas; color: CanvasText;
        }
        select:focus-visible { outline: 3px solid Highlight; outline-offset: 4px; }
        select:disabled { opacity: .6; cursor: not-allowed; }
        /* Без поддержки остаётся обычный нативный select со стрелкой. */
        @supports (appearance: base-select) and selector(select::picker(select)) {
          select { appearance: base-select; }
          select::picker(select) {
            appearance: base-select;
            border: 1px solid CanvasText; border-radius: 8px;
            background: Canvas; color: CanvasText;
          }
          select::picker-icon { color: currentColor; }
          select:open::picker-icon { rotate: 180deg; }
          option { padding: 8px 12px; }
          option::checkmark { color: currentColor; }
          option:checked { font-weight: bold; }
          option:focus-visible { outline: 2px solid Highlight; outline-offset: -2px; }
        }

        Подробнее: оформляемый select и файловые поля в HTML Standard .

        Для интерактивной части включите JavaScript. Required запрещает пустое значение, type задаёт формат, min и max — диапазон. Label с for, совпадающим с id поля, помогает понять назначение поля и перейти к нему нажатием. Учебная форма не отправляет данные.

        Учебник и справочник

        Начните с уроков «Формы» и «Элементы формы HTML5». К каждому заданию приложена ссылка на соответствующее объяснение.

        Нормативное описание: Constraint Validation API в стандарте HTML.

        Связанные элементы и атрибуты: label, input, form, select, textarea, fieldset, type, name, required, pattern, min, max, step, maxlength, readonly, disabled, autocomplete.