GrOtpInput
A field for a code of known length: an SMS confirmation, a one-time password, a PIN.
Overview
When to take it
- the code comes from outside — from an SMS, an email or an app: the length is known in advance, and the field shows it;
- the check runs as soon as it is filled in —
completefires without a “Send” button; - a PIN on a lock screen —
maskedhides the characters, the behaviour stays the same; - the code is read aloud or copied by eye —
groupssplits it into parts, like a card number.
When to take something else
| Need | Take |
|---|---|
| A string of arbitrary length | GrInput |
| A password that is typed, not received | GrInput with type="password" |
| A number with steps and bounds | GrNumberInput |
| The user makes up the values themselves | GrInputTag |
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrOtpInput v-model="value" />Examples 3 each with a live demo and its code
A sign-in code from a text message
groupssplits the code the way the message prints it, andcompletechecks it with no «Submit» button.one-time-codelets the phone offer the code from the message. A refused code clears the cells throughclear()and keeps the focus, so the next attempt starts at the first cell.Messages · nowNorthwind: 482 913 is your sign-in code. Do not share it.Check your phoneWe sent a code to •• 4821.The code works for 10 minutes.A PIN for a new card
maskedshows dots where a second pair of eyes is likely, while the model keeps the digits.invalidmarks the whole repeated PIN — one attempt, not four wrong cells.Set a PIN for your new cardVisa Business •••• 7720 New PINRepeat the PINA TV pairing code
A code read off a screen avoids look-alikes:
patterntakes one character, so O, 0, I and 1 never reach the model, and a pasted «k7pm-w2qx» keeps only what fits. Thecellslot shows letters in capitals as the TV prints them, andseparatorputs a dot between the groups.Connect a TVType the code shown on the TV screen, e.g. K7PM·W2QX.
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
disabled | boolean | undefined | false | Disable the field: the code cannot be entered and is not submitted with the form. It adds up with `GrFormField`. |
length | number | undefined | undefined | The number of cells. |
type | "text" | "numeric" | "alphanumeric" | undefined | "numeric" | The allowed alphabet and the layout of the mobile keyboard. |
modelValuev-model | string | undefined | "" | The code as a whole, without separators. |
readonly | boolean | undefined | false | Read-only: the code is visible but cannot be changed. |
invalid | boolean | undefined | false | An error — for example, the server rejected the code: the border of the cells and `aria-invalid`. It adds up with `GrFormField`. |
required | boolean | undefined | false | A required field (`aria-required`). It adds to the `required` of `GrFormField`. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The size of the cells and the type size of the characters. Unset — from `GrConfigProvider`, otherwise `md`. |
placeholder | string | undefined | "" | The sign in an empty cell. |
ariaLabel | string | undefined | undefined | The accessible name outside `GrFormField`: the cells are hidden from a screen reader, and a single field on top of them is read. |
name | string | undefined | undefined | The name of the field for a native form submission. |
form | string | undefined | undefined | The `id` of the form, if the field lies outside it. |
pattern | string | undefined | undefined | An alphabet of your own as a regex for **one character**. Overrides `type`. |
groups | number[] | undefined | undefined | Splitting into groups: `[3, 3]` draws `123 – 456`. The sum is obliged to equal `length`. |
masked | boolean | undefined | undefined | Dots instead of the characters — the PIN scenario. Does not affect `modelValue`. |
autofocus | boolean | undefined | false | Focus the field right after mounting — for a screen where entering the code is the whole action. |
oneTimeCode | boolean | undefined | true | `autocomplete="one-time-code"`: the code from an SMS is filled in on iOS and macOS. |
webOtp | boolean | undefined | false | Receiving the code from an SMS on Android through WebOTP. Off: it requires HTTPS and a special tail in the text of the message, which the backend adds. |
Slots
| Slot | Type | Description |
|---|---|---|
cell |
| Styling of your own for a cell. The size and the border are still held by the component. |
separator | { index: number; } | The sign between groups. By default — a dash. |
Events
| Event | Signature | Description |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: string): void | — |
change | (event: "change", value: string): void | — |
focus | (event: "focus", event: FocusEvent): void | — |
blur | (event: "blur", event: FocusEvent): void | — |
complete | (event: "complete", value: string): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
focus | () => void | Focus the code field. |
blur | () => void | Remove the focus from the code field. |
clear | () => void | Erase the code and return the focus to the first cell — when the server rejected the code and expects a new one. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrOtpInputConfigurableProps, GrOtpInputProps, GrOtpInputEmits } from '@feugene/granularity/components/GrOtpInput'Filedist/types/components/GrOtpInput/config.d.ts
const grOtpInputConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrOtpInput">;Filedist/types/components/GrOtpInput/defaults.d.ts
interface GrOtpInputConfigurableProps {
size: GrComponentSize;
length: number;
masked: boolean;
}Filedist/types/components/GrOtpInput/safelist.d.ts
const grOtpInputSafelist: string[];Filedist/types/components/GrOtpInput/GrOtpInput.vue.d.ts
interface GrOtpInputProps {
modelValue?: string;
length?: number;
type?: GrOtpInputType;
pattern?: string;
groups?: number[];
masked?: boolean;
placeholder?: string;
size?: GrOtpInputSize;
autofocus?: boolean;
oneTimeCode?: boolean;
webOtp?: boolean;
name?: string;
form?: string;
disabled?: boolean;
readonly?: boolean;
invalid?: boolean;
required?: boolean;
ariaLabel?: string;
}interface GrOtpInputEmits {
(e: 'update:modelValue', value: string): void;
(e: 'change', value: string): void;
(e: 'focus', event: FocusEvent): void;
(e: 'blur', event: FocusEvent): void;
(e: 'complete', value: string): void;
}Filedist/types/components/GrOtpInput/grOtpInputStyles.d.ts
type GrOtpInputSize = GrComponentSize;Filedist/types/components/GrOtpInput/otpValue.d.ts
type GrOtpInputType = typeof GR_OTP_INPUT_TYPES[number];Filedist/types/components/GrOtpInput/index.d.ts
type GrOtpInputInstance = ComponentExposed<typeof GrOtpInputComponent>;Accessibility
APG pattern
—
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
Many cells, one field
The component draws a row of cells, but in the DOM it is a single <input>,
stretched over the row and invisible: the text, the caret and the selection
highlight are transparent. The cells are decoration with aria-hidden.
This is the load-bearing decision, and almost all the behaviour of the field follows from it:
| here | six separate fields | |
|---|---|---|
| Pasting a code | native | distribute across the cells by hand |
| A code from an SMS on iOS | autocomplete="one-time-code" works | is filled into the first cell only |
Selection, Ctrl+A, undo | native | none |
Arrows, Home/End | the native caret | write by hand |
| Tab order | a single stop by construction | roving tabindex by hand |
| Screen reader | “edit field, 123456” | six unnamed fields in a row |
The package’s requirement “a composite widget is a single Tab stop”
(../keyboard.md) is met here by the construction, not on top
of it.
The cost is a single one: the active cell is not stored but derived from the caret position of the real field.
Typing replaces rather than shifts
The only place where the behaviour diverges from an ordinary field. The length is fixed, so a character typed into an occupied cell replaces its content, and the tail of the code stays in place.
The normalisation hangs on the input event, not on beforeinput, and that is
not an implementation detail: mobile keyboards and autofill send a whole string
in one event. The handler must accept alike a single character from the keyboard
and six from a message — they cannot be told apart by the event.
Characters outside the alphabet are dropped silently rather than blocking the
input: 123-456 copied from a messenger gives 123456. That is the main paste
scenario, and it must not require the user to clean the string by hand.
A click puts the caret into the first empty cell
Rather than where the cursor landed. The cells are decoration, a mouse hit on
invisible text means nothing meaningful, and “continue from where you stopped”
is the only thing expected of a code. Editing the middle is left to the arrows
and Backspace.
The code from a message is filled in by itself
Two channels, and together they cover both mobile platforms:
| Prop | Platform | State |
|---|---|---|
oneTimeCode | iOS, macOS | on: autocomplete="one-time-code" costs nothing and works by itself |
webOtp | Android | off |
webOtp is off not out of caution: WebOTP requires HTTPS and a special tail in
the text of the message (@example.com #123456), which the backend adds. On by
default, it would silently fail to work for everyone who does not know about
that.
The request lives in onMounted and is withdrawn when the code is filled in and
on unmount: navigator in the body of setup crashes server rendering
(../ssr.md), and an unclosed request outlives leaving the page.
A missing API, the user’s refusal and a cancellation are not errors: the field
simply waits for manual input.
The alphabet
type | What it accepts | inputmode |
|---|---|---|
numeric | digits | numeric |
alphanumeric | digits and Latin letters, converted to upper case | text |
text | any non-whitespace character | text |
pattern sets its own alphabet with a regex for a single character and
overrides type entirely — together with the case conversion, because a custom
regex decides for itself which case is allowed. A broken regex does not crash the
render: the component warns in dev and falls back to the type.
The mask is visual
masked draws dots, but the value of the field remains the real code. That is
how it should be: type="password" would break one-time-code, and there is
nobody to hide the value from — the text of the real field is transparent
anyway.
Appearance
size — xs…lg; the side of a cell repeats the height of GrInput at the same
step, so that the code sits in one line with the fields of a form. The points of
adjustment are the --gr-otp-input-cell-size, --gr-otp-input-active-brd and
--gr-otp-input-caret hooks (../tokens.md).
The caret is drawn as its own, because the real one is hidden together with the
text. The blinking is a CSS animation, so it obeys the package’s global motion
clamp (../motion.md): under prefers-reduced-motion the caret
stops visible, that is, exactly what it should be in a focused field.
The cell and separator slots change the appearance without touching the
behaviour: the size, the border and the caret are still held by the component.
Limits
- the resend timer — the state of the screen, not of the field;
- checking the code — the application listens to
completeand decides for itself; - the SMS format for WebOTP — the
@domain #codetail is added by the backend; - a general-purpose input mask — a phone number and a date are a different task.
Props in this example