<pin-input>
A framework-agnostic, accessible PIN input. Fully customizable via
::part(). Works anywhere HTML works.
npm install @javierortega95/pin-input
Import the component and use it with any framework — no wrappers, no adapters needed.
import '@javierortega95/pin-input' <pin-input length="6" name="otp" autocomplete="one-time-code" ></pin-input> const input = document.querySelector('pin-input') input.addEventListener('pin-change', (e) => { console.log(e.detail.value) }) input.addEventListener('pin-complete', (e) => { console.log(e.detail.value) })
import '@javierortega95/pin-input' export default function App() { return ( <pin-input length="6" name="otp" autocomplete="one-time-code" onpin-change={(e) => console.log(e.detail.value)} onpin-complete={(e) => console.log(e.detail.value)} /> ) }
Requires React 19. For React 18 and earlier, use
useRef + addEventListener.
<script setup> import '@javierortega95/pin-input' function onPinChange(e) { console.log('change:', e.detail.value) } function onPinComplete(e) { console.log('complete:', e.detail.value) } </script> <template> <pin-input length="6" name="otp" autocomplete="one-time-code" @pin-change="onPinChange" @pin-complete="onPinComplete" /> </template>
import '@javierortega95/pin-input' import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core' import { bootstrapApplication } from '@angular/platform-browser' @Component({ selector: 'app-root', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <pin-input length="6" name="otp" autocomplete="one-time-code" (pin-change)="onPinChange($event)" (pin-complete)="onPinComplete($event)" ></pin-input> `, }) export class App { onPinChange(e: Event) { console.log('change:', (e as CustomEvent).detail.value) } onPinComplete(e: Event) { console.log('complete:', (e as CustomEvent).detail.value) } } bootstrapApplication(App)
Unstyled by default. Use
::part()
to apply any CSS — no specificity battles, no overrides.
pin-input::part(wrapper) { display: flex; gap: 8px; } pin-input::part(slot) { width: 48px; height: 56px; border: 1.5px solid #d0d7de; border-radius: 8px; display: flex; align-items: center; justify-content: center; } pin-input::part(slot filled) { border-color: #94a3b8; } pin-input::part(slot active) { border-color: #58a6ff; } pin-input::part(cursor) { width: 1.5px; height: 22px; background: #58a6ff; }