Skip to main content

Form Validation While Filling

This template explains how to validate form fields and show relevant error messages in an accessible manner while the user is filling a form. The moment to do this is when the user leaves a field (on blur). Validation while the user is typing in a field is usually a bad idea as the screen reader user experience can become very bad. Note that due to differeces between screen readers it is difficult to get consistent experience for all users when validation is done while filling. Usually the most user friendly way is to validate when the user submits - see another template for that.

Hint: Press F on your keyboard to view both templates and components in fullscreen and ESC to exit the fullscreen mode. You can also open the template in a new browser window.

Open in new window
<!DOCTYPE html>
<html class="duet-bg-gradient duet-sticky-footer" lang="fi">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="stylesheet" href="" integrity="sha384-5JYmtSD7nykpUvSmTW1CHMoBDkBZUpUmG0vuh+NUVtZag3F75Kr7+/JU3J7JV6Wq" crossorigin="anonymous" />
<link rel="stylesheet" href="" integrity="sha384-gAsPTC0FMrhu7u+bto7taKhZKMTMVW9nTDLoQggqkaWcIgtLIS2RXWpGBf5Xhyxn" crossorigin="anonymous" />
<link rel="stylesheet" href="" integrity="sha384-WtWdB1LQngvwOFx8qmwg0LGAtGfbAkyRQg6cUQY9kRcvHrAjnIdMTeLZW3kY72Ww" crossorigin="anonymous" />
<script type="module" src="" integrity="sha384-VfrbUATL1nVQUZDXS4nULYwa2VcYvMXgqowNlRnmJw+ysLr+PI5MX+w7C/fvMqBY" crossorigin="anonymous"></script>
<script nomodule src="" integrity="sha384-LAVmWp6MuvQApACfXECBtM099TApltfQokBGHoROZDpUq57yCSRM2c2d0CLn2J5Y" crossorigin="anonymous"></script>
<duet-header language="fi"></duet-header>
<duet-layout center>
<div slot="main">
<duet-card padding="large">
<duet-spacer breakpoint="x-small" size="medium"></duet-spacer>

<!-- The form has novalidate attribute to prevent browsers' native error messages appearing -->
<form id="example-form" action="#" novalidate>

<duet-heading level="h1" visual-level="h2">Ota yhteyttä</duet-heading>
Lähetä meille viesti kun haluat antaa palautetta tai pyytää yhteydenottoa.
Jos olet jo asiakkaamme, asiasi hoituu parhaiten, kun kirjaudut verkkopalveluun.
Täytä kaikki kentät, jotta voimme auttaa sinua mahdollisimman hyvin.
<duet-spacer size="medium"></duet-spacer>

The name validation pattern uses unicode classes to match all letters (including combining marks), not
just the latin alphabet. This is to allow names with diacritics, such as "Müller". Additionally we need to
allow spaces and some punctuation to match names like "Hélèn O'Neil" and "Matti M. Korhonen-Virtanen"

placeholder="Matti Meikäläinen"
pattern="^[\p{Letter}\p{Mark} \.\-']+$"
label="Viesti asiakaspalveluun"
caption="Kuvaile asiaasi mahdollisimman tarkasti, niin voimme auttaa sinua paremmin."
placeholder="Kirjoita tähän"
label="Yhteydenoton syy"

<duet-radio label="Palaute" value="one"></duet-radio>
<duet-radio label="Yhteydenottopyyntö" value="two"></duet-radio>

<duet-spacer size="x-large"></duet-spacer>

<!-- Alert to show if the form was not submitted due to errors -->
<duet-alert variation="danger" style="display: none;" id="submit-error-message">
<div class="visible"></div>
<!-- This is used to announce errors of each field for screen readers -->

<duet-button submit variation="primary">Lähetä viesti</duet-button>

<duet-footer logo-href="#" language="fi"></duet-footer>

// First select the form
const form = document.querySelector("#example-form")

