Формы, которые не бесят
Восемь состояний одного поля, момент показа ошибки, стилизация чекбоксов без обмана и мобильная клавиатура, которая открывается правильной.
Что ты научишься делать
- Оформишь все состояния поля, а не только покой и фокус
- Покажешь ошибку тогда, когда она помогает, а не мешает
- Настроишь мобильную клавиатуру и автозаполнение
#Форма — место, где люди уходят
До формы человек уже дошёл: нашёл сайт, выбрал товар, решился. Форма — последний шаг, и именно на нём отваливаются.
Причины почти всегда одни и те же: непонятно, что не так; ошибка появилась раньше, чем закончил печатать; поле сбросилось; на телефоне открылась буквенная клавиатура для номера телефона.
Всё это чинится вёрсткой, без единой строчки серверного кода.
#Момент показа ошибки
Самая раздражающая вещь в формах — поле, которое краснеет, пока ты ещё печатаешь. Ты набрал первую букву почты, а сайт уже кричит, что адрес неверный.
Причина в псевдоклассе :invalid: он срабатывает сразу, ещё до того, как человек что-либо ввёл. Пустое обязательное поле недействительно с момента загрузки страницы.
Так не надо
/* Краснеет сразу при загрузке страницы */
input:invalid {
border-color: #c8394a;
}Пустое обязательное поле недействительно с самого начала. Человек открыл форму и увидел красное поле, ничего не сделав.
Так надо
/* Краснеет только после попытки ввода */
input:user-invalid {
border-color: #c8394a;
background: #fff5f6;
}<code>:user-invalid</code> срабатывает после того, как человек взаимодействовал с полем и ушёл из него. Ровно тот момент, когда подсказка уместна.
#Полезные псевдоклассы
| Псевдокласс | Когда срабатывает | Зачем |
|---|---|---|
:user-invalid | После взаимодействия и ухода из поля | Показ ошибки в правильный момент |
:user-valid | То же, но когда всё верно | Зелёная галочка после заполнения |
:placeholder-shown | Пока поле пустое | Отличить пустое от заполненного без скрипта |
:autofill | Значение подставил браузер | Убрать жёлтый фон автозаполнения |
:has(:focus) | Внутри есть элемент в фокусе | Подсветить всю группу полей |
:disabled, :read-only | Соответствующее состояние | Приглушить, но оставить читаемым |
#Сообщения об ошибках
Плохое сообщение говорит, что не так. Хорошее — что сделать.
| Плохо | Хорошо |
|---|---|
| Некорректное значение | Нужен адрес вида имя@почта.ру |
| Ошибка валидации | Телефон должен начинаться с +7 или 8 |
| Поле обязательно | Без имени мы не поймём, к кому обращаться |
| Неверный формат | Дата в формате 31.12.2026 |
Сообщение должно стоять рядом с полем, а не общим списком сверху. И быть связано с полем через aria-describedby, иначе читалка его не свяжет.
<div class="field"> <label for="mail">Почта</label> <input type="email" id="mail" name="mail" required aria-describedby="mail-error" aria-invalid="true"> <!-- role="alert" объявит текст сразу, как только он появится --> <p class="field__error" id="mail-error" role="alert"> Похоже, здесь опечатка — нужен адрес вида имя@почта.ру </p></div>#Мобильная клавиатура и автозаполнение
Два атрибута, которые экономят человеку минуты, а стоят пары слов.
inputmode выбирает раскладку клавиатуры на телефоне. autocomplete разрешает браузеру подставить сохранённые данные — адрес, имя, карту.
<input type="text" name="name" autocomplete="name" autocapitalize="words"><input type="email" name="email" inputmode="email" autocomplete="email" autocapitalize="off" spellcheck="false"><!-- type="tel" даёт телефонную клавиатуру, inputmode уточняет её вид --><input type="tel" name="phone" inputmode="tel" autocomplete="tel"><!-- Индекс: только цифры, но type="number" здесь не годится — он режет ведущие нули и показывает стрелки --><input type="text" name="zip" inputmode="numeric" pattern="[0-9]*" autocomplete="postal-code"><input type="text" name="card" inputmode="numeric" autocomplete="cc-number">#Стилизация без обмана
Долгое время чекбоксы и переключатели «стилизовали» так: настоящий элемент прятали, а рядом рисовали свой из div. Это ломает доступность: пропадает фокус, состояние не объявляется, клавиатура не работает.
Сегодня половина задач решается свойством accent-color, а остальное — стилизацией самого элемента через appearance: none.
/* Самый простой путь: перекрасить встроенный вид одной строкой */:root { accent-color: #6544e0;}/* Полный контроль — но элемент остаётся настоящим */.check { appearance: none; inline-size: 20px; block-size: 20px; border: 2px solid #ccd; border-radius: 6px; display: grid; place-content: center;}.check::before { content: ""; inline-size: 10px; block-size: 10px; clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0, 43% 62%); background: #fff; transform: scale(0); transition: transform .12s ease;}.check:checked { background: #6544e0; border-color: #6544e0;}.check:checked::before { transform: scale(1); }/* Фокус обязан остаться видимым */.check:focus-visible { outline: 3px solid #6544e0; outline-offset: 2px;}#Длинные формы
Форма из двадцати полей отпугивает одним видом. Приёмы, которые действительно помогают.
- Разбить на шаги. Три экрана по пять полей проходят охотнее, чем один на пятнадцать. Обязательно показывай, сколько шагов всего.
- Сгруппировать через
fieldset. «Контакты», «Доставка», «Оплата» — человек видит структуру и понимает, сколько осталось. - Убрать необязательное. Каждое поле должно оправдывать своё существование. Отчество нужно налоговой, а не форме подписки.
- Помечать необязательные, а не обязательные. Если из десяти полей девять обязательны, девять звёздочек — это шум. Помечай исключения.
- Не сбрасывать введённое. Ошибка на четвёртом шаге не должна стирать первые три.
#Как это выглядит в реальном проекте
@layer components { .field { display: grid; gap: 4px; } .field__input { padding: 10px 12px; border: 1px solid var(--color-border); border-radius: var(--radius-sm); } /* Подсвечиваем всю группу, когда фокус внутри */ .field:has(:focus-visible) .field__label { color: var(--color-accent); } /* Ошибка — после попытки ввода, не раньше */ .field__input:user-invalid { border-color: var(--color-danger); background: color-mix(in oklab, var(--color-danger) 8%, transparent); } /* Сообщение показываем только когда поле реально ошибочно */ .field__error { display: none; color: var(--color-danger); } .field:has(:user-invalid) .field__error { display: block; } /* Браузер подставил значение — убираем жёлтый фон, но оставляем признак автозаполнения */ .field__input:autofill { box-shadow: inset 0 0 0 100px var(--color-surface); -webkit-text-fill-color: var(--color-text); } .field__input:disabled { opacity: .6; cursor: not-allowed; }}#Когда НЕ надо
- Прятать настоящий элемент ради красоты. Пропадает фокус, клавиатура и объявление состояния. Стилизуй сам элемент через
appearance: none. - Отключать вставку из буфера. Запрет вставки в поле подтверждения почты не повышает надёжность, а мешает людям с менеджерами паролей.
- Ставить
maxlengthвслепую. Обрезание на лету путает: человек печатает, а символы не появляются. Лучше показать счётчик. - Проверять формат телефона строгой маской. Форматы разные в разных странах, а на границах масок люди застревают. Принимай как есть, нормализуй на сервере.
#Поддержка браузерами
| Возможность | Состояние |
|---|---|
:placeholder-shown | Во всех современных браузерах |
:user-invalid, :user-valid | Во всех современных браузерах; в старых сборках был префикс :-moz-ui-invalid |
accent-color | Во всех современных браузерах |
appearance: none | Везде |
:autofill | Поддерживается; в WebKit нужен префикс :-webkit-autofill |
inputmode | Во всех мобильных браузерах |
Где спотыкаются почти все
Использован <code>:invalid</code> вместо <code>:user-invalid</code>
Почему Пустое обязательное поле недействительно с загрузки страницы — форма краснеет до того, как человек к ней прикоснулся.
Как надо Проверяй через :user-invalid: он ждёт взаимодействия.
<code>type="number"</code> для телефона или индекса
Почему Обрезаются ведущие нули, появляются стрелки, прокрутка колесом меняет значение.
Как надо type="text" плюс inputmode="numeric".
Настоящий чекбокс спрятан, рядом нарисован свой
Почему Теряется фокус и объявление состояния. С клавиатуры такой чекбокс не переключить.
Как надо Стилизуй сам элемент: appearance: none плюс :checked и :focus-visible.
Сообщение об ошибке не связано с полем
Почему Читалка озвучит поле без объяснения, что с ним не так.
Как надо Свяжи через aria-describedby и добавь role="alert".
- Состояние поля field state
- Покой, наведение, фокус, заполнено, ошибка, недоступно, только чтение, проверка.
- Отложенная проверка :user-invalid
- Псевдокласс, срабатывающий только после взаимодействия человека с полем.
- Режим ввода inputmode
- Атрибут, задающий вид клавиатуры на сенсорных устройствах.
- Автозаполнение autocomplete
- Подсказка браузеру, какие сохранённые данные подставить.
- Акцентный цвет accent-color
- Свойство, перекрашивающее встроенный вид чекбоксов, переключателей и ползунков.
Проверь себя
Псевдокласс не ждёт взаимодействия. Для правильного момента есть <code>:user-invalid</code>.
Пробел невидим, но включает псевдокласс — и подпись уезжает без единой строчки скрипта.
<code>number</code> обрезает ведущие нули и добавляет стрелки. Нужны цифры на клавиатуре, а не арифметика.
Стилизовать нужно сам элемент через <code>appearance: none</code> — тогда он остаётся настоящим.
Так подсказка ощущается помощью: она не мешает печатать и исчезает, как только человек начал исправлять.
Задание
Форма со всеми состояниями
Форма оформлена наполовину: ошибка появляется сразу при загрузке, фокус не виден, чекбокс не покрашен, а у полей нет подсказок для мобильной клавиатуры и автозаполнения.
Почини: замени :invalid на :user-invalid, верни видимый фокус через :focus-visible, задай accent-color, добавь полям autocomplete и inputmode, а сообщение об ошибке свяжи с полем через aria-describedby.
Подсказка 1
Замени .field__input:invalid на .field__input:user-invalid — остальное правило оставь как есть.
Подсказка 2
Фокус: замени :focus { outline: none; } на :focus-visible { outline: 3px solid #6544e0; outline-offset: 1px; }
Подсказка 3
Чекбокс: добавь в начало CSS правило :root { accent-color: #6544e0; }
Подсказка 4
Полям: <input ... autocomplete="email"> и <input ... inputmode="tel" autocomplete="tel">, а почте ещё aria-describedby="mail-error".
Показать решение
<form class="form"> <div class="field"> <label for="mail">Почта</label> <input class="field__input" type="email" id="mail" name="mail" required inputmode="email" autocomplete="email" autocapitalize="off" spellcheck="false" aria-describedby="mail-error"> <p class="field__error" id="mail-error">Нужен адрес вида имя@почта.ру</p> </div> <div class="field"> <label for="phone">Телефон</label> <input class="field__input" type="tel" id="phone" name="phone" inputmode="tel" autocomplete="tel"> </div> <label class="agree"> <input type="checkbox" checked> <span>Согласен на обработку данных</span> </label> <button type="submit">Отправить</button></form>:root { accent-color: #6544e0; }.form { display: grid; gap: 14px; max-width: 320px; }.field { display: grid; gap: 4px; }label { font-size: 14px; font-weight: 600; }.field__input { padding: 9px 12px; font: inherit; border: 1px solid #ccd; border-radius: 8px;}/* Только после того, как человек поработал с полем */.field__input:user-invalid { border-color: #c8394a; background: #fff5f6;}.field__error { margin: 0; font-size: 13px; color: #c8394a; }.agree { display: flex; align-items: center; gap: 8px; font-weight: 400; }button { padding: 11px; font: inherit; font-weight: 600; color: #fff; background: #6544e0; border: 0; border-radius: 999px;}:focus-visible { outline: 3px solid #6544e0; outline-offset: 1px;}