Components · Identity
UsernameAvailabilityInput
Input with an injected, debounced async check, a status icon and an announced result.
Workspace address
Try “team” to see a taken address.
Submit enabled: no
import { useState } from 'react';
import { Field, FieldDescription, FieldLabel, UsernameAvailabilityInput, type AsyncValidationStatus, type AsyncValidator } from '@rookcom/monarch-react';
const TAKEN = new Set(['admin', 'help', 'support', 'team']);
const demoCheck: AsyncValidator = (value, signal) => {
if (value.length < 3) return { valid: false, message: 'Use at least 3 characters.' };
if (!/^[a-z0-9-]+$/.test(value))
return { valid: false, message: 'Use lowercase letters, numbers and hyphens.' };
return new Promise((resolve, reject) => {
const t = setTimeout(
() =>
resolve(
TAKEN.has(value) ? { valid: false, message: 'That address is taken.' } : { valid: true },
),
600,
);
signal.addEventListener('abort', () => {
clearTimeout(t);
reject(new DOMException('Aborted', 'AbortError'));
});
});
};
const slugify = (raw: string) =>
raw
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '');
function AddressDemo() {
const [status, setStatus] = useState<AsyncValidationStatus>('idle');
return (
<div style={{ width: '100%', maxWidth: 300 }}>
<Field required>
<FieldLabel>Workspace address</FieldLabel>
<UsernameAvailabilityInput
prefix="example.com/"
placeholder="your-team"
validate={demoCheck}
transform={slugify}
onStatusChange={(s) => setStatus(s)}
/>
<FieldDescription>Try “team” to see a taken address.</FieldDescription>
</Field>
<p style={{ margin: '8px 0 0', fontSize: 13 }}>
Submit enabled: {status === 'valid' ? 'yes' : 'no'}
</p>
</div>
);
}
<AddressDemo />Usage examples
Settled states
import { UsernameAvailabilityInput } from '@rookcom/monarch-react';
<div style={{ display: 'grid', gap: 12, width: '100%', maxWidth: 300 }}>
<UsernameAvailabilityInput
aria-label="Available handle"
defaultValue="maya-lee"
validate={() => ({ valid: true })}
debounce={0}
/>
<UsernameAvailabilityInput
aria-label="Taken handle"
defaultValue="admin"
validate={() => ({ valid: false, message: 'That handle is taken.' })}
debounce={0}
/>
</div>Purpose
A text input whose value is checked while the user types, such as a handle, workspace address or slug. The product supplies the check; the component debounces it, drops stale answers, shows the status and announces the result.
Use when
- A value must be unique or otherwise approved by a server before the form can be sent.
- The user benefits from knowing the answer before submitting (handles, addresses, slugs).
Avoid when
- Validation is purely local: use Input with FieldError.
- The check is expensive or rate-limited per keystroke: validate on submit instead.
Design principles
- Generic by design: the product passes
validate, so restricted names and lookups stay in the product. - Status is never colour alone: every state has an icon and a text message.
- Only settled results are announced; "checking" is shown, not spoken on every pause.
- The latest value always wins: earlier checks are aborted and their late answers ignored.
API reference
import { UsernameAvailabilityInput, useAsyncValidation } from '@rookcom/monarch-react';| Export | Kind | Rendering | Root class |
|---|---|---|---|
UsernameAvailabilityInput | component | client | .mn-username-availability-input |
useAsyncValidation | hook | client | – |
useAsyncValidation(value, validate, { debounce }) is the same logic as a hook, returning { status, message, revalidate }, for products that need it on another control.
The ref and native input props go to the <input>; className goes to the .mn-username-availability-input wrapper.
Generated from the TypeScript declarations of packages/react/src/components/identity/username-availability-input.
Properties
validate(value, signal) may return a result synchronously or as a promise. Return null for "nothing to check" (for example the user’s current handle); throw or reject for "could not check".
UsernameAvailabilityInput
A text input whose value is checked asynchronously while the user types: handles, workspace addresses, slugs. The product supplies the check; the component debounces it, aborts stale requests, shows a status icon with a text message, and announces the result to screen readers.
| Prop | Type | Description |
|---|---|---|
validate required | AsyncValidator | The product's check: local rules and the availability lookup. Required. |
children | ReactNode | |
className | string | Class for the outer wrapper (the input itself is `.mn-username-availability-input__input`). |
debounce | number | Milliseconds without typing before `validate` runs. Default 400. |
defaultValue | string | Initial text when uncontrolled. |
disabled | boolean | |
id | string | |
invalid | boolean | Marks the value invalid regardless of the check. Inside a Field, the Field's `invalid` is used. |
messages | Partial<UsernameAvailabilityInputMessages> | Override the default status texts. |
onStatusChange | ((status: AsyncValidationStatus, message: string | undefined) => void) | Called whenever the status changes, so a form can block submit until `valid`. |
onValueChange | ((value: string) => void) | Called with the new text (after `transform`) on every change. |
prefix | ReactNode | Fixed text before the value, such as a domain ("example.com/"). Read as part of the description. |
size | "sm" | "md" | "lg" | |
transform | ((raw: string) => string) | Rewrites typed text before it is stored, e.g. lower-casing or slugifying. |
value | string | The text (controlled). |
Variants
| Prop | Values | Description |
|---|---|---|
size | 'sm''md''lg' | Control height from --mn-control-*, as for Input. |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Idle | data-status="idle" | Empty, or the validator returned null. No icon or message. |
| Checking | data-status="checking" | Spinner and "Checking availability…" from the first keystroke until the answer arrives. |
| Valid | data-status="valid" | Check icon, success border and message (default "Available"). |
| Invalid | data-status="invalid", aria-invalid="true" | Warning icon, danger border and the validator’s message (default "Not available"). |
| Error | data-status="error" | The check failed: an info icon, a message and a "Try again" button that re-runs it at once. |
Interaction behaviour
- Each change applies
transform, callsonValueChange, then runsvalidateafterdebouncems without typing (default 400). - A new value aborts the previous check through its AbortSignal; a result that arrives late is ignored.
onStatusChange(status, message)fires on every status change, so the form can disable submit untilvalid.- Controlled with
value+onValueChange, or uncontrolled withdefaultValue.
Keyboard support
| Keys | Action |
|---|---|
| Tab | Moves to the input, then to "Try again" when a check has failed. |
Typing and editing are the native input’s own; the component adds no key handling.
Accessibility
- Role
- textbox
- Accessible name
- A FieldLabel, or
aria-label.
- The prefix and the status message are linked with
aria-describedby, so the full address and the result are read with the field. - Settled results are announced through a polite
role="status"region that is always present. aria-invalidis set while the value is unavailable or the Field is invalid.- Icons are
aria-hidden; the text carries the meaning. - Autocorrect, autocapitalise and spellcheck are off, since handles are not words.
Responsive behaviour
- Fills its container. A long prefix is truncated with an ellipsis at half the width, leaving room to type.
- The message wraps under the field at narrow widths.
Theme support
- Status colours are semantic tokens (
--mn-color-success-*,--mn-color-danger-*,--mn-color-warning-strong), so light, dark and more-contrast themes apply. - The spinner slows under
prefers-reduced-motion(Spinner’s own rule).
Do and don't
Do
- Pass the AbortSignal to
fetchso abandoned checks stop. - Return
nullfor the user’s current value, so editing a profile does not report their own handle as taken. - Keep submit disabled until the status is
valid, and re-check on the server when the form is sent.
Don't
- Do not put restricted-name lists or uniqueness rules in Monarch; they belong in
validate. - Do not set
debounceto 0 for a server check; it sends a request per keystroke.
Related components
Dependencies
Uses the .mn-input styles from text-field for the control.
Testing coverage
- Debounces the check, shows "checking" without announcing it, then announces and describes the result.
- Marks unavailable values invalid with the default or the validator’s message.
- Aborts stale checks; a late answer never overwrites the latest one.
- A failed check shows an error and "Try again" re-runs it.
- Empty values and
nullresults are idle;onStatusChangereports each change. transformrewrites the value in controlled mode.- The prefix and Field description are part of the accessible description.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery; Cross-browser axe: /components/username-availability-input |
Tests: packages/react/src/components/identity/username-availability-input