PIN Input

A PIN input is a specialized form component that presents a series of single-character input fields for entering Personal Identification Numbers (PINs), verification codes, one-time passwords (OTPs), or similar short numeric codes. Each digit occupies its own input field, creating a segmented entry experience that clearly communicates the expected code length to the user.

The component automatically advances focus to the next field when a digit is entered, supports Backspace to move back to the previous field, and allows Arrow key navigation between fields. The combined value of all digit fields is available as a single bindable string. This pattern is commonly used in two-factor authentication flows, SMS verification, and secure PIN entry interfaces. Each individual input field has an accessible label indicating its position (e.g., "Digit 1 of 4") to support screen reader users.

Example

Show demo markup
<div class="pin-input-div" role="group" aria-label="PIN code"><input type="text" maxlength="1" size="1" aria-label="Digit 1" /> <input type="text" maxlength="1" size="1" aria-label="Digit 2" /> <input type="text" maxlength="1" size="1" aria-label="Digit 3" /> <input type="text" maxlength="1" size="1" aria-label="Digit 4" /></div>
Usage example
<script lang="ts">
  import PinInputDiv from './PinInputDiv.svelte';

  let pin = $state("");
</script>

<PinInputDiv label="Enter PIN" bind:value={pin} />
Show Svelte source
// In your Svelte component:
import PinInputDiv from "lily-design-system-svelte-headless/components/PinInputDiv/PinInputDiv.svelte";

<PinInputDiv>
  <!-- content -->
</PinInputDiv>

Usage

Basic 4-digit PIN input:

<PinInput label="Enter PIN" value={pin} />
<p>Entered PIN: {pin}</p>

6-digit verification code for two-factor authentication:

<PinInput label="Verification Code" length={6} value={code} />

PIN input in a login verification form:

<Form label="Verify your identity" onsubmit={handleVerify}>
  <p>Enter the 6-digit code sent to your phone.</p>
  <Field label="Verification code">
    <PinInput label="Verification code" length={6} value={verificationCode} />
  </Field>
  <Button type="submit">Verify</Button>
  <ActionLink href="/resend">Resend code</ActionLink>
</Form>

Disabled state while verifying:

<PinInput label="Verifying code" length={6} value={code} disabled />
<Loading>Verifying...</Loading>

Props

  • label: string (required) -- accessible label for the group via aria-label
  • length: number (default: 4) -- number of digit inputs
  • value: string (default: "") -- bindable combined string of all digits
  • disabled: boolean (default: false) -- disables all inputs
  • ...restProps: Any additional HTML attributes on the group div

Keyboard Interactions

  • Digit entry: enters value and auto-focuses next input
  • Backspace: clears current or moves to previous input
  • ArrowLeft: focuses previous input
  • ArrowRight: focuses next input
  • Tab: standard tab navigation in/out of the group

ARIA

  • role="group" with aria-label on the container
  • Each input has aria-label="Digit X of Y" for positional context
  • inputmode="numeric" for numeric keyboard on mobile

When to Use

  • Use for two-factor authentication, SMS verification codes, and OTP entry flows.
  • Use when you need a segmented input that clearly communicates the expected code length.
  • Use for MFA flows where the user enters a time-based code from an authenticator app.
  • Use for account verification during sign-up or password reset.

When Not to Use

  • Do not use for passwords -- use PasswordInput instead.
  • Do not use for general numeric entry -- use NumberInput instead.
  • Do not use for reference numbers or codes longer than 8 digits -- use TextInput with inputmode="numeric" instead.

Headless

The PinInput headless component provides a <div role="group"> containing individual <input> fields with inputmode="numeric", maxlength="1", per-digit aria-label attributes, and auto-focus navigation. The consumer provides all visual styling for the digit fields, spacing, and container layout.

Styles

The consumer provides all CSS styling. The component renders with a .pin-input-div class for targeting. No default styles are included — this is a fully headless component.

Testing

  • Verify the component renders a <div> element with class pin-input-div
  • Verify role="group" with aria-label` on the container
  • Verify Each input has aria-label="Digit X of Y" for positional context
  • Verify keyboard interactions work correctly
  • Verify pass-through attributes are applied

Advice

  • Designers: Space digit fields evenly and make each field large enough for easy touch input; consider visual separators between groups (e.g., after the third digit in a 6-digit code).
  • Developers: Handle paste events to distribute a pasted code across all digit fields, and auto-submit the form when all digits are entered for a smoother user experience.

Implementation Notes

  • Renders as <div role="group"> containing individual single-digit <input> elements
  • Uses reactive state for internal digits array and two-way binding for combined value
  • Auto-focuses next input on digit entry
  • Backspace in empty input moves focus to previous input
  • ArrowLeft/ArrowRight navigate between inputs
  • Only accepts numeric characters (0-9)
  • inputmode="numeric" ensures numeric keyboard on mobile devices
  • maxlength="1" restricts each input to a single character

Related components

  • password-input — an input for entering a password with obscured text <input type="password">
  • input-with-mask — an input displaying a format mask placeholder for the user to fill in

References

  • WAI-ARIA Group Role: https://www.w3.org/TR/wai-aria-1.2/#group

Canonical contract

Generated from this component's canonical metadata — the machine-checked source the implementations are held to.

Metadata

  • Component: pin-input-div
  • PascalCase: PinInputDiv
  • Description: a series of single-digit inputs for entering a PIN or OTP code
  • Status: beta — implemented and unit-tested in all seven frameworks with an axe-clean demo page; not yet exercised in composed flows
  • HTML tag: <div>
  • CSS class: .pin-input-div
  • Interactive: yes

ARIA

  • role="group" with aria-label on the container
  • Each input's aria-label comes from the optional digitLabel(position, length) (e.g. "Digit 1 of 4"); without it each input is named by label — no English default
  • inputmode="numeric" for numeric keyboard on mobile

Keyboard

  • Digit entry: enters value and auto-focuses next input
  • Backspace: clears current or moves to previous input
  • ArrowLeft: focuses previous input
  • ArrowRight: focuses next input
  • Tab: standard tab navigation in/out of the group

Props

  • label: string (required) -- accessible label for the group via aria-label
  • length: number (default: 4) -- number of digit inputs
  • digitLabel: (position: number, length: number) => string (optional) -- each input's accessible name; defaults to label
  • value: string (default: "") -- bindable combined string of all digits
  • disabled: boolean (default: false) -- disables all inputs
  • ...restProps: Any additional HTML attributes on the group div