5.3 KiB
<PinInput> — eidos
Multi-cell OTP / verification code input. A transparent
absolutely-positioned <input> captures every keystroke and paste;
visible bordered cells follow the hidden input's caret. Field-aware
(disabled / readonly / required / invalid OR-merge with the enclosing
Field.Provider).
Usage
<script lang="ts">
import { PinInput } from '$uix/eidos/components/pin-input';
let code = $state('');
</script>
<PinInput
bind:value={code}
length={6}
type="numeric"
size="md"
onComplete={(value) => console.log('filled:', value)}
>
{#snippet children({ cells })}
{#each cells as cell, i}
<PinInput.Cell {cell} index={i} />
{/each}
{/snippet}
</PinInput>
Parts
| Part | Notes |
|---|---|
PinInput (root) |
Provider; container + hidden capture input. Exposes { cells, isFocused, isHovering, value } via children snippet. |
Cell |
One visible cell per character position. Receives cell (PinInputCellData) and index from the snippet. |
Eidos-layer props
| Prop | Type | Default | Notes |
|---|---|---|---|
size |
'sm' | 'md' | 'lg' | 'xl' (responsive) |
'md' |
Cell dimensions + glyph size. |
Headless props (forwarded to soma)
value, onValueChange, onComplete, length, type, pattern,
pasteTransformer, mask, placeholder, disabled, readonly,
required, invalid, name, autocomplete, inputmode,
aria-label. See src/uix/soma/components/pin-input/types.ts.
Cell data-* (emitted by morfo)
| Attr | When |
|---|---|
data-active |
The cell holds the caret (focused + about to be typed in). |
data-filled |
The cell has a character. |
data-disabled |
Cascaded from the provider's disabled state. |
Passive justification
Passive at the morfo level — pin-input's morfo declares parts
(Provider, Cell, Input) but no events array. The user-perceived
interactions (typing, paste, completion) are mediated by the hidden
<input>'s native events; sema-side perceptual feedback piggy-backs
on the commit family base when the consumer hooks onComplete.
The component IS interactive from the user's perspective. "Passive"
here is a contract-layer classification, not a UX one — same logic
as <AlertDialog> (delegates events to the underlying primitive).
Baseline
WAI-ARIA Textbox pattern: https://www.w3.org/WAI/ARIA/apg/patterns/textbox/
The OTP-input UX has no formal ARIA pattern. Soma follows the de-facto behaviour established by Radix-style implementations and the iOS / Android native code-input controls:
- Single hidden
<input>(not one input per cell) so paste, IME and autofill (one-time-code) work naturally. - Paste distributes across cells via the morfo's
pasteTransformerhook. - Backspace clears the previous cell when the current one is empty.
- Numeric
typesetsinputmode='numeric'+autocomplete='one-time-code'so iOS / Android offer the SMS-autofill banner.
Comparativa
| Lib | Single hidden input | Mask | Paste distribute | Type presets | Custom pattern |
Snippet cells |
|---|---|---|---|---|---|---|
| Radix Primitives | n/a (not provided) | — | — | — | — | — |
| Bits UI | ✓ | ✓ | ✓ | numeric/alphanumeric/alphabetic | ✓ | ✓ |
| Ark UI | input per cell | ✓ | ✓ | numeric/alphanumeric | — | — |
| Chakra v3 | input per cell | ✓ | ✓ | numeric/alphanumeric | — | — |
shadcn/ui (input-otp) |
✓ | ✓ | ✓ | numeric (default) | ✓ | ✓ |
| Eidos (this) | ✓ | ✓ | ✓ | numeric / alphanumeric / alphabetic / custom | ✓ | ✓ |
Aligned with the shadcn / Bits family (single hidden input). Ark and Chakra use one input per cell which works but loses paste / IME quality.
Decisiones
- Snippet-driven Cell rendering. Forces the consumer to iterate
cellsand render<PinInput.Cell />per index. Auto-rendering would hide the per-indexindexarg which downstream consumers use for analytics / focus targeting. - Cell consumes
data-activefor the focus ring, not a literal<input:focus>. Only the hidden input is focused; the visible cells receive a synthetic focus ring viadata-activeto match the caret position. - No
variantknob. Pin inputs are a singular visual pattern (bordered boxes); variants like outline-only / soft / ghost noise the contract. Consumers can recolor cells viadata-colorif needed. - Size as t-shirt (
sm/md/lg/xl). Matches the rest of UIX's input scale.
Gaps
| Gap | Disposición | Detalle |
|---|---|---|
Group separator (e.g. 123-456) |
diferir | Soma's pasteTransformer strips dashes; rendering a visible separator between groups (Bits' <PinInput.Group> part) isn't in the morfo. Add when a real consumer needs it. |
| Animated caret transition between cells | diferir | Soma emits data-active instantly; a transitioned bar could be a nice polish. |
data-color cascade |
diferir | Coloured cells (success on complete, danger on invalid) would propagate from the root's data-color. Add when the consumer pattern stabilises. |
Reference
- Bits UI: https://bits-ui.com/docs/components/pin-input
- Ark UI: https://ark-ui.com/docs/components/pin-input
- Chakra v3: https://chakra-ui.com/docs/components/pin-input
- shadcn/ui (
input-otp): https://ui.shadcn.com/docs/components/input-otp