OTP Input
Row of single-character cells for a one-time code or PIN.
OTP Input is a standalone component. Each cell is a native input styled like Input; the component owns roving keyboard navigation, paste distribution and the composite value.
import { KuiOtpInput } from '@kikita-labs/ui';Bind value with a signal. (complete) fires once with the completed code when every cell becomes filled, and again only after the code became incomplete and was refilled.
<div class="otp-example">
<kui-otp-input [(value)]="code" (complete)="verify($event)" />
<span>Entered: {{ code() || 'nothing' }}. Completed: {{ verified() || 'not yet' }}</span>
</div>length defaults to 6. size uses the xs to lg scale of Input. autoFocus focuses the first cell after render.
mask renders every cell as a password input. integerOnly false accepts letters and digits, uppercased, for backup codes.
<div class="otp-example">
<kui-otp-input mask [length]="4" [(value)]="pin" ariaLabel="PIN code" />
<kui-otp-input
[integerOnly]="false"
[length]="8"
size="sm"
[(value)]="backupCode"
ariaLabel="Backup code"
/>
</div>Label, hint, error and the required state stay kui-field's job. loading disables and blurs the cells and centers a Loader; there is no built-in success or error status, since a server result is unknown to the component.
<div class="otp-example">
<kui-field
label="Code from email"
hint="We sent a 6-digit code to your email. Try 123456."
[error]="wrong() ? 'Code is wrong or expired' : ''"
>
<kui-otp-input [(value)]="code" [loading]="verifying()" [invalid]="wrong()" />
</kui-field>
<button kuiButton shape="outline" size="sm" type="button" (click)="toggleVerifying()">
Toggle loading
</button>
</div>- Inside
kui-fieldthe first cell adopts the field control id, so the label targets it and a click on the label focuses it. The hint and error reach the group througharia-describedby. kui-otp-inputimplementsFormValueControl<string>: put[formField]on it, not onkui-field. A required validator message and the invalid cell styling stay hidden until the field is touched.focus(options?)moves focus to the first cell that can take focus, sofield().focusBoundControl()works.
Set defaults.otpInput once for the application or for a subtree. Each option resolves as local input > defaults.otpInput.<option> > built-in default.
import { provideKikitaUi } from '@kikita-labs/ui';
export const appConfig: ApplicationConfig = {
providers: [
provideKikitaUi({
defaults: {
otpInput: {
/* options below */
},
},
}),
],
};| Name | Type | Default | Description |
|---|---|---|---|
| size | 'xs' | 'sm' | 'md' | 'lg' | — | Component size. Takes precedence over the global defaults.size. |
| mask | boolean | — | Masks the entered characters. |
| integerOnly | boolean | — | Accepts digits only. |
Every text the component owns (accessible names, visible words, announcements) is a typed message with an English default. Override it for the application with provideKikitaUi({ messages }), or for a subtree with provideKuiMessages.
| Name | Type | Default | Description |
|---|---|---|---|
| label | string | — | Accessible name of the cell group. Default Verification code. |
| digit | KuiMessageFn<{ index: number; total: number; }> | — | Accessible name of one cell. Default Digit {index} of {total}. |
| verifying | string | — | Name of the verifying state. Default Verifying code. |
Inputs verified against @kikita-labs/ui v2.0.0 public typings.
| Name | Type | Default | Description |
|---|---|---|---|
| length | number | 6 | Number of cells. Static numeric values are coerced; an invalid or non-positive value uses 1. |
| size | 'xs' | 'sm' | 'md' | 'lg' | undefined | undefined | Cell size, the same scale as Input. Falls back to defaults.otpInput.size, then the root size, then md. |
| mask | boolean | undefined | undefined | Renders every cell as type="password" (defaults.otpInput.mask, then false). |
| integerOnly | boolean | undefined | undefined | Restricts input to digits with a numeric mobile keyboard (defaults.otpInput.integerOnly, then true). false accepts letters and digits, uppercased. |
| autoFocus | boolean | false | Focuses the first cell that can take focus after the first render, and again on every false to true change. |
| ariaLabel | string | undefined | undefined | Accessible name of the role="group" cell group. Falls back to the otpInput.label message (Verification code). |
| [(value)] | string | '' | Joined characters of every cell, in order. Set by [formField] or [(value)]. |
| disabled | boolean | false | Disables every cell with the native attribute. Set by [formField] or directly. |
| readonly | boolean | false | Makes every cell read-only and also blocks pasting a new code and the cross-cell Backspace clear. |
| required | boolean | false | Marks the code required: the first cell, which the field label points at, gets the native required state. Also follows a required ancestor kui-field. |
| loading | boolean | false | Disables every cell, blurs the code in place and shows a centered Loader without changing the group size. |
| invalid | boolean | false | Marks every cell invalid. Through [formField] the raw state is gated by touched before it shows. |
| errors | readonly ValidationError[] | [] | Current validation errors. Set by [formField]. |
| touched | boolean | false | Whether the control has been touched. Set by [formField]. |
| (touch) | void | - | Emitted after any cell edit; marks the control touched in the form system. |
| (complete) | string | - | Emitted once with the completed value when every cell becomes filled. It fires again only after the code became incomplete and was refilled. |
| focus(options?) | (options?: FocusOptions) => void | - | Moves focus to the first cell that can take focus, so focusBoundControl() works. Does nothing while every cell is disabled. |
| --kui-otp-gap / -cell-size / -cell-font-size / -loading-blur | CSS custom properties | - | Gap between cells, the square cell size, the digit font size and the loading blur (2px). |
| KuiOtpInputOptions | interface | - | Shape of defaults.otpInput: size, mask and integerOnly. |
The group is named, each cell is positioned, and keyboard movement stays inside the group.
- The group is
role="group"with theariaLabelor theotpInput.labelmessage; each cell has its ownDigit N of Mname from theotpInput.digitmessage. - Typing a valid character fills the cell and moves to the next empty one. Backspace on an empty cell clears and refocuses the previous cell. ArrowLeft and ArrowRight move without changing values, and Home and End jump to the first and last cell. Tab leaves the whole group.
- Ctrl or Cmd + V distributes a pasted code from the first cell, whichever cell had focus.
- The first cell carries
autocomplete="one-time-code"for SMS autofill on iOS and Android. disabledandloadinguse the native attribute, so the cells leave the tab order.readonlyalso blocks paste and the cross-cell Backspace clear.
Documented limits of the installed package.
- Clearing a cell while later cells stay filled collapses those positions in the joined
value, as in other OTP inputs. - There is no built-in success or error status icon after verification.
- A formal assistive-technology review has not been run.