Тренажёр 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 обычно не позволяет ввести лишние символы.
Оформление и проверка при отправке
Novalidate влияет на пробную отправку, но не отключает ValidityState и явную проверку кнопкой «Проверить форму».
Какие данные попадут в FormData
| Name | Value |
|---|
Почему поле входит в данные? Выберите поле в таблице, чтобы перейти к нему в превью. Проверка значения и включение в FormData — разные вещи: ошибочное поле тоже может присутствовать в данных.
| Поле | Результат | Причина |
|---|
Повторяющиеся name создают отдельные пары. Кнопки отправки не включаются в показанный FormData(form): для их данных нужен конкретный submitter.
Отключённые поля, поля без name и неотмеченные checkbox/radio не включаются. Readonly включается. Повторяющиеся имена сохраняются отдельными строками. Пароли в этой таблице скрыты.
HTML, CSS и JavaScript формы
<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>: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; }// Учебная отправка перехватывается: сервер не используется.
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, {});Начните с настройки отдельных полей или перейдите к заданиям «Исправь форму» : воспроизведите ошибку и найдите причину.
Обязательная почта
Настройте обязательное поле электронной почты. Пустая строка и hello должны отклоняться, reader@example.com — приниматься. Поле должно оставаться доступным для ввода.
Объяснение в учебнике и справочнике
Как воспроизвести ошибку
Подсказка
Выберите email и включите required. Сам email не делает поле обязательным.
Показать разбор
Type=email проверяет формат непустого значения; required запрещает пустое. Проверка формата не подтверждает существование почтового ящика.
Просмотр разбора не засчитывает решение. Примените настройки и проверьте результат.
Вводите учебные данные в превью. Они не отправляются и не сохраняются. Экспорт содержит настройки и начальные значения, а не текущий ввод.
Результат проверки поля
Все флаги ValidityState
Проверяется фактический ввод браузера. Minlength срабатывает при пользовательском вводе; программное начальное значение не всегда даёт tooShort. Maxlength обычно не позволяет ввести лишние символы.
Оформление и проверка при отправке
Novalidate влияет на пробную отправку, но не отключает ValidityState и явную проверку кнопкой «Проверить форму».
Какие данные попадут в FormData
| Name | Value |
|---|
Почему поле входит в данные? Выберите поле в таблице, чтобы перейти к нему в превью. Проверка значения и включение в FormData — разные вещи: ошибочное поле тоже может присутствовать в данных.
| Поле | Результат | Причина |
|---|
Повторяющиеся name создают отдельные пары. Кнопки отправки не включаются в показанный FormData(form): для их данных нужен конкретный submitter.
Отключённые поля, поля без name и неотмеченные checkbox/radio не включаются. Readonly включается. Повторяющиеся имена сохраняются отдельными строками. Пароли в этой таблице скрыты.
HTML, CSS и JavaScript формы
<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>: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; }// Учебная отправка перехватывается: сервер не используется.
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 и внешние ресурсы не используются. Код и введённые данные остаются только на этой странице, сохраняются лишь отметки решений.
Свяжите подпись с полем
Исправьте подпись «Имя»: нажатие на неё должно переводить фокус в доступное текстовое поле с 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.