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.
A rejected code is the normal case, not the exception. Clear the bound value, then call focus() on the instance (bind:this) and the caret is back in the
first cell — autoFocus alone cannot do this, it runs on mount and whenever it
turns true. The order matters: focus() reads the value, so clear first.
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)}
/>Retry after a rejected code
<script>
import { PinInput } from '@urbicon-ui/blocks';
let code = $state('');
let error = $state('');
let pin;
async function verify(v) {
if (await verifyTwoFactor(v)) return;
error = 'Incorrect code — try again.';
code = '';
pin?.focus();
}
</script>
<PinInput
bind:this={pin}
bind:value={code}
label="Verification code"
helper="Enter the 6-digit code from your authenticator app."
length={6}
{error}
onComplete={verify}
onValueChange={() => (error = '')}
/>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
30 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 | Calls focus() on mount and whenever it turns true — the caret lands on
the first empty cell, and no later edit moves it again. Does nothing while
disabled. For the retry after a rejected code, call focus() on the
instance instead. | |
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 | defaultsuccesswarningdanger | default | Controls the color theme and semantic meaning of the PinInput. Affects the overall appearance and user perception. Available options: default, success, warning, danger. | |
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. | |
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 | smmdlg | md | Controls the dimensions, padding, and text size of the PinInput. Affects the component's physical footprint. Available options: sm, md, lg. | |
slotClasses | Partial<Record<PinInputSlots, string>> | — | Per-slot class overrides merged with tv() styles. Slots: root (what class
also targets) | label | requiredMark | group | cell | separator | message. | |
tier variant | modifycommit | 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: modify, commit. | |
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 | outlinedfilledghost | outlined | Controls the visual style and presentation of the PinInput. Determines the component's visual treatment. Available options: outlined, filled, ghost. | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'class' | 'id' | 'aria-label') | |
...PinInputVariants variant | VariantProps | — | Styling variants from PinInputVariants |
06 Types
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
PinInputProps | interface | props | — | — | |
PinInputVariants | type | variant | — | — |
07 Installation
Import
import { PinInput } from '@urbicon-ui/blocks';