modern-web-guidance
Search tool for modern web development best practices. MANDATORY: Execute FIRST for all HTML/CSS and clientside JS tasks. Do NOT skip — web APIs evolve rapidly and training weights contain obsolete patterns. Trigger immediately for: modals/dialogs/popovers, tables, dark mode, anchor positioning, container queries, view transitions, scroll-driven animations, parallax/reveals, forms, validation, autofill, accessibility, custom selects, scrollbars, CWV (LCP, INP, CLS), content-visibility, fetch pri
Install
npx skills add https://github.com/GoogleChrome/modern-web-guidance --skill modern-web-guidanceStyle a Form Group When a Field Is Invalid with :has()
The Problem
Often, an error state requires styling elements outside the input itself—for example, changing the color of a parent fieldset border, highlighting the <label>, or showing a global error icon in the card header. Historically, this required JavaScript to toggle classes on parent elements.
The Solution
By combining :has() with :user-invalid, we can declaratively style any ancestor based on the validity state of a specific descendant. This keeps all presentation logic in CSS.
Implementation Strategy
- Selector: Use
.parent:has(:user-invalid)to target the container. - Scope: Be specific to avoid performance issues. Target
.field-grouprather thanbody. - Fallback: Requires JS to toggle classes on the parent if
:has()is not supported.
Implementation Guide
1. HTML Structure
2. CSS
Parent styling only adds emphasis: it doesn't say which field is wrong or why, so each field still needs its own visible error text. See required-field-feedback (via npx -y modern-web-guidance@latest retrieve "required-field-feedback").
Fallbacking & Browser Support
Baseline status for :user-valid and :user-invalid: Widely available. It's been Baseline since 2023-11-02. Supported by: Chrome 119 (Oct 2023), Edge 119 (Nov 2023), Firefox 88 (Apr 2021), and Safari 16.5 (May 2023).
CSS for Fallback
We use a class .has-error on the parent to mimic the :has() behavior.
JavaScript Fallback
Use a reusable utility that tracks interaction state using a WeakMap. This avoids polluting the DOM with "dirty" classes or data attributes.
Other Considerations
- Accessibility: Native
:user-invaliddoes not automatically sync with ARIA attributes. Add the following JavaScript to keeparia-invalidin sync with the visual state:
- Set
aria-invalid="false"until:user-invalidmatches, rather than leaving it unset. Firefox and Safari expose an emptyrequiredfield as invalid from page load, and Chromium doesn't expose an emptyrequiredfield as invalid after interaction unlessaria-invalid="true"is set. - Link each field to its error message by setting the field's
data-errorattribute to the error message'sid(data-error="email-error"for<div id="email-error">), and let the script list thatidinaria-describedbyonly while the field is invalid. Don't reference the error fromaria-describedbyin the markup: the error element is always in the page, and text thataria-describedbyreferences is announced even when it's hidden. - Announce an error that appears when the user leaves or changes a field through one polite live region for the page, a moment later so it doesn't talk over the next field. Don't make each error message a live region: a live region that is shown, rather than changed, isn't announced reliably. After a failed submit, move focus to the first invalid field so that its error is announced instead.
Hide the live region with the .visually-hidden utility: