PinInput
A row of single-character cells for entering a short code like a 2FA one-time code or a PIN, with auto-advance, paste-to-fill, and optional masking.
Playground
<PinInput />01 Examples
Six-digit OTP with a completion callback
<script>
import { PinInput } from '@urbicon-ui/blocks';
let code = $state('');
let status = $state('');
</script>
<PinInput
label="One-time code"
length={6}
bind:value={code}
onComplete={(v) => (status = 'Verifying ' + v + '…')}
/>
{#if status}
<p>{status}</p>
{/if}Alphanumeric with a grouped separator
<script>
import { PinInput } from '@urbicon-ui/blocks';
let licenseKey = $state('');
</script>
<PinInput
label="License key"
length={8}
size="sm"
type="alphanumeric"
uppercase
separator="-"
groupSize={4}
bind:value={licenseKey}
/>Error state
<PinInput label="Security code" length={6} value="12" error="Incorrect code" />02 Two-factor / OTP
PinInput fits the one-time-code step of a two-factor flow. The first cell carries autocomplete="one-time-code", so iOS offers the code from an incoming SMS as a
keyboard suggestion. Send the bound value from onComplete to your verify endpoint
— with the auth package, that is createTwoFactorHandlers's verify group behind POST /api/auth/2fa/verify.
An autofilled code lands in the first cell and is distributed across the row, exactly like a
paste. Give it a visible label and a helper line so the source of the
code (authenticator app vs. SMS) is never ambiguous.
Authenticator verification field
<PinInput
label="Verification code"
helper="Enter the 6-digit code from your authenticator app."
length={6}
bind:value={code}
onComplete={(v) => verifyTwoFactor(v)}
/>03 Customization
Every visible part is a named slot: root (what class also targets), label, group (the cell row), cell, separator, and message. Pass slotClasses to merge
classes onto any of them, or unstyled to drop every default class and rebuild
from scratch. For a look you reuse across the app, register a preset once on <BlocksProvider> and reference it by name instead of repeating overrides.
Terminal-style cells via slotClasses
<PinInput
label="Access code"
length={6}
value="4711"
slotClasses={{
group: 'gap-3',
cell: 'rounded-lg bg-surface-subtle border-border-default font-mono text-2xl text-primary'
}}
/>04 Accessibility
Group semantics
The cell row is a role="group", named by aria-labelledby when a
visible label is set, or by aria-label otherwise.
Each cell announces its position
Each cell carries aria-label="Character N of M", so a screen-reader user always
knows where the caret sits.
The error reaches every cell
aria-describedby is set on each cell individually, not just on the group — so the
message is read wherever the caret sits, not only on entering the field.
Keyboard
Typing a valid character auto-advances to the next cell. Backspace clears the
current cell, or — when that one is already empty — steps back and clears the previous one; Delete clears without moving. The arrow keys plus Home / End move between cells, and a paste is distributed across the cells from the caret.
05 API Reference
31 propsProp | Type | Default | Description | |
|---|---|---|---|---|
aria-label | string | — | Accessible name for the cell group when no visible label is set. Each
cell additionally announces its position ("Character 2 of 6"). | |
autoFocus | boolean | false | Focus the first empty cell on mount. | |
class | string | — | Extra classes merged onto the root element. | |
disabled | boolean | false | Blocks input and dims the whole row. | |
error | string | — | Error message below the cells. When set it overrides helper, colours the
cells danger, and sets aria-invalid on every cell. | |
groupSize | number | 3 | Cells per group when separator is set. | |
helper | string | — | Helper text below the cells — hidden when error is present. | |
id | string | — | Root id; the cells derive their ids and ARIA wiring from it. | |
intent variant | dangerdefaultsuccesswarning | default | Controls the color theme and semantic meaning of the PinInput. Affects the overall appearance and user perception. Available options: danger, default, success, warning. | |
label | string | — | Group label rendered above the cells and linked via aria-labelledby. | |
length | number | 6 | Number of cells. | |
mask | boolean | false | Render each filled cell as a masked dot (password style). For a standing PIN or passcode, not for a throwaway SMS code — masking one buys no secrecy and costs the user the ability to check what they typed. | |
messageType variant | errorhelper | helper | Controls the messageType behavior and appearance of the PinInput component. Available options: error, helper. | |
name | string | — | Shared name for a hidden input, for native form submission. | |
onComplete | (value: string) => void | — | Fires each time the row becomes complete, with the full value — so correcting a rejected code fires it again. Not once per mount. | |
onValueChange | (value: string) => void | — | Fires after any change (typing, paste, backspace) with the full value. | |
placeholder | string | '' | Placeholder character shown in every empty cell. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ PinInput: {...} }}>. | |
readonly | boolean | false | Shows the value but refuses edits; the cells stay focusable. | |
required | boolean | false | Marks the label with an asterisk and sets aria-required on the cells. It does
not block a native submit — the value lives in component state, so validate it
yourself before you act on it. | |
separator | string | — | Render a separator between groups of groupSize cells (e.g. 123-456).
A string is shown verbatim; omit for no separator. | |
size variant | lgmdsm | md | Controls the dimensions, padding, and text size of the PinInput. Affects the component's physical footprint. Available options: lg, md, sm. | |
slotClasses | Partial<Record<PinInputSlots, string>> | — | Per-slot class overrides merged with tv() styles. Slots: root (what class
also targets) | label | group | cell | separator | message. | |
tier variant | commitmodify | modify | Selects the semantic radius tier of the PinInput — the shape family it belongs to (--radius-commit/-modify/-contain/-bridge). Shape is retuned per family in your theme, so this picks the family rather than a pixel value. Available options: commit, modify. | |
type | numericalphanumeric | 'numeric' | Allowed characters and keyboard hint. numeric accepts 0-9 and sets a
numeric inputmode; alphanumeric also accepts A-Z/a-z. | |
unstyled | boolean | — | Remove all default tv() classes — only user-provided classes apply. | |
uppercase | boolean | false | Uppercase alphanumeric input as it is entered — keeps a code like ABCD
visually consistent regardless of caps lock. Ignored for numeric. | |
value | string | — | Current value — the concatenated cell characters. Supports bind:value. | |
variant variant | filledghostoutlined | outlined | Controls the visual style and presentation of the PinInput. Determines the component's visual treatment. Available options: filled, ghost, outlined. | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'class' | 'id' | 'aria-label') | |
...PinInputVariants variant | VariantProps | — | Styling variants from PinInputVariants |
06 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
PinInputProps | interface | props | 0 | — | |
PinInputSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. | |
PinInputVariants | type | variant | 0 | — | |
SlotNames | type | helper | 0 | Extracts the slot-name union from a slotted tv() config function — the
companion to VariantProps. The slot-mode overload returns
(props?) => { [K in keyof S]: SlotFn }, so keyof ReturnType<T> is exactly
the set of slot names a component declares in tv({ slots: … }).
Use it to type a component's slotClasses prop from the single source of
truth (its *.variants.ts) instead of hand-maintaining a parallel union
that silently drifts when a slot is added or renamed: | |
VariantProps | type | helper | 1 | — |
07 Installation
Import
import { PinInput } from '@urbicon-ui/blocks';