Form Validation
Provide clear, accessible feedback to users with Chassis CSS's form validation styles, utilizing HTML5 validation or custom implementations.
When using client-side custom validation styles and tooltips, additional work is needed to ensure proper accessibility. For better out-of-the-box accessibility, consider using server-side validation or native browser validation.
Understanding form validation
Chassis CSS provides flexible options for implementing form validation:
- Native validation: Uses the browser's built-in
:invalidand:validpseudo-classes to style form controls based on validation state. - Progressive enhancement: Chassis uses the
.was-validatedclass on forms to control when validation styles appear. This prevents invalid styles from showing when the page first loads. - Server-side fallback: The
.is-invalidand.is-validclasses provide direct control for server-side validation scenarios. - Custom validation messages: Add contextual feedback to users with
.valid-feedbackand.invalid-feedbackelements. - API integration: All validation styles work with the browser's Constraint Validation API, allowing you to create custom validation logic with JavaScript.
- Accessible feedback: Position validation messages consistently outside input groups for optimal screen reader compatibility.
Client-side validation
For custom Chassis validation styles, add the novalidate attribute to your <form> element. This disables browser default tooltips but still allows JavaScript validation using the Constraint Validation API.
Key features of Chassis validation:
- Custom feedback styles with contextual colors and icons
- Support for all form controls including input groups
- Consistent positioning of feedback messages
- JavaScript integration for dynamic validation
<form class="row g-3 needs-validation" novalidate>
<div class="medium:col-4">
<label for="validationCustom01" class="form-label">First name</label>
<input type="text" class="form-input" id="validationCustom01" value="Mark" required>
<div class="valid-feedback">
Looks good!
</div>
</div>
<div class="medium:col-4">
<label for="validationCustom02" class="form-label">Last name</label>
<input type="text" class="form-input" id="validationCustom02" value="Otto" required>
<div class="valid-feedback">
Looks good!
</div>
</div>
<div class="medium:col-4">
<label for="validationCustomUsername" class="form-label">Username</label>
<div class="input-group">
<span class="input-addon" id="inputGroupPrepend">@</span>
<input type="text" class="form-input" id="validationCustomUsername" aria-describedby="inputGroupPrepend" required>
</div>
<div class="invalid-feedback">
Please choose a username.
</div>
</div>
<div class="medium:col-6">
<label for="validationCustom03" class="form-label">City</label>
<input type="text" class="form-input" id="validationCustom03" required>
<div class="invalid-feedback">
Please provide a valid city.
</div>
</div>
<div class="medium:col-3">
<label for="validationCustom04" class="form-label">State</label>
<select class="form-select" id="validationCustom04" required>
<option selected disabled value="">Choose...</option>
<option>California</option>
<option>New York</option>
<option>Texas</option>
</select>
<div class="invalid-feedback">
Please select a valid state.
</div>
</div>
<div class="medium:col-3">
<label for="validationCustom05" class="form-label">Zip</label>
<input type="text" class="form-input" id="validationCustom05" required>
<div class="invalid-feedback">
Please provide a valid zip.
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="check-input" type="checkbox" value="" id="invalidCheck" required>
<label class="check-label" for="invalidCheck">
Agree to terms and conditions
</label>
<div class="invalid-feedback">
You must agree before submitting.
</div>
</div>
</div>
<div class="col-12">
<button class="button primary" type="submit">Submit form</button>
</div>
</form> The example below shows the JavaScript needed to implement custom validation with Chassis CSS.
// Example starter JavaScript for disabling form submissions if there are invalid fields
;(() => {
'use strict'
// Fetch all the forms we want to apply custom Chassis validation styles to
const forms = document.querySelectorAll('.needs-validation')
// Loop over them and prevent submission
Array.from(forms).forEach((form) => {
form.addEventListener(
'submit',
(event) => {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
},
false
)
})
})()
Native browser validation
If you prefer simpler implementation, you can use the browser's built-in validation. Chassis CSS works seamlessly with native validation UI, though these styles cannot be customized with CSS.
<form class="row g-3">
<div class="medium:col-4">
<label for="validationDefault01" class="form-label">First name</label>
<input type="text" class="form-input" id="validationDefault01" value="Mark" required>
</div>
<div class="medium:col-4">
<label for="validationDefault02" class="form-label">Last name</label>
<input type="text" class="form-input" id="validationDefault02" value="Otto" required>
</div>
<div class="medium:col-4">
<label for="validationDefaultUsername" class="form-label">Username</label>
<div class="input-group">
<span class="input-addon" id="inputGroupPrepend2">@</span>
<input type="text" class="form-input" id="validationDefaultUsername" aria-describedby="inputGroupPrepend2" required>
</div>
</div>
<div class="medium:col-6">
<label for="validationDefault03" class="form-label">City</label>
<input type="text" class="form-input" id="validationDefault03" required>
</div>
<div class="medium:col-3">
<label for="validationDefault04" class="form-label">State</label>
<select class="form-select" id="validationDefault04" required>
<option selected disabled value="">Choose...</option>
<option>California</option>
<option>New York</option>
<option>Texas</option>
</select>
</div>
<div class="medium:col-3">
<label for="validationDefault05" class="form-label">Zip</label>
<input type="text" class="form-input" id="validationDefault05" required>
</div>
<div class="col-12">
<div class="form-check">
<input class="check-input" type="checkbox" value="" id="invalidCheck2" required>
<label class="check-label" for="invalidCheck2">
Agree to terms and conditions
</label>
</div>
</div>
<div class="col-12">
<button class="button primary" type="submit">Submit form</button>
</div>
</form> While native browser validation styles cannot be changed with CSS, you can customize the validation message text using JavaScript's setCustomValidity() method.
Server-side validation
For applications that validate on the server, Chassis CSS provides the .is-valid and .is-invalid state classes that can be applied to form controls directly, without needing the .was-validated parent class.
For invalid fields, always connect error messages to their inputs using aria-describedby to ensure screen readers announce feedback properly. Multiple IDs can be referenced if a field has both help text and error messages.
<form class="row g-3">
<div class="medium:col-4">
<label for="validationServer01" class="form-label">First name</label>
<input type="text" class="form-input is-valid" id="validationServer01" value="Mark" required>
<div class="valid-feedback">
Looks good!
</div>
</div>
<div class="medium:col-4">
<label for="validationServer02" class="form-label">Last name</label>
<input type="text" class="form-input is-valid" id="validationServer02" value="Otto" required>
<div class="valid-feedback">
Looks good!
</div>
</div>
<div class="medium:col-4">
<label for="validationServerUsername" class="form-label">Username</label>
<div class="input-group has-validation">
<span class="input-addon" id="inputGroupPrepend3">@</span>
<input type="text" class="form-input is-invalid" id="validationServerUsername" aria-describedby="inputGroupPrepend3 validationServerUsernameFeedback" required>
</div>
<div id="validationServerUsernameFeedback" class="invalid-feedback">
Please choose a username.
</div>
</div>
<div class="medium:col-6">
<label for="validationServer03" class="form-label">City</label>
<input type="text" class="form-input is-invalid" id="validationServer03" aria-describedby="validationServer03Feedback" required>
<div id="validationServer03Feedback" class="invalid-feedback">
Please provide a valid city.
</div>
</div>
<div class="medium:col-3">
<label for="validationServer04" class="form-label">State</label>
<select class="form-select is-invalid" id="validationServer04" aria-describedby="validationServer04Feedback" required>
<option selected disabled value="">Choose...</option>
<option>California</option>
<option>New York</option>
<option>Texas</option>
</select>
<div id="validationServer04Feedback" class="invalid-feedback">
Please select a valid state.
</div>
</div>
<div class="medium:col-3">
<label for="validationServer05" class="form-label">Zip</label>
<input type="text" class="form-input is-invalid" id="validationServer05" aria-describedby="validationServer05Feedback" required>
<div id="validationServer05Feedback" class="invalid-feedback">
Please provide a valid zip.
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="check-input is-invalid" type="checkbox" value="" id="invalidCheck3" aria-describedby="invalidCheck3Feedback" required>
<label class="check-label" for="invalidCheck3">
Agree to terms and conditions
</label>
<div id="invalidCheck3Feedback" class="invalid-feedback">
You must agree before submitting.
</div>
</div>
</div>
<div class="col-12">
<button class="button primary" type="submit">Submit form</button>
</div>
</form> Supported form elements
Chassis CSS validation styles work consistently across all basic form elements:
<form class="was-validated">
<div class="mb-medium">
<label for="validationTextarea" class="form-label">Textarea</label>
<textarea class="form-input" id="validationTextarea" placeholder="Required example textarea" required></textarea>
<div class="invalid-feedback">
Please enter a message in the textarea.
</div>
</div>
<div class="form-check mb-medium">
<input type="checkbox" class="check-input" id="validationFormCheck1" required>
<label class="check-label" for="validationFormCheck1">Check this checkbox</label>
<div class="invalid-feedback">Example invalid feedback text</div>
</div>
<div class="form-check">
<input type="radio" class="check-input" id="validationFormCheck2" name="radio-stacked" required>
<label class="check-label" for="validationFormCheck2">Toggle this radio</label>
</div>
<div class="form-check mb-medium">
<input type="radio" class="check-input" id="validationFormCheck3" name="radio-stacked" required>
<label class="check-label" for="validationFormCheck3">Or toggle this other radio</label>
<div class="invalid-feedback">More example invalid feedback text</div>
</div>
<div class="mb-medium">
<select class="form-select" required aria-label="select example">
<option value="">Choose an option</option>
<option value="1">Option One</option>
<option value="2">Option Two</option>
<option value="3">Option Three</option>
</select>
<div class="invalid-feedback">Example invalid select feedback</div>
</div>
<div class="mb-medium">
<input type="file" class="form-input" aria-label="file example" required>
<div class="invalid-feedback">Example invalid form file feedback</div>
</div>
<div class="mb-medium">
<button class="button primary" type="submit" disabled>Submit form</button>
</div>
</form> Tooltip validation
For compact layouts, Chassis CSS provides tooltip-style validation feedback. Simply replace .valid-feedback and .invalid-feedback with .valid-tooltip and .invalid-tooltip classes.
Tooltips require a parent element with position: relative. The column classes in our examples already have this, but you may need to add it to your own layouts.
<form class="row g-3 needs-validation" novalidate>
<div class="medium:col-4 position-relative">
<label for="validationTooltip01" class="form-label">First name</label>
<input type="text" class="form-input" id="validationTooltip01" value="Mark" required>
<div class="valid-tooltip">
Looks good!
</div>
</div>
<div class="medium:col-4 position-relative">
<label for="validationTooltip02" class="form-label">Last name</label>
<input type="text" class="form-input" id="validationTooltip02" value="Otto" required>
<div class="valid-tooltip">
Looks good!
</div>
</div>
<div class="medium:col-4 position-relative">
<label for="validationTooltipUsername" class="form-label">Username</label>
<div class="input-group has-validation">
<span class="input-addon" id="validationTooltipUsernamePrepend">@</span>
<input type="text" class="form-input" id="validationTooltipUsername" aria-describedby="validationTooltipUsernamePrepend" required>
</div>
<div class="invalid-tooltip">
Please choose a unique and valid username.
</div>
</div>
<div class="medium:col-6 position-relative">
<label for="validationTooltip03" class="form-label">City</label>
<input type="text" class="form-input" id="validationTooltip03" required>
<div class="invalid-tooltip">
Please provide a valid city.
</div>
</div>
<div class="medium:col-3 position-relative">
<label for="validationTooltip04" class="form-label">State</label>
<select class="form-select" id="validationTooltip04" required>
<option selected disabled value="">Choose...</option>
<option>California</option>
<option>New York</option>
<option>Texas</option>
</select>
<div class="invalid-tooltip">
Please select a valid state.
</div>
</div>
<div class="medium:col-3 position-relative">
<label for="validationTooltip05" class="form-label">Zip</label>
<input type="text" class="form-input" id="validationTooltip05" required>
<div class="invalid-tooltip">
Please provide a valid zip.
</div>
</div>
<div class="col-12">
<button class="button primary" type="submit">Submit form</button>
</div>
</form> Form helps
The .form-help component is hidden by default when validation feedback is present. Add the .always-show class to keep form helps visible alongside validation feedback.
Always place form help and validation feedback elements as siblings after the input group.
The example below demonstrates proper ARIA attribute usage with aria-describedby referencing both the feedback and assist elements, ensuring screen readers announce both the error state and help text. Check the Smashing Magazine article for more details on accessible form validation.
<div class="input-group">
<span class="input-addon">@</span>
<input type="text" class="form-input" id="username-input"
aria-describedby="username-feedback username-help" placeholder="Username" required>
</div>
<div class="invalid-feedback" id="username-feedback"></div>
<div class="form-help always-show" id="username-help">Enter your username</div>
<div class="mt-medium">
<button class="button primary" type="button" onclick="validateUsername()">Validate username</button>
</div> function validateUsername() {
const input = document.getElementById("username-input")
const feedback = document.getElementById("username-feedback")
// Clear previous validation state
input.classList.remove("is-invalid")
feedback.innerText = ""
if (!input.validity.valid) {
// Apply invalid state
input.classList.add("is-invalid")
feedback.innerText = "Username is invalid."
}
} CSS
Chassis CSS uses a powerful combination of Sass and CSS variables to create a flexible form system that adapts to themes and design requirements.
Custom properties
Check the overview page to see the CSS variables shared by all form elements.
Sass variables
These Sass variables control the component's appearance and can be modified in the project's variables file before compilation.
$form-validated-class: was-validated;
$form-valid-class: is-valid;
$form-invalid-class: is-invalid;
$form-valid-tooltip-fg-color: var(--#{$prefix}success-fg-solid);
$form-valid-tooltip-bg-color: var(--#{$prefix}success-bg-solid);
$form-invalid-tooltip-fg-color: var(--#{$prefix}danger-fg-solid);
$form-invalid-tooltip-bg-color: var(--#{$prefix}danger-bg-solid);
$form-valid-box-shadow: 0 0 $focus-ring-blur $form-input-focus-width #{to-opacity(var(--#{$prefix}success), $focus-ring-opacity)};
$form-invalid-box-shadow: 0 0 $focus-ring-blur $form-input-focus-width #{to-opacity(var(--#{$prefix}danger), $focus-ring-opacity)};
$form-feedback-icon-size: var(--#{$prefix}icon-size);
$form-feedback-icon-input-position: center right var(--#{$prefix}padding-x);
$form-feedback-icon-input-padding-e: #{calc($form-feedback-icon-size + var(--#{$prefix}padding-x) + var(--#{$prefix}gap))};
$form-feedback-icon-textarea-position: top var(--#{$prefix}padding-y) right var(--#{$prefix}padding-x);
$form-feedback-icon-select-position: center right #{calc(var(--#{$prefix}padding-x) + var(--#{$prefix}gap) + var(--#{$prefix}caret-size))};
$form-feedback-icon-select-padding-e: #{calc($form-feedback-icon-size + var(--#{$prefix}caret-size) + var(--#{$prefix}padding-x) + var(--#{$prefix}gap) * 2)};
Design tokens
These design tokens with $cx prefixes are managed by design teams in Figma using the Tokens
Studio plugin. See the design tokens page for more details.
The following design tokens define the validation icons and sizes:
$form-feedback-valid-icon: svg-icon($cx-icon-form-input-feedback-valid);
$form-feedback-invalid-icon: svg-icon($cx-icon-form-input-feedback-invalid);
$form-medium-icon-size: $cx-size-form-input-medium-icon;
$form-large-icon-size: $cx-size-form-input-large-icon;
$form-small-icon-size: $cx-size-form-input-small-icon;
Check the overview page to see the design tokens shared by all form elements.