Skip to content

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, with inputmode="numeric" (numeric keyboard on mobile)
  • alphanumeric: letters + digits
  • text: 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()

Focus first emptyFocus cell 3Blur

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: autofocus is 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)

Submit

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

Mark as invalidRestore normal

aria-invalid / success static example

  • loading: pending state — cells disabled, container aria-busy="true", an overlay spinner, all input and keyboard locked
  • success: success state — cells get the --oas-color-success green border (focus ring follows)
  • aria-invalid: error state — synced to the container and all cells with a danger border (in observedAttributes; dynamic setAttribute applies 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:

EventWhendetail
oas-inputevery cell input (realtime){ value, index }
oas-completecode becomes complete (re-fires after edit-back and refill){ value }
oas-changeblur commit: fired when the component loses focus with a value changed since focus{ value }
oas-focus / oas-blurfocus 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-change and oas-complete fired together on completion with identical semantics
  • New: oas-input (per-cell) / oas-complete (filled) / oas-change (blur commit) are orthogonal
  • Migration guide:
    • Legacy oas-change listeners doing realtime validation / input linkage → switch to oas-input
    • Legacy oas-change listeners doing validate-on-complete → switch to oas-complete (add auto-submit if auto submission is desired)
    • Need to confirm the final value on blur (form commit semantics) → keep listening to oas-change

type attribute is now semantic:

  • Legacy: type passed through to the native input type (e.g. tel) and defaulted to text (any character); under type="number" the native input accepted e / E / + / - / .
  • New: type is a semantic tier number (default) / text / alphanumeric, always a native text cell with per-character filtering; invalid values (including legacy native types like tel) fall back to number with a warning
  • Existing usages without an explicit type change from "any character" to "digits only" — set type="text" explicitly if arbitrary characters are needed

API ​

oas-pin-input ​

Attributes ​

AttributeDescriptionTypeDefault
aria-invalidInvalid state (synced to container and cells, marked danger)string—
attachedAttached form: no gaps between cells, shared borders, end-point radii onlyboolean—
auto-submitAuto-submit when full (calls requestSubmit on the closest form; no-op without one)boolean—
autofocusFocus the first empty cell on first connectionboolean—
disabledDisabledboolean—
lengthNumber of code digitsstring6
loadingSubmitting: cells disabled + aria-busy + spinner overlay, keyboard input fully lockedboolean—
maskAsterisk maskingboolean—
otpOTP semantics: inner cells get autocomplete="one-time-code" (triggers system SMS code autofill)boolean—
patternPer-character regex constraint (overrides type filter; invalid regex warns and falls back)string—
placeholderPlaceholder for empty cellsstring—
readonlyReadonlyboolean—
separatorSeparator content (positioned via separator-after)string—
separator-afterInsert a separator after cell N (1-based indexes, comma-separated, e.g. 3,7; defaults to between every adjacent pair)string—
sizeSize preset small / medium (default) / largestringmedium
successSuccess green border (error wins when both set)——
typeCell input typestringnumber
valueCurrent value (controlled)string—
variantVariant: outlined (default) / filled / underlinedstringoutlined

Property (JS property; see each row for attribute reflection) ​

PropertyDescriptionTypeDefault
valueCurrent 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 attributestring—

Events ​

EventDescription
oas-blurFires when focus leaves the component, detail: { index }
oas-changeCommit on blur: dispatched when focus leaves the component and the value differs from the focus-time snapshot, detail: { value }
oas-completeDispatched when filled, detail: { value }
oas-focusFires when focus enters the component (not on cell-to-cell moves), detail: { index }
oas-inputPer-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.

Released under the MIT OR Apache-2.0 License.