From b259ce033339688ea284fde5d7093e0cfb49e81e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 07:28:31 +0000 Subject: [PATCH] refactor(forms)!: stop auto-colouring fields on native :user-invalid/:user-valid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit optional/forms.css flipped --sf-field-border-color to --sf-color-danger / --sf-color-success automatically whenever the browser's own constraint validation (:user-invalid / :user-valid) fired. A required or pattern-constrained field could render "broken" (red border) as soon as it was touched, before the user submitted anything or the app ran its own validation — the framework silently overriding an element's appearance based on browser-internal state the consumer never opted into. This is the same category of surprise as the .sf-card/.sf-btn auto-shrink rule removed previously, so it is removed too. - Delete the input/select/textarea :user-invalid / :user-valid rules (optional/forms.css). The token itself, its resting/focus/autofill consumers, and the explicit .sf-is-invalid / .sf-is-valid / .sf-is-warning / .sf-is-info / .sf-is-danger state classes (core/states.css) are unchanged — they remain the one way to colour a field's validation state. - No token-registry/docs/demos regeneration needed: --sf-field-border-color was never declared as a :root token (pure var()-fallback hook), so its removal from the automatic trigger doesn't change the public token surface. - Update stale test comments referencing the removed mechanism (tests/forms.spec.js). No new Playwright assertion was added for the removal itself: :user-invalid does not reliably match via synthetic focus/blur in headless automation (verified — it didn't even match before this change), so a test asserting its absence would be flaky by construction. The explicit path already has solid dedicated coverage in tests/states-full.spec.js. - Document the break in docs/migration.md and CHANGELOG.md. Verified: full node --test unit suite (109/109) and all 10 CSS gates pass with no registry drift, confirming the token surface is untouched. BREAKING CHANGE: form fields no longer auto-colour their border on native :user-invalid / :user-valid. Add .sf-is-invalid / .sf-is-valid explicitly when your code determines a field's validation state. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01GnwqYHgHNAWPbsUJWUeY1w --- CHANGELOG.md | 1 + docs/migration.md | 26 ++++++++++++++++++++++++++ optional/forms.css | 13 ------------- tests/forms.spec.js | 6 +++--- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ca7cf7df..a2223e9a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Breaking Changes - **components:** `.sf-btn` nested in `.sf-card` no longer auto-shrinks its label to `--sf-text-s`. Buttons in cards now render at their own size like anywhere else; add `.sf-btn--s` for a compact card action. The orphaned `--sf-card-btn-font-size` token is removed. See `docs/migration.md`. +- **forms:** form fields no longer auto-colour on native `:user-invalid` / `:user-valid` — a field no longer looks "broken" the instant the browser considers it invalid, before your app or the user submits anything. The explicit `.sf-is-invalid` / `.sf-is-valid` (and `.sf-is-warning` / `.sf-is-info` / `.sf-is-danger`) state classes are unaffected and remain the way to colour a field's validation state. See `docs/migration.md`. ## [0.7.15] - 2026-07-11 diff --git a/docs/migration.md b/docs/migration.md index 22327a43d..5b179baef 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -67,6 +67,32 @@ the size tier on that card: .sf-card--compact .sf-btn { --sf-btn-font-size--size: var(--sf-text-s); } ``` +### Form fields no longer auto-colour on native `:user-invalid` / `:user-valid` (breaking) + +`optional/forms.css` used to flip `--sf-field-border-color` to `--sf-color-danger` +/ `--sf-color-success` automatically whenever the browser's own constraint +validation (`:user-invalid` / `:user-valid`) fired — so a `required` or +`pattern`-constrained field could render "broken" (red border) as soon as it was +touched, before the user submitted anything or your app ran its own validation. +This was the same category of surprise as the card/button rule above — the +framework silently overriding an element's appearance based on browser-internal +state you didn't opt into — so it has been removed. + +**What changed for you:** native constraint validation no longer changes a +field's border colour by itself. The explicit state classes in `core/states.css` +— `.sf-is-invalid` / `.sf-is-error`, `.sf-is-valid` / `.sf-is-success`, +`.sf-is-warning`, `.sf-is-info`, `.sf-is-danger` — are unaffected and remain the +one way to colour a field's validation state; add the class when *your* code +(not the browser) decides the field is invalid: + +```html + +``` + +The underlying `--sf-field-border-color` token and its consumers (resting +border, focus, autofill) are unchanged — only the automatic pseudo-class +trigger is gone. + ### `--sf-touch-target` decoupled from `--sf-size-l`; size scale regularised (breaking) `--sf-touch-target` used to be `var(--sf-size-l)`, which coupled the WCAG 2.5.5 diff --git a/optional/forms.css b/optional/forms.css index 988a1dbce..91d972c02 100644 --- a/optional/forms.css +++ b/optional/forms.css @@ -86,19 +86,6 @@ } /* stylelint-enable selector-pseudo-class-no-unknown */ - /* Native validation states */ - input:user-invalid, - select:user-invalid, - textarea:user-invalid { - --sf-field-border-color: var(--sf-color-danger); - } - - input:user-valid, - select:user-valid, - textarea:user-valid { - --sf-field-border-color: var(--sf-color-success); - } - /* Numeric inputs */ input[type="number"] { font-variant-numeric: var(--sf-font-numeric, tabular-nums); diff --git a/tests/forms.spec.js b/tests/forms.spec.js index a31ca6c08..dfe1cde9f 100644 --- a/tests/forms.spec.js +++ b/tests/forms.spec.js @@ -3,8 +3,8 @@ // previously with no dedicated spec (only touched incidentally by a11y.spec). // Asserts the computed styling contract for text inputs, textarea, select, the // disabled state, and the --sf-field-border-color token indirection that the -// :user-invalid / :user-valid states pivot on. Loads the optimal bundle (which -// includes optional/forms.css) in both themes. +// explicit .sf-is-invalid / .sf-is-valid state classes (core/states.css) set. +// Loads the optimal bundle (which includes optional/forms.css) in both themes. import { test, expect } from '@playwright/test'; import { renderWithBundle, NO_TRANSITIONS_STYLE } from './render-helpers.js'; @@ -63,7 +63,7 @@ for (const theme of ['light', 'dark']) { await mount(page, ``); const cs = await computed(page, ['border-top-color']); // The border resolves through --sf-field-border-color (the same hook - // :user-invalid/:user-valid flip to danger/success). + // .sf-is-invalid/.sf-is-valid flip to danger/success — core/states.css). expect(cs['border-top-color']).toBe('rgb(1, 2, 3)'); });