Тренажёр HTML-форм и валидации
Соберите форму, исследуйте проверку введённых данных и решайте задания настройками или HTML-кодом.
Для вашего удобства рекомендую использовать полноэкранный режим.
Формы и валидация
Как пользоваться
В режиме «Исследовать» начните с одного поля, регистрации, заказа или обратной связи. Добавляйте поля и меняйте их порядок, выбирайте настраиваемое поле, задавайте атрибуты и вводите пробные значения в превью. Настройки типов и ограничений применяются к HTML, а проверку выполняет браузер.
Кнопка «Проверить введённые данные» показывает ошибки и переводит фокус на первое ошибочное поле. «Пробная отправка» внутри превью демонстрирует встроенную проверку перед событием submit; отправка перехватывается локально. Сводка ошибок содержит ссылки на поля. Пройдите форму клавишами Tab и Shift+Tab, попробуйте нажимать подписи.
Откройте инспектор, чтобы увидеть флаги ValidityState, сообщение браузера и связь label с полем. Таблица FormData показывает текущие пары name — value. Разбор «Поле → данные формы» объясняет, почему каждое поле включено или исключено; нажмите его подпись, чтобы перейти к полю. «Очистить ввод» возвращает начальные значения; «Сбросить настройки» возвращает исходную форму. Ввод не сохраняется между посещениями.
Можно добавить оформление полей, изменить акцент, скругление, отступы и число колонок. HTML, CSS и JavaScript доступны с подсветкой и копированием; скачивание сохраняет работающий учебный пример целиком.
Что важно понимать о проверке форм
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, {});Как пользоваться
В режиме «Практиковаться» исправьте форму по условию и нажмите «Проверить решение». Подсказки и разбор не засчитывают решение. Галочки и прогресс сохраняются в браузере; сброс удаляет только отметки этого тренажёра. Ссылка на выбранное задание содержит его ID, без введённых значений.
В заданиях «Исправь форму» сначала воспроизведите описанную неисправность в превью. Затем измените настройки, повторите действие и проверьте решение. «Сбросить настройки» позволяет снова увидеть исходную ошибку, сохраняя отметки решённых заданий.
Начните с настройки отдельных полей или перейдите к заданиям «Исправь форму» : воспроизведите ошибку и найдите причину.
Обязательная почта
Настройте обязательное поле электронной почты. Пустая строка и 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-форм» исправляйте разметку в редакторе с подсветкой. Нажмите «Обновить и проверить», чтобы применить HTML и проверить решение одним действием. Отдельное «Обновить превью» позволяет сначала попробовать заполнить форму. При ошибке разметки проверка не запускается. До обновления проверка недоступна. «Вернуть исходный HTML» начинает текущую задачу заново, сохраняя прогресс. В итоговых заданиях регистрации, оформления заказа и обратной связи начните с пустой формы, выполните список требований и раскрывайте подсказки постепенно; просмотр подсказок не засчитывает решение. Неподдерживаемая разметка отмечается в редакторе; последний результат остаётся в превью.
В HTML-практике можно скачать самостоятельный HTML-файл с оформлением и перехватом отправки. Введённые в превью значения в файл не попадают. Блок «Браузер и сервер» имитирует задержку, отказ и успешный ответ: попробуйте отмену и повторную попытку. Реальных запросов нет. Подробная проверка раскрывается при ошибках и сворачивается после успешного решения.
Исправьте 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.