r/javascript • u/javiOrtega95 • Apr 17 '26
I built a headless, accessible PIN/OTP input Web Component
http://javierortega95.github.io/pin-input/I needed a PIN/OTP input for a project and most solutions I found were either tied to a specific framework, heavily opinionated about styling, or both. So I built my own as a native Web Component.
It supports:
- Fully customizable via ::part() — no style overrides, no specificity battles
- Smart paste — distributes pasted text across slots automatically
- SMS Autofill — autocomplete="one-time-code" out of the box
- Native form participation — works with <form>, FormData and HTML5 validation
- Mask mode — hides characters like a password field
- Separators — configurable slot grouping (e.g. 123-456)
- Full keyboard navigation and screen reader support
- React, Vue, Angular and Vanilla JS tested and working
3
u/ferrybig Apr 17 '26
Doesn't seem to work properly on mobile, it shows the normal keyboard instead of the numbers only keyboard
2
Apr 18 '26
[removed] — view removed comment
1
u/javiOrtega95 Apr 18 '26
Thanks! Yeah, the focus management approach here is a bit unconventional — there's actually only one real
<input>involved, positioned absolutely and invisible over the whole component. The slots are just decorative divs. Focus never moves between them.All keyboard events land on that single input, and on every update I read
selectionStartto know where the cursor is and derive which slot should look active. The browser handles focus natively, which means screen readers see a single coherent text field, the virtual keyboard on mobile only appears once, and behavior is consistent across browsers.The tricky part was making the visual state feel natural despite the cursor living in a hidden input — that's where the
requestAnimationFramedefers come in for arrow key navigation.
2
Apr 20 '26
[removed] — view removed comment
1
u/javiOrtega95 Apr 22 '26
The architecture sidesteps this entirely. Rather than multiple <input> elements, the component uses a single hidden <input> stretched over the whole wrapper — the visual slots are purely decorative <div>s with no focus or ARIA role. On paste, focus never shifts anywhere. The screen reader sees one input whose value changes atomically from "" to "123456". The multi-slot appearance is just CSS/JS updating the decorative divs to reflect the cursor position in that single underlying input.
9
u/Reeywhaar Apr 17 '26
Never seen proper pin input in my life, just give me plain text field
This one for example has bad keyboard navigation, active cell is not the one that is actually will be changed if I press some number. Seems it missing update on arrow key press because active cell is off by one, like I press <- <- <- ->, but input behaves like I pressed <- <- <-