Pin Input
One-character-per-box input for PINs, OTPs, and verification codes with auto-advance, paste support, and optional masking.
<arc-pin-input> Overview
PinInput renders a row of individual input boxes — one per character — designed for entering PINs, one-time passwords, and verification codes. Each box accepts a single character and automatically advances focus to the next box on entry, creating a fast and fluid typing experience. The component supports backspace navigation (moving back to the previous box when the current one is empty), arrow key movement between boxes, and full clipboard paste that fills multiple boxes at once.
The type prop controls character validation: "number" restricts input to digits 0-9, "alphanumeric" allows letters and digits, and "text" accepts any single character. When mask is enabled, entered characters are obscured with dots (using CSS -webkit-text-security: disc) for sensitive codes. An optional separator prop inserts a visual dash between groups of boxes — for example, setting separator="3" on a 6-digit code renders it as three boxes, a dash, and three more boxes.
PinInput fires arc-input on every character entry or deletion, providing the current partial value. When all boxes are filled it fires arc-change — the commit for a fixed-length code — along with arc-complete, the more specific name kept for consumers that auto-submit. Either one makes it easy to trigger form submission or validation at the right moment without polling or length-checking.
Guidelines
When to use
- Set `length` to match the expected code length — 4 for PINs, 6 for OTPs, etc.
- Use `type="number"` for numeric-only codes and set `inputmode="numeric"` for mobile keyboards
- Enable `mask` for sensitive codes like passwords or security PINs
- Listen for `arc-complete` to auto-submit or validate once the full code is entered
- Provide a `label` so users understand what code they are entering
When not to use
- Do not use PinInput for general text entry — it is designed exclusively for fixed-length codes
- Do not set `length` higher than ~8 — long codes are better handled with a standard text input
- Do not omit the `label` prop when the pin input is used standalone without surrounding context
- Do not use `separator` values that produce uneven groups at the end (e.g. `separator="4"` on a 6-digit code)
- Avoid placing PinInput in very narrow containers — each box needs at least 42px width plus gaps
Features
- Auto-advance focus to the next box after each valid character entry
- Backspace navigates to and clears the previous box when the current box is empty
- Arrow key navigation between boxes without modifying content
- Clipboard paste support that fills multiple boxes from the cursor position
- Configurable `type` validation: `"number"`, `"alphanumeric"`, or `"text"`
- Mask mode via `mask` prop for obscuring sensitive codes with dots
- Visual separator dashes between groups via the `separator` prop (e.g. every 3 boxes)
- Split events: `arc-input` on every keystroke, `arc-change` and `arc-complete` when all boxes are filled
Preview
Usage
This component requires JavaScript. No pure HTML/CSS version is available — use the Web Component directly or a framework wrapper.
<arc-pin-input label="OTP Code" length="6" separator="3"></arc-pin-input>
<script>
document.querySelector('arc-pin-input')
.addEventListener('arc-complete', e => {
console.log('Code entered:', e.detail.value);
});
</script> import { PinInput } from '@arclux/arc-ui-react';
export default function Example() {
return (
<PinInput
label="OTP Code"
length={6}
separator={3}
onArcComplete={(e) => console.log('Code:', e.detail.value)}
/>
);
} <script setup>
import { PinInput } from '@arclux/arc-ui-vue';
</script>
<template>
<PinInput
label="OTP Code"
:length="6"
:separator="3"
@arc-complete="(e) => console.log('Code:', e.detail.value)"
/>
</template> <script>
import { PinInput } from '@arclux/arc-ui-svelte';
</script>
<PinInput
label="OTP Code"
length={6}
separator={3}
on:arc-complete={(e) => console.log('Code:', e.detail.value)}
/> import { Component } from '@angular/core';
import { PinInput } from '@arclux/arc-ui-angular';
@Component({
imports: [PinInput],
template: `
<arc-pin-input
label="OTP Code"
[length]="6"
[separator]="3"
(arc-complete)="onComplete($event)"
></arc-pin-input>
`,
})
export class MyComponent {
onComplete(e: CustomEvent) {
console.log('Code:', e.detail.value);
}
} import { PinInput } from '@arclux/arc-ui-solid';
export default function Example() {
return (
<PinInput
label="OTP Code"
length={6}
separator={3}
onArcComplete={(e) => console.log('Code:', e.detail.value)}
/>
);
} import { PinInput } from '@arclux/arc-ui-preact';
export default function Example() {
return (
<PinInput
label="OTP Code"
length={6}
separator={3}
onArcComplete={(e) => console.log('Code:', e.detail.value)}
/>
);
} API
-
valuestring'' - Current combined value across all boxes. Reflected as an attribute.
-
namestring'' -
disabledbooleanfalse - Disables all boxes, reducing opacity to 40% and blocking input.
-
separatornumber0 - Inserts a visual dash separator every N boxes. Set to 0 to disable separators.
-
labelstring'' - Label text displayed above the input boxes in uppercase accent font.
-
lengthnumber4 - Number of input boxes to render. Determines the expected code length.
-
type'number' | 'alphanumeric' | 'text''number' - Character validation mode.
numberallows digits only,alphanumericallows letters and digits,textallows any character. -
maskbooleanfalse - When true, obscures entered characters with dots for sensitive codes.
-
readonlybooleanfalse - Prevents entering, deleting, or pasting characters while the boxes stay focusable and the value still submits.
-
size'sm' | 'md' | 'lg''md' - Control size.
mdis the default;smandlgscale the digit boxes. -
formAssociatedbooleantrue -
propertiesobject{ // flag(), unlike `disabled`. The exclusion in props.js is specifically // about form-associated *platform* semantics: a `disabled` content // attribute that is merely present makes the element actually disabled // per the HTML spec, and formDisabledCallback assigns the property back, // so no converter can win. Neither of these is platform-mapped — // `required` is enforced by _computeValidity() below and `readonly` by // each component's own interaction handlers — so the stock converter buys // nothing here and costs the usual bug: `required="false"` read as true, // blocking submission of a form the author meant to leave optional. // Finding #48's shape, across all 26 form controls at once. required: flag(false), readonly: flag(false), } - Lit merges static properties up the prototype chain, so every consumer
gets these without declaring them.
requiredparticipates in constraint validation below;readonlyreflects for styling and is enforced by each component's interaction handlers (the mixin can't know which gestures mutate state). -
autoValidatesbooleantrue - Components that run their own constraint-validation logic (pattern checks, range checks) opt out of the automatic required sync by overriding this to false, and own the whole validity flag set instead.
-
form -
validity -
validationMessage -
requiredbooleanfalse
Methods
-
checkValidity()boolean - Whether the control currently satisfies its constraints, per the native
constraint-validation API. Fires
invalidon the element when it does not, and reports nothing to the user. -
reportValidity()boolean - As checkValidity(), but also shows the browser's validation message against the control when it fails.
Events
-
arc-input - Fired on every character entry or deletion.
event.detail.valuecontains the current partial value. -
arc-change - Fired when the pin is complete — every box filled. That is the commit for a fixed-length value.
-
arc-complete - Fired alongside arc-change when all boxes are filled. The more specific name, kept for consumers that auto-submit.