// Then select the form fields
const formFields = {
name: form.querySelector("duet-input[name=name]"),
email: form.querySelector("duet-input[name=email]"),
city: form.querySelector("duet-select[name=city]"),
message: form.querySelector("duet-textarea"),
reason: form.querySelector("duet-radio-group[name=reason]"),

// Set the options for the city selection = [
{ label: "Helsinki", value: "1" },
{ label: "Tampere", value: "2" },
{ label: "Vantaa", value: "3" },
{ label: "Espoo", value: "4" }

// Select the error message elements
const submitErrorMessage = document.querySelector("#submit-error-message")
const submitErrorVisibleMessage = submitErrorMessage.querySelector(".visible")
const submitErrorHiddenMessage = submitErrorMessage.querySelector("duet-visually-hidden")

// Once the user types something in the name field, remove the error
for (const field in formFields) {
formFields[field].addEventListener("duetChange", function() {
formFields[field].error = ""
formFields[field].addEventListener("duetFocus", event => {
const input =
input.previousValue = input.value
formFields[field].addEventListener("duetBlur", event => {
const input =
// Only validate if the value has changed
// This prevents the error message from appearing when the user tabs through the form
if (input.previousValue !== input.value) {

const errorMessages = {
missing: "on pakollinen kenttä",
invalid: {
name: "ei voi sisältää numeroita tai erikoismerkkejä",
email: "ei ole oikeassa muodossa",
general: "on virheellinen"
notSubmitted: "Lomaketta ei lähetetty, siinä on virheitä."

function clearSubmitErrorMessage() {
submitErrorVisibleMessage.textContent = ""
submitErrorHiddenMessage.textContent = "" = "none"

function validateField(input) {
const value = input.value
const nativeElement = input.querySelector("input, textarea")
input.error = ""
// Check the field is not empty and that it is valid (use the native element's validity object)
if (!value) {
input.error = `${input.label}${errorMessages.missing}`
// Set the required attribute only when validation fails - otherwise screen readers will announce the field
// as invalid already when the user tabs to it for the first time
input.required = true
} else if (nativeElement && !nativeElement.validity.valid) {
input.error = `${input.label}${errorMessages.invalid[] || errorMessages.invalid.general}`

// Validate form also on submit
// If there are errors, prevent the form from submitting
form.addEventListener("submit", function(event) {

const errors = []

for (const field in formFields) {
const input = formFields[field]
if (input.error) {
if (errors.length) {
// Display an error the form was not submitted
submitErrorVisibleMessage.textContent = errorMessages.notSubmitted

// Announce errors of each field to screen readers
// As we use this method to announce the errors, we can set the accessibleLiveError attributes to off
submitErrorHiddenMessage.textContent = => error.error).join(", ") = "block"

// NOTE: with some forms, you may want to focus the first error, but this is usually not a good idea
// as the focusing will interrupt the screan readers reading out of the errors.
// If you want to focus the first error, you can use the following code:
// if (errors[0].setFocus) {
// // If the field has a setFocus method, use it
// errors[0].setFocus()
// } else {
// // If the field is a radio or choice, focus the first option
// errors[0].querySelector("duet-radio, duet-choice")?.setFocus()
// }

// Prevent the form from submitting

// Show data in the footer component (not part of the form validation)
const footer = document.querySelector("duet-footer")
footer.items = [
{ label: 'Hae korvausta', href: '#', icon: 'navigation-make-claim' },
{ label: 'Osta vakuutus', href: '#', icon: 'action-buy-insurance' },
{ label: 'Yhteystiedot', href: '#', icon: 'form-tel' }
] = [
{ label: 'Turvallisuus ja käyttöehdot', href: '#' },
{ label: 'Evästeet', href: '#' },
{ label: 'Henkilötietojen käsittely', href: '#' },



To install this template’s dependencies into your project, run:

npm install @duetds/components
npm install @duetds/css
npm install @duetds/fonts

For further guidelines, please see each package’s documentation.


Follow these practical tutorials to learn how to build simple page layouts using Duet’s CSS Framework, Web Components and other features:


Building Layouts


Using CLI Tools


Creating Custom Patterns


Server Side Rendering


Sharing Prototypes


Usage With Markdown


If you experience any issues while using a template, please head over to the Support page for more guidelines and help.