Form validation
Provide valuable, actionable feedback to your users with HTML5 form validation, via custom styles and JavaScript.
On this page
Browsers' built-in form validation is the preferred way to handle client-side validation. When using semantic inputs and validation constraints attributes (like required, pattern...), the browser automatically adds a :user-invalid pseudo-class on invalid fields that OUDS Web will style.
Alternatively you can programmatically add an aria-invalid="true" attribute to manually mark a field as invalid, this is especially useful for server-side validation.
How it works
OUDS Web provides custom feedback styles that apply custom colors, borders, focus styles, and background icons to better communicate feedback. To use them, you’ll need to add the novalidate boolean attribute to your <form>. This disables the browser default feedback tooltips, but still provides access to the form validation APIs in JavaScript.
Here’s how form validation works with OUDS Web:
- HTML form validation is applied via CSS’s pseudo-class
:user-invalid. It applies to<input />,<select>, and<textarea>elements. :user-invalidwill only be applied after the user has interacted with the field. Read more about:user-invalidbehavior.- As a fallback, an
aria-invalid="true"attribute may be used instead of the pseudo-class:user-invalidfor server-side validation. - All modern browsers support the constraint validation API, a series of JavaScript methods for validating form controls. You may provide custom validity messages with
setCustomValidityin JavaScript. - Feedback messages should use our custom feedback styles with additional HTML and CSS, rather than the browser defaults (which differ for each browser and can't be styled via CSS).
With that in mind, consider the following demos for our custom form validation styles and optional server-side ARIA attributes.
Accessibility
OUDS Web's form elements are designed to be accessible in particular by always using a <label> linked to a form element (beware though to use the right id). This allows assistive technologies to convey the purpose of each form field to users.
You must take care to specify the validation rules for each field using an appropriate attribute like required, pattern, min, minLength, etc. This allows assistive technologies to understand the validation requirements for each field and communicate them to users. The form fields will be marked as :user-invalid if the user input doesn't meet the specified validation rules. It is also a good practice to add correct inputmode and autocomplete attributes on your fields as needed.
To make your form validation accessible, you also have to ensure that any feedback messages are properly associated with the relevant form field using aria-describedby when the field becomes invalid. This is important for users of assistive technologies, as it allows them to understand what went wrong and how to fix it. This is done with a bit of JavaScript in the following example.
Client-side
Try to submit the form below; our JavaScript will intercept the submit event, and you’ll see the :user-invalid styles applied to the fields. For invalid fields, it also associates the invalid feedback/error message with the relevant form field using aria-describedby (noting that this attribute allows more than one id to be referenced, in case the field already points to additional description/helper text).
<form class="col-sm-7 needs-validation" novalidate>
<p>
This form uses client-side validation with custom validation styles. Try to submit the form without filling it out to see them in action.
</p>
<p class="fw-bold">
Required fields are marked with a star.
</p>
<fieldset class="control-items-list mb-large">
<legend>Title <span aria-hidden="true">*</span></legend>
<div class="radio-button-item component-max-width">
<div class="control-item-assets-container">
<input class="control-item-indicator" type="radio" value="" name="title" id="mr" data-errormessage="titleFeedback" required />
</div>
<div class="control-item-text-container">
<label class="control-item-label" for="mr">Mr.</label>
</div>
</div>
<div class="radio-button-item component-max-width">
<div class="control-item-assets-container">
<input class="control-item-indicator" type="radio" value="" name="title" id="ms" data-errormessage="titleFeedback" required />
</div>
<div class="control-item-text-container">
<label class="control-item-label" for="ms">Ms.</label>
</div>
</div>
<div class="radio-button-item component-max-width">
<div class="control-item-assets-container">
<input class="control-item-indicator" type="radio" value="" name="title" id="other" data-errormessage="titleFeedback" required />
</div>
<div class="control-item-text-container">
<label class="control-item-label" for="other">Other or prefer not to choose</label>
</div>
</div>
<p class="control-item-error-message" id="titleFeedback">
You must choose an option for the title.
</p>
</fieldset>
<div class="text-input component-max-width mb-large">
<div class="text-input-container">
<label for="username">Username <span aria-hidden="true">*</span></label>
<input type="text" class="text-input-field" id="username" data-errormessage="usernameFeedback" autocomplete="username" placeholder=" " required>
</div>
<p id="usernameFeedback" class="error-text">
Username is required.
</p>
</div>
<div class="text-input component-max-width mb-large">
<div class="text-input-container">
<label for="inputPassword">Password <span aria-hidden="true">*</span></label>
<input type="password" id="inputPassword" class="text-input-field" aria-describedby="inputPasswordPrefixHelper" data-errormessage="passwordFeedback" autocomplete="new-password" placeholder=" " required minlength="8">
<button class="btn btn-minimal btn-icon">
<svg aria-hidden="true">
<use xlink:href="/orange/docs/1.0/assets/img/ouds-web-sprite.svg#accessibility-vision"/>
</svg>
<span class="visually-hidden">Show password</span>
</button>
</div>
<p id="inputPasswordPrefixHelper" class="helper-text">Enter a password with at least 8 characters.</p>
<p id="passwordFeedback" aria-live="polite" class="error-text">
Password is required and must be at least 8 characters.
</p>
</div>
<div class="select-input component-max-width mb-large">
<div class="select-input-container">
<label for="continent">Continent <span aria-hidden="true">*</span></label>
<select class="select-input-field" id="continent" data-errormessage="continentFeedback" required>
<option value="" disabled selected></option>
<option value="1">Europe</option>
<option value="2">Oceania</option>
<option value="3">America</option>
<option value="4">Asia</option>
<option value="5">Africa</option>
</select>
</div>
<p id="continentFeedback" class="error-text">
Continent is required.
</p>
</div>
<div class="text-area component-max-width mb-large">
<div class="text-area-container">
<label for="comments">Comments <span aria-hidden="true">*</span></label>
<textarea id="comments" class="text-area-field" data-errormessage="commentsFeedback" required></textarea>
</div>
<p id="commentsFeedback" class="error-text">
Comments are required.
</p>
</div>
<div class="switch-item-container mb-large">
<div class="switch-item control-item-reverse component-max-width">
<div class="control-item-assets-container">
<input class="control-item-indicator" type="checkbox" role="switch" value="" id="readTermsAndConditions" data-errormessage="readTermsAndConditionsFeedback" required />
</div>
<div class="control-item-text-container">
<label class="control-item-label" for="readTermsAndConditions">I have read the terms and conditions <span aria-hidden="true">*</span></label>
</div>
</div>
<p class="control-item-error-message mb-xsmall" id="readTermsAndConditionsFeedback">
You must confirm that you have read the terms and conditions.
</p>
</div>
<div class="checkbox-item-container mb-large">
<div class="checkbox-item component-max-width">
<div class="control-item-assets-container">
<input class="control-item-indicator" type="checkbox" value="" id="agreeTermsAndConditions"
data-errormessage="agreeTermsAndConditionsFeedback" required/>
</div>
<div class="control-item-text-container">
<label class="control-item-label" for="agreeTermsAndConditions">I agree to the terms and conditions <span aria-hidden="true">*</span></label>
</div>
</div>
<p class="control-item-error-message" id="agreeTermsAndConditionsFeedback">
You must accept the terms and conditions to proceed.
</p>
</div>
<button type="submit" class="btn btn-strong">Submit</button>
</form> The following script is just a stub doing the minimum to handle inputs descriptions and ARIA descriptions when fields are invalid. We are using a data attribute to store the error description id and change the aria-describedby attribute when necessary.
// Example starter JavaScript for managing a11y in forms with client-side validation
(() => {
'use strict'
function manageFeedbackMessage(field) {
// Get the ID of the feedback message from the attribute data-errormessage
const errorMessageId = field.dataset.errormessage
if (!errorMessageId) {
return
}
const currentDescription = field.getAttribute('aria-describedby') || ''
// aria-describedby can contain multiple space separated ids;
// we need to preserve everything other than the feedback id
const tokens = currentDescription
.split(/\s+/)
.filter(token => token && token !== errorMessageId)
if (!field.checkValidity()) {
// Add feedback id on aria-describedby if field invalid
tokens.unshift(errorMessageId)
}
// Apply update on aria-describedby
if (tokens.length > 0) {
field.setAttribute('aria-describedby', tokens.join(' '))
} else {
field.removeAttribute('aria-describedby')
}
}
// Fetch all the forms we want to apply custom OUDS Web validation styles to
const forms = document.querySelectorAll('.needs-validation')
// Loop over them and prevent submission
forms.forEach(form => {
// Gets all the fields of the form (input, select, textarea)
const fields = form.querySelectorAll('input, select, textarea')
// Initially manages feedback messages and add input listeners
fields.forEach(field => {
field.addEventListener('input', () => {
manageFeedbackMessage(field)
})
})
form.addEventListener('submit', event => {
fields.forEach(field => {
manageFeedbackMessage(field)
})
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
// Focus on first error for accessibility
form.querySelector(':invalid').focus()
}
}, false)
})
})()
Server-side
We recommend using client-side validation before server-side validation. If server-side validation returns any invalid field, you can indicate it by setting the aria-invalid attribute to true.
For invalid fields, ensure that the invalid feedback/error message is associated with the relevant form field using aria-describedby (noting that this attribute allows more than one id to be referenced, in case the field already points to additional description/helper text).
<form novalidate>
<p>
This form simulates server-side validation with custom validation styles.
</p>
<p class="fw-bold">
Required fields are marked with a star.
</p>
<fieldset class="control-items-list mb-large">
<legend>Title <span aria-hidden="true">*</span></legend>
<div class="radio-button-item component-max-width">
<div class="control-item-assets-container">
<input class="control-item-indicator" aria-invalid="true" type="radio" value="" name="title" id="mr2" aria-describedby="title2Feedback" required />
</div>
<div class="control-item-text-container">
<label class="control-item-label" for="mr2">Mr.</label>
</div>
</div>
<div class="radio-button-item component-max-width">
<div class="control-item-assets-container">
<input class="control-item-indicator" aria-invalid="true" type="radio" value="" name="title" id="ms2" aria-describedby="title2Feedback" required />
</div>
<div class="control-item-text-container">
<label class="control-item-label" for="ms2">Ms.</label>
</div>
</div>
<div class="radio-button-item component-max-width">
<div class="control-item-assets-container">
<input class="control-item-indicator" aria-invalid="true" type="radio" value="" name="title" id="other2" aria-describedby="title2Feedback" required />
</div>
<div class="control-item-text-container">
<label class="control-item-label" for="other2">Other or prefer not to choose</label>
</div>
</div>
<p class="control-item-error-message" id="title2Feedback">
You must choose an option for the title.
</p>
</fieldset>
<div class="text-input component-max-width mb-large">
<div class="text-input-container">
<label for="username2">Username <span aria-hidden="true">*</span></label>
<input type="text" class="text-input-field" aria-invalid="true" id="username2" aria-describedby="username2Feedback" autocomplete="username" placeholder=" " required>
</div>
<p id="username2Feedback" class="error-text">
Username is required.
</p>
</div>
<div class="text-input component-max-width mb-large">
<div class="text-input-container">
<label for="inputPassword2">Password <span aria-hidden="true">*</span></label>
<input type="password" id="inputPassword2" class="text-input-field" aria-invalid="true" aria-describedby="inputPassword2PrefixHelper password2Feedback" autocomplete="new-password" placeholder=" " required minlength="8">
<button class="btn btn-minimal btn-icon">
<svg aria-hidden="true">
<use xlink:href="/orange/docs/1.0/assets/img/ouds-web-sprite.svg#accessibility-vision"/>
</svg>
<span class="visually-hidden">Show password</span>
</button>
</div>
<p id="inputPassword2PrefixHelper" class="helper-text">Enter a password with at least 8 characters.</p>
<p id="password2Feedback" class="error-text">
Password is required and must be at least 8 characters.
</p>
</div>
<div class="select-input component-max-width mb-large">
<div class="select-input-container">
<label for="continent2">Continent <span aria-hidden="true">*</span></label>
<select class="select-input-field" aria-invalid="true" id="continent2" aria-describedby="continent2Feedback" required>
<option value="" disabled selected></option>
<option value="1">Europe</option>
<option value="2">Oceania</option>
<option value="3">America</option>
<option value="4">Asia</option>
<option value="5">Africa</option>
</select>
</div>
<p id="continent2Feedback" class="error-text">
Continent is required.
</p>
</div>
<div class="text-area component-max-width mb-large">
<div class="text-area-container">
<label for="comments2">Comments <span aria-hidden="true">*</span></label>
<textarea id="comments2" class="text-area-field" aria-invalid="true" aria-describedby="comments2Feedback" required></textarea>
</div>
<p id="comments2Feedback" class="error-text">
Comments are required.
</p>
</div>
<div class="switch-item-container mb-large">
<div class="switch-item control-item-reverse component-max-width">
<div class="control-item-assets-container">
<input class="control-item-indicator" aria-invalid="true" type="checkbox" role="switch" value="" id="readTermsAndConditions2" aria-describedby="readTermsAndConditions2Feedback" required />
</div>
<div class="control-item-text-container">
<label class="control-item-label" for="readTermsAndConditions2">I have read the terms and conditions <span aria-hidden="true">*</span></label>
</div>
</div>
<p class="control-item-error-message mb-xsmall" id="readTermsAndConditions2Feedback">
You must confirm that you have read the terms and conditions.
</p>
</div>
<div class="checkbox-item-container mb-large">
<div class="checkbox-item component-max-width">
<div class="control-item-assets-container">
<input class="control-item-indicator" aria-invalid="true" type="checkbox" value="" id="agreeTermsAndConditions2"
aria-describedby="agreeTermsAndConditions2Feedback" required/>
</div>
<div class="control-item-text-container">
<label class="control-item-label" for="agreeTermsAndConditions2">I agree to the terms and conditions <span aria-hidden="true">*</span></label>
</div>
</div>
<p class="control-item-error-message" id="agreeTermsAndConditions2Feedback">
You must accept the terms and conditions to proceed.
</p>
</div>
</form>