Form validation and counter
Two small components that work on top of the attributes you already write. The form component turns the browser's constraint validation into inline messages, a summary and sensible focus, with messages you control per error type. The counter shows characters or words next to a text field and can mark the field invalid when a soft limit is exceeded. Without JavaScript the browser validates and maxlength limits the text as usual.
<form class="iv-form" data-iv-component="form" data-iv-summary="true" data-iv-validate-on="blur" action="#" method="get" style="max-width: 34rem" aria-describedby="v-note">
<p class="iv-u-text-sm iv-u-text-muted iv-u-m-0" id="v-note">Demo form with made-up fields. Nothing is sent to a server: a valid submit only reloads this page.</p>
<div class="iv-field">
<label class="iv-label" for="v-name">Full name <span class="iv-label__required" aria-hidden="true">*</span></label>
<input class="iv-input" id="v-name" name="name" type="text" required minlength="3" autocomplete="name"
data-iv-error-value-missing="Enter the name that should appear on the invoice."
data-iv-error-too-short="Use at least 3 characters, so the name can be matched.">
</div>
<div class="iv-field">
<label class="iv-label" for="v-email">Work email <span class="iv-label__required" aria-hidden="true">*</span></label>
<input class="iv-input" id="v-email" name="email" type="email" required autocomplete="email" aria-describedby="v-email-help"
data-iv-error-value-missing="Enter an email address we can reply to."
data-iv-error-type-mismatch="Write the whole address, like [email protected].">
<p class="iv-field__help iv-u-m-0" id="v-email-help">Used only for this demo; it is never stored.</p>
</div>
<div class="iv-field">
<label class="iv-label" for="v-site">Website</label>
<input class="iv-input" id="v-site" name="site" type="url" placeholder="https://example.com" autocomplete="url"
data-iv-error-type-mismatch="Start the address with https:// so it can be opened.">
</div>
<div class="iv-field">
<label class="iv-label" for="v-age">Age <span class="iv-label__required" aria-hidden="true">*</span></label>
<input class="iv-input" id="v-age" name="age" type="number" required min="18" max="120" inputmode="numeric"
data-iv-error-value-missing="Enter your age in years."
data-iv-error-range-underflow="This demo is for people aged 18 or over."
data-iv-error-range-overflow="Enter an age of 120 or less.">
</div>
<div class="iv-field">
<label class="iv-label" for="v-start">Start date <span class="iv-label__required" aria-hidden="true">*</span></label>
<input class="iv-input" id="v-start" name="start" type="date" required min="2026-10-01"
data-iv-error-value-missing="Pick the day the plan should start."
data-iv-error-range-underflow="Pick a day from 1 October 2026 onwards.">
</div>
<div class="iv-field">
<label class="iv-label" for="v-plan">Plan <span class="iv-label__required" aria-hidden="true">*</span></label>
<select class="iv-select" id="v-plan" name="plan" required
data-iv-error-value-missing="Choose one of the three plans.">
<option value="">Choose a plan</option>
<option value="starter">Starter</option>
<option value="team">Team</option>
<option value="enterprise">Enterprise</option>
</select>
</div>
<div class="iv-field">
<label class="iv-label" for="v-pass">Password <span class="iv-label__required" aria-hidden="true">*</span></label>
<input class="iv-input" id="v-pass" name="password" type="password" required pattern="[A-Za-z0-9]{8,}" autocomplete="new-password"
data-iv-valid-sample="Bilbao2026"
aria-describedby="v-pass-help"
data-iv-error-value-missing="Choose a password for the demo account."
data-iv-error-pattern-mismatch="Use 8 characters or more, letters and digits only.">
<p class="iv-field__help iv-u-m-0" id="v-pass-help">Made-up account: the value never leaves the page.</p>
</div>
<div class="iv-field">
<label class="iv-check">
<input type="checkbox" id="v-terms" name="terms" required
data-iv-error-value-missing="Tick the box to accept the demo terms.">
I accept the terms of this demo
</label>
</div>
<div class="iv-cluster">
<button class="iv-button iv-button--primary" type="submit">Create account (demo)</button>
<button class="iv-button iv-button--ghost" type="reset">Reset</button>
</div>
</form>// data-iv-component="form"
{
summary: true,
validateOn: "blur",
errorValueMissing: "Enter the name that should appear on the invoice.",
errorTooShort: "Use at least 3 characters, so the name can be matched.",
errorTypeMismatch: "Write the whole address, like [email protected].",
errorRangeUnderflow: "This demo is for people aged 18 or over.",
errorRangeOverflow: "Enter an age of 120 or less.",
validSample: "Bilbao2026",
errorPatternMismatch: "Use 8 characters or more, letters and digits only.",
}Messages
Each control can carry one message per error type: data-iv-error-value-missing, -type-mismatch, -pattern-mismatch, -too-short, -too-long, -range-underflow, -range-overflow, -step-mismatch, -bad-input and -custom, or a single data-iv-error. Without any of them the browser's own message is used. To add your own rules listen to iv:validate and call detail.setError(message); an empty string clears it.
Options
| Option | Attribute | Default | Effect |
|---|---|---|---|
validateOn | data-iv-validate-on | blur | blur validates a field when it loses focus and then on every input while invalid; input validates on every keystroke; submit only on submit. |
summary | data-iv-summary | false | Render an error summary with links to the fields at the top of the form. |
summaryTitle | data-iv-summary-title | Please fix the following | Heading of the summary. |
focusFirst | data-iv-focus-first | true | Move focus to the first invalid field (or the summary) after a failed submit. |
scroll | data-iv-scroll | true | Scroll the focused field into view. |
live | data-iv-live | true | Announce error messages with aria-live. |
Methods and events
validate() returns whether the form is valid and applies every state, validateField(control), reset(), destroy(); read errors. Events: iv:validate on each control (detail.control, message, setError), iv:invalid on the form after a failed submit (detail.errors), and iv:valid (cancelable) right before a valid submit goes through, which is where an asynchronous submission takes over.
Counter
Put data-iv-component="counter" on the field wrapper. The count follows maxlength or data-iv-max; with a soft data-iv-max the reader can keep typing, the counter turns to its "over" state and the control becomes invalid through setCustomValidity, so the form component and the browser both report it.
<div class="iv-stack" style="max-width: 34rem">
<p class="iv-u-text-sm iv-u-text-muted iv-u-m-0">Two demo fields with made-up text. Nothing is submitted.</p>
<div class="iv-field" data-iv-component="counter" data-iv-mode="chars">
<label class="iv-label" for="c-bio">Short bio</label>
<textarea class="iv-textarea" id="c-bio" name="bio" maxlength="280" aria-describedby="c-bio-help" placeholder="A couple of lines about your work">Product designer in Bilbao. I draw interfaces that stay readable at three in the morning, and I like forms that say what is wrong instead of turning red and staying quiet about it. Currently writing about design systems.</textarea>
<p class="iv-field__help iv-u-m-0" id="c-bio-help">Hard limit: the browser stops typing at 280 characters, with or without JavaScript.</p>
</div>
<div class="iv-field" data-iv-component="counter" data-iv-mode="words" data-iv-max="40" data-iv-template="{count} of {max} words · {remaining} left" data-iv-over-text="Trim the pitch to 40 words or fewer.">
<label class="iv-label" for="c-pitch">Pitch</label>
<textarea class="iv-textarea" id="c-pitch" name="pitch" aria-describedby="c-pitch-help">We build small tools for teams that ship on Fridays. This pitch runs past the limit on purpose, so the counter turns red, the control is marked invalid and the form refuses to send it until the text is shorter than the agreed limit.</textarea>
<p class="iv-field__help iv-u-m-0" id="c-pitch-help">Soft limit: you can type past 40 words, and the field is then marked invalid until you trim it.</p>
</div>
</div>// data-iv-component="counter"
{
mode: "chars",
max: 40,
template: "{count} of {max} words · {remaining} left",
overText: "Trim the pitch to 40 words or fewer.",
}| Option | Attribute | Default | Effect |
|---|---|---|---|
mode | data-iv-mode | chars | chars or words. |
max | data-iv-max | maxlength | Limit shown and, when there is no maxlength, enforced as a validation error. |
warnAt | data-iv-warn-at | 0.9 | Fraction of the limit at which the counter changes to its warning state. |
template | data-iv-template | {count} / {max} | Text of the counter; {remaining} is also available. |
overText | data-iv-over-text | Too long | Validation message when a soft limit is exceeded. |
Assistive technology
Invalid controls carry aria-invalid and are described by their message; the summary is a live region and each entry is a link to its field; the counter is announced politely. Under reduced motion the shake on a failed submit is skipped.
Without JavaScript
The browser validates with its own bubbles and maxlength caps the text. Soft limits and word counts need JavaScript; nothing else does.
Common mistakes
- Validating only in JavaScript: keep the HTML attributes, they are the rules.
- A summary without
focusFirst: readers of screen magnifiers may never see it. - Messages that describe the rule instead of the fix: write "Enter a date after today", not "Invalid date".