feat(forms): add .sf-live-validate — opt-in native validation feedback - #604
Conversation
Follow-up to the removal of unconditional :user-invalid/:user-valid
auto-colouring. On reflection the removal was the right call — :user-invalid
fires on simple focus+blur (verified: a real submit-button click is the
reliable trigger; bare focus()/blur() does not fire it reliably even inside a
<form>), so a still-empty required field could be marked invalid mid
multi-field form-fill, before any submit was attempted. This is a known
anti-pattern; Bootstrap's own .was-validated gate exists specifically to avoid
firing validation styling from bare native pseudo-classes.
.sf-live-validate is that same gate, framework-native: apply it to a <form> or
<fieldset> to scope the :user-invalid/:user-valid -> --sf-field-border-color
pivot to that subtree, active only once a submit has actually been attempted.
Restores the zero-JS convenience the removed behaviour offered, without its
premature-feedback failure mode — and without requiring per-field JS the way
the explicit .sf-is-invalid/.sf-is-valid classes do.
- optional/forms.css: new scoped rule pair, right where the unconditional
version used to live.
- docs/token-annotations.json: class description (flows into
docs/api-index.{json,md} and docs/classes.md via `npm run docs`).
- docs/migration.md: cross-reference added to the existing forms
breaking-change section (this PR is additive, not itself breaking).
- CHANGELOG.md: Features entry.
- tests/forms.spec.js: new deterministic Playwright test. :user-invalid does
not reliably match via synthetic focus()/blur() in headless automation (the
reason the removal PR shipped without a trigger-level test), but a real
submit-button click does, reliably, once the native validation bubble is
suppressed via preventDefault() on the `invalid` event — the standard
pairing for custom validation styling. Asserts --sf-field-border-color
directly (not border-top-color, which briefly reflects Chromium's own
transient bubble-UI rendering) — matching the assertion style already used
in tests/states-full.spec.js for the same token.
Verified in both themes via a direct Chromium harness before porting into the
Playwright spec. Full node --test unit suite (109/109) and all 10 CSS gates
pass; audit confirms 293 .sf-classes (+1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnwqYHgHNAWPbsUJWUeY1w
|
Warning Review limit reached
Next review available in: 51 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughAdds the opt-in ChangesNative validation feedback
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant Form
participant Browser
participant LiveValidateCSS
Form->>Browser: Submit required fields
Browser->>LiveValidateCSS: Apply native validation states
LiveValidateCSS->>Form: Set field border color within .sf-live-validate
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds
Confidence Score: 4/5Safe to merge; the CSS addition is scoped, purely opt-in, and touches no existing rules. The CSS implementation is clean: two scoped rule pairs inside the existing
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Form submit click] --> B{Form has .sf-live-validate?}
B -- No --> C[Browser fires :user-invalid on field]
C --> D[No CSS rule matches — --sf-field-border-color unchanged]
B -- Yes --> E[Browser fires :user-invalid on field]
E --> F[.sf-live-validate input:user-invalid sets --sf-field-border-color = --sf-color-danger]
F --> G[Field border turns danger color]
H[User types invalid value then blurs] --> B
style D fill:#f9f9f9,stroke:#ccc
style G fill:#fde,stroke:#f66
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[Form submit click] --> B{Form has .sf-live-validate?}
B -- No --> C[Browser fires :user-invalid on field]
C --> D[No CSS rule matches — --sf-field-border-color unchanged]
B -- Yes --> E[Browser fires :user-invalid on field]
E --> F[.sf-live-validate input:user-invalid sets --sf-field-border-color = --sf-color-danger]
F --> G[Field border turns danger color]
H[User types invalid value then blurs] --> B
style D fill:#f9f9f9,stroke:#ccc
style G fill:#fde,stroke:#f66
Reviews (1): Last reviewed commit: "feat(forms): add .sf-live-validate — opt..." | Re-trigger Greptile |
| /* Opt-in native validation feedback. Unconditional :user-invalid/:user-valid | ||
| styling was removed in 0.8.0 (see docs/migration.md): it fires on simple | ||
| focus+blur, so a still-empty required field could be marked invalid mid | ||
| multi-field form-fill, before any submit was attempted — the pattern | ||
| Bootstrap's own .was-validated gate exists to avoid. .sf-live-validate is | ||
| that same gate: put it on a <form> or <fieldset> to scope the pivot to | ||
| that subtree, so it only lights up on a real submit attempt (or explicit | ||
| interaction) instead of applying everywhere by default. */ | ||
| .sf-live-validate input:user-invalid, | ||
| .sf-live-validate select:user-invalid, | ||
| .sf-live-validate textarea:user-invalid { | ||
| --sf-field-border-color: var(--sf-color-danger); | ||
| } | ||
|
|
||
| .sf-live-validate input:user-valid, | ||
| .sf-live-validate select:user-valid, | ||
| .sf-live-validate textarea:user-valid { | ||
| --sf-field-border-color: var(--sf-color-success); | ||
| } |
There was a problem hiding this comment.
docs/llm-guide.md not updated — CLAUDE.md requirement
CLAUDE.md mandates: "Any PR that touches core/*.css, optional/*.css, or token-registry.json must also review docs/llm-guide.md and update it if needed." This PR touches optional/forms.css but docs/llm-guide.md is not among the changed files. Section 9.10 of the guide documents --sf-field-border-color as the hook set by validation states, but doesn't mention .sf-live-validate as the mechanism to re-enable native-triggered feedback. An LLM consuming the guide to help a user implement form validation would have no way to learn this class exists.
Context Used: CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| .sf-live-validate input:user-invalid, | ||
| .sf-live-validate select:user-invalid, | ||
| .sf-live-validate textarea:user-invalid { | ||
| --sf-field-border-color: var(--sf-color-danger); | ||
| } | ||
|
|
||
| .sf-live-validate input:user-valid, | ||
| .sf-live-validate select:user-valid, | ||
| .sf-live-validate textarea:user-valid { | ||
| --sf-field-border-color: var(--sf-color-success); | ||
| } |
There was a problem hiding this comment.
Interaction with explicit
.sf-is-invalid / .sf-is-valid state classes inside .sf-live-validate
The new rules have specificity 0-2-1 (.sf-live-validate + element + pseudo-class), while the explicit .sf-is-invalid / .sf-is-valid state classes live in core/states.css. If a field inside .sf-live-validate has .sf-is-invalid applied for custom/server-side validation but the browser considers it natively valid (:user-valid), the .sf-live-validate input:user-valid rule will set --sf-field-border-color to --sf-color-success, silently overriding the manually-applied invalid styling. This is an edge case, but a silent, non-obvious CSS cascade conflict worth documenting in migration.md or a code comment.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
docs/api-index.json (1)
24890-24890: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCategory label "Forms (classless)" is a mismatch for a class-based opt-in.
sf-live-validateis an explicitly applied CSS class (.sf-live-validateon<form>/<fieldset>), not a "classless" enhancement, yet it's filed under the "Forms (classless)" category (also reflected in theby_categorycounts). This is likely inherited from a category bucket meant for classless form styling defaults; consider a distinct category (e.g., "Forms") for class-based form opt-ins to avoid confusing downstream consumers of this generated index.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/api-index.json` at line 24890, The category assigned to sf-live-validate is incorrect for its class-based opt-in. Update its entry in the generated API index to use the appropriate distinct Forms category, and synchronize any related by_category counts or references so downstream index data consistently reflects the new category.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/migration.md`:
- Around line 96-109: Update the forms guidance in llm-guide.md to document that
native :user-invalid/:user-valid feedback is no longer unconditional and is now
enabled opt-in by adding .sf-live-validate to a form or fieldset. Include the
submit-attempt gating and subtree scoping behavior, with an appropriate usage
example consistent with the migration documentation.
---
Nitpick comments:
In `@docs/api-index.json`:
- Line 24890: The category assigned to sf-live-validate is incorrect for its
class-based opt-in. Update its entry in the generated API index to use the
appropriate distinct Forms category, and synchronize any related by_category
counts or references so downstream index data consistently reflects the new
category.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 634dfccb-cef8-42d5-9514-37c8086f9c78
📒 Files selected for processing (12)
CHANGELOG.mdconfigurator/src/data/classes.generated.jsondemos/full-api-demo-with-overrides.htmldemos/full-api-demo.htmldocs/api-index.jsondocs/api-index.mddocs/classes.mddocs/migration.mddocs/registry.jsondocs/token-annotations.jsonoptional/forms.csstests/forms.spec.js
| **Want the native-triggered feedback back, without writing JS?** Add | ||
| `.sf-live-validate` to the `<form>` (or a `<fieldset>`) — it re-enables the | ||
| `:user-invalid`/`:user-valid` → `--sf-field-border-color` pivot, scoped to that | ||
| subtree, but only once a submit has actually been attempted (not on simple | ||
| focus+blur, which is what made the original unconditional behaviour fire | ||
| prematurely mid-form-fill): | ||
|
|
||
| ```html | ||
| <form class="sf-live-validate"> | ||
| <input type="email" required> | ||
| <button type="submit">Submit</button> | ||
| </form> | ||
| ``` | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm llm-guide.md references the new class / was touched in this PR.
rg -n "sf-live-validate" docs/llm-guide.md
git log --oneline -- docs/llm-guide.md | head -5Repository: codeslash-dev/SLASHED
Length of output: 655
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== files ==\n'
git ls-files docs/migration.md docs/llm-guide.md optional/forms.css || true
printf '\n== migration excerpt ==\n'
sed -n '80,130p' docs/migration.md
printf '\n== llm-guide excerpt (search terms) ==\n'
rg -n -C 3 "form|validate|token role|browser support|user-invalid|user-valid|sf-live-validate|border-color" docs/llm-guide.md || true
printf '\n== forms.css excerpt (search terms) ==\n'
rg -n -C 3 "sf-live-validate|user-invalid|user-valid|sf-field-border-color|form|fieldset" optional/forms.css || trueRepository: codeslash-dev/SLASHED
Length of output: 10362
Document .sf-live-validate in docs/llm-guide.md
optional/forms.css now makes native validation feedback opt-in. Add the new .sf-live-validate gate and the removed unconditional :user-invalid/:user-valid behavior to the guide so the forms docs match the shipped behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/migration.md` around lines 96 - 109, Update the forms guidance in
llm-guide.md to document that native :user-invalid/:user-valid feedback is no
longer unconditional and is now enabled opt-in by adding .sf-live-validate to a
form or fieldset. Include the submit-attempt gating and subtree scoping
behavior, with an appropriate usage example consistent with the migration
documentation.
Source: Coding guidelines
…e-safety note Address CodeRabbit + Greptile review on PR #604: - docs/llm-guide.md: document .sf-live-validate as the opt-in gate for native :user-invalid/:user-valid feedback (CLAUDE.md mandates reviewing this file for any optional/*.css change; both reviewers independently flagged the omission, even though llm-guide.md was previously 100% token-only). - scripts/lib/api-index/extract.js: optional/forms.css was hardcoded 'Forms (classless)' from when the file had zero .sf-* classes; now that .sf-live-validate exists, drop the stale qualifier. Regenerated docs/api-index.{json,md} + configurator's synced copy. - optional/forms.css: add a code comment addressing Greptile's cascade concern — verified (not just asserted) in Chromium that a manually-applied .sf-is-invalid always wins over .sf-live-validate's native-triggered colour, regardless of selector specificity, because slashed.states is declared AFTER slashed.forms in the layer order (core/layers.css) — cascade layer order beats specificity. Greptile's specific claim (specificity 0-2-1 beats 0-1-0, so the native rule would silently override the manual one) doesn't hold once layers are accounted for; documented the real (safe) behaviour instead of "fixing" a conflict that doesn't exist. Full node --test unit suite (109/109) and all 10 CSS gates pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnwqYHgHNAWPbsUJWUeY1w
Why
Follow-up to #603 (removing unconditional
:user-invalid/:user-validauto-colouring). On reflection, that removal was the right call, but for a more precise reason than originally documented::user-invalidfires on simple focus+blur — verified in this PR's own investigation: a real submit-button click reliably triggers it, but a barefocus()/blur()does not, even inside a<form>. In practice this means a still-empty required field gets marked invalid the moment a user tabs past it while filling out a multi-field form — before they've attempted to submit anything. This is a known, named anti-pattern: Bootstrap's own.was-validatedclass exists specifically to gate validation styling behind an explicit "the user tried to submit" signal, rather than firing from bare native pseudo-classes unconditionally.So the removal correctly killed the anti-pattern — but it also took away a real, useful zero-JS convenience for consumers who do want native-triggered feedback (very common: "colour the field red once you hit submit").
What
.sf-live-validateis SLASHED's own version of Bootstrap's.was-validatedgate — scoped, opt-in, and only active on a real submit attempt:Within that subtree,
:user-invalid/:user-validoninput/select/textareadrive--sf-field-border-color— same token the explicit.sf-is-invalid/.sf-is-validclasses (core/states.css) already set. Outside.sf-live-validate, nothing changes — nothing is scoped, nothing colours automatically.optional/forms.css— the scoped rule pair, in the same spot the unconditional version used to live.docs/token-annotations.json— class description (flows intodocs/api-index.{json,md}/docs/classes.mdvianpm run docs; 293 → verified, +1 class, 329 → 330).docs/migration.md— cross-reference added to the existing forms breaking-change section (this PR itself is additive, not breaking).CHANGELOG.md— Features entry.Testing note
:user-invaliddoesn't reliably match via syntheticfocus()/blur()in headless automation — confirmed directly, which is why #603 shipped without a trigger-level test. A real submit-button click is reliable, once the native validation bubble is suppressed viapreventDefault()on theinvalidevent (the standard pairing for custom validation styling — you virtually never want both the native bubble and custom border colouring).tests/forms.spec.jsuses exactly this pattern; verified independently in a direct Chromium harness (both themes) before porting into the Playwright spec. Assertion targets--sf-field-border-colordirectly rather than the renderedborder-top-color, which transiently reflects Chromium's own bubble-UI rendering — matching the assertion style already used for this token intests/states-full.spec.js.Verification
Full node
--testunit suite (109/109) and all 10 CSS gates pass.auditconfirms 293.sf-classes(was 292, +1 =.sf-live-validate); no token-registry drift (no new--sf-*token — reuses--sf-field-border-color).Notes
main(includes refactor(components)!: drop the .sf-card auto-shrink of nested button labels #602/refactor(forms)!: stop auto-colouring fields on native :user-invalid/:user-valid #603) — isolated diff (12 files, incl. 2 auto-regenerated demos).🤖 Generated with Claude Code
Generated by Claude Code
Summary by CodeRabbit
New Features
.sf-live-validateclass to enable native form validation feedback within a specific form or fieldset.Documentation
Tests
.sf-live-validate.