Skip to main content Skip to docs navigation

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 :invalid and :valid pseudo-classes to style form controls based on validation state.
  • Progressive enhancement: Chassis uses the .was-validated class on forms to control when validation styles appear. This prevents invalid styles from showing when the page first loads.
  • Server-side fallback: The .is-invalid and .is-valid classes provide direct control for server-side validation scenarios.
  • Custom validation messages: Add contextual feedback to users with .valid-feedback and .invalid-feedback elements.
  • 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
Looks good!
Looks good!
@
Please choose a username.
Please provide a valid city.
Please select a valid state.
Please provide a valid zip.
You must agree before submitting.
html
<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.

@
html
<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.

Looks good!
Looks good!
@
Please choose a username.
Please provide a valid city.
Please select a valid state.
Please provide a valid zip.
You must agree before submitting.
html
<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:

Please enter a message in the textarea.
Example invalid feedback text
More example invalid feedback text
Example invalid select feedback
Example invalid form file feedback
html
<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.

Looks good!
Looks good!
@
Please choose a unique and valid username.
Please provide a valid city.
Please select a valid state.
Please provide a valid zip.
html
<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.

@
Enter your username
html
<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.