TGViewer
Логово верстальщика Логово верстальщика @webdevlair · 7.97K subscribers
Post #4595 332
⁣CSS :user-invalid и :user-valid: отложенная валидация без JS и без мигания ошибок

Классическая боль любой формы: input:invalid срабатывает сразу при рендере. Пользователь ещё ничего не вводил, а поле уже красное. Дальше - три костыля на выбор: :not(:placeholder-shown), класс .touched из JS или novalidate + ручная валидация.

Селекторы уровня 4 закрывают это нативно.

Как это работает

:user-invalid матчится, только если элемент одновременно:

1. кандидат на constraint validation и сейчас невалиден,
2. был «изменён пользователем» (user interacted).

Второй пункт - ключевой. По HTML-спеке флаг выставляется, когда пользователь изменил значение или попытался отправить форму. То есть до первого реального действия с полем оно остаётся нейтральным, а после сабмита все проблемные поля подсвечиваются разом - именно тот UX, к которому мы руками приходили через JS.

:user-valid - зеркало: валидно и было взаимодействие. Полезно, чтобы не красить зелёным пустые нетронутые поля.

Код

/* Плохо: красное с первого кадра рендера */
input:invalid { border-color: #e5484d; }

/* Ок: ошибка - только после взаимодействия */
input:user-invalid { border-color: #e5484d; }
input:user-valid { border-color: #30a46c; }

/* сообщение без JS: заранее в разметке, показ через CSS */
.field .msg { visibility: hidden; }
.field:has(:user-invalid) .msg { visibility: visible; }
.field:has(:user-invalid) .msg::before { content: attr(data-msg); }


<div class="field">
<input type="email" required>
<span class="msg" data-msg="Похоже, в адресе опечатка"></span>
</div>


Работает с input, select, textarea, любым типом и любым ограничением: required, pattern, minlength, type="email". Реагирует и на «ввёл - стёр», и на попытку сабмита.

Подводные камни

* Поддержка - Baseline с конца 2023 (Chrome 119+, Firefox 88+, Safari 16.5+). Для более старых - тот же селектор в :where()-фолбэке или @supports selector(:user-invalid).
* Blur сам по себе флаг не ставит. Если нужно «валидировать по потере фокуса, даже если значение не менялось» - это по-прежнему JS. Но в 95% форм такое поведение и не нужно.
* required никуда не делся. :user-invalid меняет момент показа, а не правила валидации.
* A11y не бесплатно. Псевдоклассы не выставляют aria-invalid - скринридер про ошибку не узнает. Либо дублируйте состояние атрибутом, либо полагайтесь на нативную браузерную валидацию при сабмите.
* Не стилизуйте :invalid вообще в формах с ранним показом - иначе правила конфликтуют и «мигание» возвращается.

Итого

:user-invalid убирает целый слой JS-состояния: флаг «тронуто/не тронуто» больше не нужно тащить в классы и стейт-менеджер. Разметка описывает намерение, CSS - момент показа. Классический кейс, где платформа догнала практику.
  • ❤ 1
  • 👍 1
More from @webdevlair
  1. Sep 25, 2026Post #4629
  2. Sep 24, 2026Claude Opus 5.5 не где-то там — она уже здесь, и в большинстве бенчмарков она укладывает н…
  3. Sep 24, 2026Post #4626
  4. Sep 24, 202612 CSS-фич, которые позволят отказаться от зависимостей В Frontend Focus #759 от 23 сентяб…
  5. Sep 24, 2026OpenAI тоже решила не отставать и притащила новый релиз: знакомьтесь — GPT-6 Sol и Luna. S…
  6. Sep 23, 2026Post #4620
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →