PinInput
A segmented code input with per-character constraint filtering (type / pattern), separator grouping, size and variant tiers, OTP SMS autofill, submit-on-complete, and loading / success validation feedback; plus keyboard arrow navigation, Backspace fallback, and paste auto-distribution (with filtering).
Basic Usage
Default 6 digits
Defaults to type="number" (digits only), which covers most verification code scenarios.
Custom Length
length=4
Set the number of cells via length.
Character Constraint (semantic type)
number (default) / alphanumeric / text
type is a semantic character-constraint tier (no longer passed through to the native input type):
number(default): digits[0-9]only, withinputmode="numeric"(numeric keyboard on mobile)alphanumeric: letters + digitstext: any character
Internally the cells are always native text inputs with per-character filtering — the legacy type="number" passthrough accepted e / E / + / - / . (legal syntax of native number inputs); that defect is now fixed. The constraint applies consistently to keystrokes, paste distribution, and the controlled value prefill; invalid characters are silently rejected (the common convention). With mask set, the cells switch to password masking. An invalid type falls back to number with a console.warn (deduplicated per value).
Custom Regex (pattern)
pattern=[0-3] (digits 0–3 only)
pattern accepts a regex string matched per character (e.g. [0-3] restricts to 0–3, [a-m] to the first half of the alphabet); when set it overrides the built-in charset of type (inputmode still follows type). An invalid regex string warns and falls back to the type constraint.
Separators
3+4 phone code grouping
Between every pair (separator-after omitted)
separator is the separator content (string); separator-after is a comma-separated list of 1-based cell indices ("3" = after the 3rd cell, "2,5" = after the 2nd and 5th); when omitted, separators are inserted between every adjacent pair. The separator element exposes part="separator" for styling, is aria-hidden to screen readers, and never participates in the value.
Sizes
small / medium / large
size has three tiers aligned with the global control-height tokens (small=24px / medium=32px default / large=40px); cell width and font scale along. Invalid values fall back to medium with a warning.
Placeholder
placeholder on every cell
placeholder is shown in each empty cell (a single character like ○ by convention).
Variants & Attached
outlined (default) / filled / underlined
attached (shared borders)
variant tiers: outlined (default) / filled / underlined; attached removes the gaps and merges adjacent borders (rounded corners only at the ends, the focused cell paints above its neighbors). An invalid variant falls back to outlined with a warning. All styles use logical properties and mirror automatically under dir="rtl".
Mask
mask
mask switches the cells to password type (native platform dots), masking the input values.
OTP SMS Autofill
otp
The otp boolean sets autocomplete="one-time-code" on the cells, so mobile platforms (iOS / Android) show the SMS code suggestion above the keyboard and fill it on tap.
Autofocus & Focus Methods
autofocus + focus(index) / blur()
Setting the autofocus attribute focuses the first empty cell on first connect; focus(index?) focuses a given cell (defaulting to the first empty cell, or the last cell when full; out-of-range falls back to the default strategy), and blur() blurs the active cell. Component-level enter/leave dispatches oas-focus / oas-blur (carrying the cell index); moving between cells does not re-dispatch.
Vue note:
autofocusis a reflected HTMLElement property — a bare attribute gets coerced away by Vue's property assignment, so use the:autofocus="true"binding form (it takes effect via attribute reflection).
Submit on Complete (auto-submit)
auto-submit (explicit opt-in)
The auto-submit boolean automatically calls requestSubmit() on the associated form (nearest form ancestor) when the code becomes complete — it fires the submit event and native constraint validation (no bypass), and is a safe no-op without a form. The "verify as soon as it's full" OTP flow needs no intermediate button.
Validation Feedback (loading / success / aria-invalid)
Type the correct code 1234 for the full flow: complete → loading (cells locked + spinner) → success; any other code → error for 1.6s, then auto-cleared for retry.
loading → success / error
aria-invalid manual toggle
aria-invalid / success static example
loading: pending state — cells disabled, containeraria-busy="true", an overlay spinner, all input and keyboard lockedsuccess: success state — cells get the--oas-color-successgreen border (focus ring follows)aria-invalid: error state — synced to the container and all cells with a danger border (in observedAttributes; dynamicsetAttributeapplies immediately); error takes precedence over success when both are set
Controlled Initial Value
value prefill
value is distributed to each cell; the portion beyond length is truncated, and characters violating the current type / pattern constraint are filtered at the display layer. Internal input writes back to the value attribute (uncontrolled channel); external changes sync immediately without rebuilding cell references.
Disabled / Readonly
disabled
readonly
Events
Try in order: click into a cell (oas-focus) → type digits (oas-input) → fill up (oas-complete) → click the page blank to blur (oas-change + oas-blur).
input / complete / change / focus / blur
The three value events are orthogonal, with no overlap:
| Event | When | detail |
|---|---|---|
oas-input | every cell input (realtime) | { value, index } |
oas-complete | code becomes complete (re-fires after edit-back and refill) | { value } |
oas-change | blur commit: fired when the component loses focus with a value changed since focus | { value } |
oas-focus / oas-blur | focus enters / leaves the component (cell moves don't dispatch) | { index } |
RTL
dir=rtl
Under dir="rtl" the cells lay out right-to-left, arrow keys reverse (ArrowLeft forward, ArrowRight back), and separator positions mirror with the visual order.
Migration Notes (Breaking Changes)
oas-change semantics changed (aligned with the form-family change convention; no longer duplicated with oas-complete):
- Legacy:
oas-changeandoas-completefired together on completion with identical semantics - New:
oas-input(per-cell) /oas-complete(filled) /oas-change(blur commit) are orthogonal - Migration guide:
- Legacy
oas-changelisteners doing realtime validation / input linkage → switch tooas-input - Legacy
oas-changelisteners doing validate-on-complete → switch tooas-complete(addauto-submitif auto submission is desired) - Need to confirm the final value on blur (form commit semantics) → keep listening to
oas-change
- Legacy
type attribute is now semantic:
- Legacy:
typepassed through to the native input type (e.g.tel) and defaulted totext(any character); undertype="number"the native input acceptede/E/+/-/. - New:
typeis a semantic tiernumber(default) /text/alphanumeric, always a nativetextcell with per-character filtering; invalid values (including legacy native types liketel) fall back tonumberwith a warning - Existing usages without an explicit
typechange from "any character" to "digits only" — settype="text"explicitly if arbitrary characters are needed
API
oas-pin-input
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
aria-invalid | Invalid state (synced to container and cells, marked danger) | string | — |
attached | Attached form: no gaps between cells, shared borders, end-point radii only | boolean | — |
auto-submit | Auto-submit when full (calls requestSubmit on the closest form; no-op without one) | boolean | — |
autofocus | Focus the first empty cell on first connection | boolean | — |
disabled | Disabled | boolean | — |
length | Number of code digits | string | 6 |
loading | Submitting: cells disabled + aria-busy + spinner overlay, keyboard input fully locked | boolean | — |
mask | Asterisk masking | boolean | — |
otp | OTP semantics: inner cells get autocomplete="one-time-code" (triggers system SMS code autofill) | boolean | — |
pattern | Per-character regex constraint (overrides type filter; invalid regex warns and falls back) | string | — |
placeholder | Placeholder for empty cells | string | — |
readonly | Readonly | boolean | — |
separator | Separator content (positioned via separator-after) | string | — |
separator-after | Insert a separator after cell N (1-based indexes, comma-separated, e.g. 3,7; defaults to between every adjacent pair) | string | — |
size | Size preset small / medium (default) / large | string | medium |
success | Success green border (error wins when both set) | — | — |
type | Cell input type | string | number |
value | Current value (controlled) | string | — |
variant | Variant: outlined (default) / filled / underlined | string | outlined |
Property (JS property; see each row for attribute reflection)
| Property | Description | Type | Default |
|---|---|---|---|
value | Current value: concatenated string of all cell characters; the property and the value attribute share one channel (the setter writes back the attribute and attribute changes sync to all cells), so it is reflected as an attribute | string | — |
Events
| Event | Description |
|---|---|
oas-blur | Fires when focus leaves the component, detail: { index } |
oas-change | Commit on blur: dispatched when focus leaves the component and the value differs from the focus-time snapshot, detail: { value } |
oas-complete | Dispatched when filled, detail: { value } |
oas-focus | Fires when focus enters the component (not on cell-to-cell moves), detail: { index } |
oas-input | Per-cell input, detail: { value, index } |
Keyboard: ←/→ to move between cells, Backspace deletes the current cell and moves back, paste auto-distribution is supported; when all cells are empty, each cell is focusable (native caret).
ARIA: the container has role="group" + aria-label, each cell has aria-label="Digit {position}", and aria-invalid is synced to both the container and all cells.