Bootstrap .was-validated / .is-invalid JS vs Native :user-valid / :user-invalid
classList.add('was-validated') on the form
+ per-field classList.add('is-invalid'). Native uses zero JS operations —
the browser activates :user-valid / :user-invalid automatically after user interaction.
Requires JS to add .was-validated on form submit, or .is-valid / .is-invalid per field. Generated by form-validation-state() mixin × 6 element types × 2 states × 2 selector paths.
:user-valid / :user-invalid activate automatically after user interaction (type, blur). No wrapper class, no JS event listeners, no classList manipulation.
| Metric | Bootstrap .was-validated | Native :user-valid |
|---|---|---|
| Setup time (event listeners for 50 fields) | - | - |
Form-level toggle (100 cycles × .was-validated) |
- | - |
Field-level toggle (100 cycles × 50 × .is-invalid) |
- | - |
| Instance memory cost | - | - |
|
JS code required
User-written validation script
|
- | - |
| CSS output size | - | - |
.was-validated + .is-invalid
:user-valid / :user-invalid
.was-validated exists — :valid fires on page load:valid / :invalid match immediately on page load — before the user touches anything. Bootstrap invented the .was-validated wrapper + JS to prevent premature validation styles. :user-valid / :user-invalid solve this natively.
:invalid — red border on page load:user-invalid — waits for interaction/* scss/mixins/_forms.scss — selector mixin */
@mixin form-validation-state-selector($state) {
.was-validated &:#{$state},
&.is-#{$state} {
@content;
}
}
/* scss/forms/_validation.scss — state mixin */
@mixin form-validation-state($state, $color, $icon, ...) {
.#{$state}-feedback { display: none; color: $color; }
.#{$state}-tooltip { display: none; ... }
@include form-validation-state-selector($state) {
~ .#{$state}-feedback { display: block; }
}
.form-control {
@include form-validation-state-selector($state) {
border-color: $border-color;
background-image: escape-svg($icon);
&:focus { box-shadow: $focus-box-shadow; }
}
}
textarea.form-control { ... }
.form-select { ... }
.form-control-color { ... }
.form-check-input {
@include form-validation-state-selector($state) {
border-color: $border-color;
&:checked { background-color: $color; }
~ .form-check-label { color: $color; }
}
}
.input-group { ... z-index stacking ... }
}
/* Loop × 2 states */
@each $state, $data in $form-validation-states {
@include form-validation-state($state, $data...);
}
/* User must write JS to trigger: */
form.addEventListener('submit', e => {
if (!form.checkValidity()) e.preventDefault();
form.classList.add('was-validated');
});
/* Replace entire mixin with direct pseudo-classes */
.form-control:user-valid,
.form-select:user-valid {
border-color: var(--bs-success);
background-image: url("...checkmark...");
}
.form-control:user-invalid,
.form-select:user-invalid {
border-color: var(--bs-danger);
background-image: url("...x-icon...");
}
.form-check-input:user-valid {
border-color: var(--bs-success);
}
.form-check-input:user-valid:checked {
background-color: var(--bs-success);
}
.form-check-input:user-invalid {
border-color: var(--bs-danger);
}
:user-invalid ~ .invalid-feedback {
display: block;
}
:user-valid ~ .valid-feedback {
display: block;
}
/* No .was-validated wrapper.
No .is-valid / .is-invalid classes.
No form-validation-state-selector() mixin.
No addEventListener('submit').
No classList.add().
Browser activates after user interaction.
Replaces:
forms/_validation.scss (187 lines)
mixins/_forms.scss (163 lines)
~60-80 lines generated CSS eliminated */
<!-- HTML: standard Bootstrap form, NO .was-validated, NO JS -->
<form novalidate>
<input type="email" class="form-control" required>
<div class="invalid-feedback">Please enter a valid email.</div>
<div class="valid-feedback">Looks good!</div>
<select class="form-select" required>...</select>
<input type="checkbox" class="form-check-input" required>
</form>
<style>
.form-control:user-valid { border-color: var(--bs-success); background-image: url("..."); }
.form-control:user-invalid { border-color: var(--bs-danger); background-image: url("..."); }
.form-check-input:user-valid { border-color: var(--bs-success); }
.form-check-input:user-invalid { border-color: var(--bs-danger); }
:user-invalid ~ .invalid-feedback { display: block; }
:user-valid ~ .valid-feedback { display: block; }
</style>