Validation feedback
Every PhoneInput state carries validationError: the structured outcome of
PhoneNumber.getValidationError() for the digits currently in the field, or null when the
number is complete and valid. It is recomputed on every state change, so a subscriber sees it move
from a partial-number kind to null as the user finishes typing. This guide covers two decisions:
when to surface it, and how to turn each kind into a message. It assumes a PhoneInput attached as
in getting started.
Reading the error per keystroke
Section titled “Reading the error per keystroke”state.validationError is a discriminated union on kind. Read it from any state snapshot:
const error = phoneInput.getState().validationError;
if (error === null) { // The number is complete and valid.} else { // error.kind names the problem; some kinds carry a payload.}The kinds and their payloads:
kind |
Payload | Meaning |
|---|---|---|
EMPTY |
none | No digits and no calling code yet. |
TOO_SHORT |
minLength: number |
Fewer digits than any valid length for the region. |
TOO_LONG |
maxLength: number |
More digits than any valid length for the region. |
INVALID_LENGTH |
possibleLengths: readonly number[] |
A length that falls between valid lengths, not on one. |
NATIONAL_PREFIX_MISSING |
expectedPrefix: string |
The region’s format requires a trunk prefix the digits omit. |
INVALID_CALLING_CODE |
none | The leading digits match no calling code. |
POSSIBLE_LOCAL_ONLY |
none | Valid only for local dialing, e.g. a subscriber number without its area code. |
PATTERN_MISMATCH |
none | A valid length, but the digits match no number pattern. |
EMPTY is the least intuitive: it appears only when no region is resolved at all. In national
mode, and in international mode with a defaultRegion, an empty field already has that region’s
length rules in scope, so an empty value reports TOO_SHORT, not EMPTY. EMPTY reaches you only
in international mode without a defaultRegion, before the typed digits resolve a calling code.
Deciding when to show it
Section titled “Deciding when to show it”Because the error is live, reflecting it directly would flag the field the instant it gains focus
and keep it flagged through every intermediate length. Gate the display on a commit point. The
minimal gate is a touched flag set on the first blur:
// inputElement and errorElement come from your markup.let touched = false;
function render(): void { const error = phoneInput.getState().validationError; const showError = touched && error !== null;
errorElement.textContent = showError ? messageFor(error) : '';}
inputElement.addEventListener('blur', () => { touched = true; render();});
phoneInput.subscribe(render);After the first blur the subscriber keeps the message in sync on every keystroke, so it clears the moment the number becomes valid and reappears if the user empties the field again. Two common variants:
- Show on submit only. Set
touched = truein the submit handler instead of on blur, then callrender(). Nothing appears until the user tries to submit. - Suppress while typing forward, show while correcting. Keep the blur gate, but also clear the
message whenever the value grows, so an in-progress number never shows
TOO_SHORT. Compare the newstate.value.lengthagainst the previous one inside the subscriber.
Whether EMPTY counts as an error is your form’s required rule. For an optional field, treat
EMPTY as no error even when touched is true:
const showError = touched && error !== null && error.kind !== 'EMPTY';Mapping each kind to copy
Section titled “Mapping each kind to copy”Switch on kind and read the payload where one exists. The payload is what lets the message name a
concrete number instead of a generic complaint:
import type { ValidationError } from '@telixon/core';
function messageFor(error: ValidationError): string { switch (error.kind) { case 'EMPTY': return 'Enter a phone number.'; case 'TOO_SHORT': return `The number is too short. It needs at least ${error.minLength} digits.`; case 'TOO_LONG': return `The number is too long. It has at most ${error.maxLength} digits.`; case 'INVALID_LENGTH': return `The number has the wrong number of digits. Valid lengths: ${error.possibleLengths.join(', ')}.`; case 'NATIONAL_PREFIX_MISSING': return `Start the number with ${error.expectedPrefix}.`; case 'INVALID_CALLING_CODE': return 'The calling code is not recognized.'; case 'POSSIBLE_LOCAL_ONLY': return 'This looks like a local number. Add the area code.'; case 'PATTERN_MISMATCH': return 'Check the number. It does not match a valid pattern.'; }}The switch is exhaustive over the union, so adding a case for a new kind is a type error until you
handle it. The same mapping applies outside the DOM: form errors maps
every kind to copy for server-side and framework validation using the same ValidationError type.
The payload numbers are region-specific and come from the resolved region. For defaultRegion: 'US'
a partial number reports { kind: 'TOO_SHORT', minLength: 10 }; a United Arab Emirates mobile typed
without its trunk prefix reports { kind: 'NATIONAL_PREFIX_MISSING', expectedPrefix: '0' }, and the
error clears to null once the 0 is present.
Announcing to assistive technology
Section titled “Announcing to assistive technology”Rendering the message into a live region announces it to screen readers, and reflecting the same
gated boolean into aria-invalid marks the field. Both belong on the commit point, not on every
keystroke. Accessibility covers the live-region markup,
aria-invalid, and aria-describedby wiring that surrounds this timing model.