Skip to contentAsheeUIAsheeUI

PinInput

A fixed-length code, one box per character, read back as a single value.

A verification code is one value typed in several places. PinInput gives it a box per character, then packs what it collects into one dense string, so a consumer reads value and never has to reason about positions: deleting a character in the middle moves the ones after it left rather than leaving a gap.

It owns typing, focus and paste, and nothing else. The code is a value the consumer verifies; no authentication logic lives here.

Usage

import { PinInput } from "@asheeui/web";

<PinInput
  label="Verification code"
  description="The four digits we sent to your phone."
  onComplete={verify}
/>

The field is controlled when value is given and owns the code otherwise, starting from defaultValue. onValueChange reports every change; onComplete fires with the whole code once it reaches its length, so a screen can verify from either. With a name, the field also renders one hidden input carrying the whole code, which is what lets a plain form post a single field rather than four.

Length, mode and grouping

length and mode are the field's two rules about what it collects. numeric keeps digits, alphanumeric keeps letters and digits, and text keeps anything but whitespace; the filter runs on typing and on paste alike, so dropping a whole code in fills the boxes with the characters the mode accepts and drops the rest.

separatorAfter draws a dash after a given box, which is how a six-digit code reads as two groups of three. It is presentational: the dash is hidden from assistive technology, and it is ignored when it would fall after the last box.

Masking and failure

masked makes each box a password field, for a code that must not be readable over a shoulder. The edge of the boxes and the tone of the message are two separate props: isInvalid colours the boxes and marks them invalid, and status decides the tone of the message below them.

That separation is the point. A code that is being checked is not a code that failed, so a field can report status="default" with a message while leaving the boxes neutral, and can mark a refused code without also claiming the reader typed something wrong in the field's own vocabulary. isDisabled dims the boxes and stops the field accepting input while the label, the description and the message stay readable.

Props

length, mode, size, masked, isDisabled and isInvalid are registered by the component as 4, numeric, md, false, false and false. radius is not registered: it resolves from the application's defaultRadius, so the boxes round with the rest of the form. When the application states no radius, the field's own fallback is sm, tighter than a text field's md, because a box the size of a character should not be a pill.

groupLabel defaults to Verification code. It names the group when the field has no visible label, and it prefixes the name of every box.

Accessibility

  • The boxes are one <fieldset>. With a label, the group is named by aria-labelledby pointing at the visible label; without one it takes groupLabel as aria-label. The component never moves focus by itself, the ref is the group, so a screen that knows why can focus it.
  • Every box names itself with its position, Verification code, character 2 of 4, because four one-character inputs are otherwise announced as four anonymous fields.
  • Typing a character advances to the next box, Backspace clears the box and steps back, Delete clears without moving, and the arrow keys, Home and End move without changing the code. A pasted code fills from the caret onward, and the first box asks the platform for one-time-code autofill.
  • isInvalid marks every box aria-invalid and colours its edge, so a refused code is never signalled by colour alone; status="error" puts the message in a live region.

The label, description and message behave as the field anatomy describes.