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