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.

otp-input.ts
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.

Entered: nothing. Completed: not yet
<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.

We sent a 6-digit code to your email. Try 123456.

<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-field the 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 through aria-describedby.
  • kui-otp-input implements FormValueControl<string>: put [formField] on it, not on kui-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, so field().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 */
        },
      },
    }),
  ],
};
NameTypeDefaultDescription
size'xs' | 'sm' | 'md' | 'lg' — Component size. Takes precedence over the global defaults.size.
maskboolean — Masks the entered characters.
integerOnlyboolean — 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.

NameTypeDefaultDescription
labelstring — Accessible name of the cell group. Default Verification code.
digitKuiMessageFn<{ index: number; total: number; }> — Accessible name of one cell. Default Digit {index} of {total}.
verifyingstring — Name of the verifying state. Default Verifying code.

Inputs verified against @kikita-labs/ui v2.0.0 public typings.

NameTypeDefaultDescription
lengthnumber6Number of cells. Static numeric values are coerced; an invalid or non-positive value uses 1.
size'xs' | 'sm' | 'md' | 'lg' | undefinedundefinedCell size, the same scale as Input. Falls back to defaults.otpInput.size, then the root size, then md.
maskboolean | undefinedundefinedRenders every cell as type="password" (defaults.otpInput.mask, then false).
integerOnlyboolean | undefinedundefinedRestricts input to digits with a numeric mobile keyboard (defaults.otpInput.integerOnly, then true). false accepts letters and digits, uppercased.
autoFocusbooleanfalseFocuses the first cell that can take focus after the first render, and again on every false to true change.
ariaLabelstring | undefinedundefinedAccessible 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)].
disabledbooleanfalseDisables every cell with the native attribute. Set by [formField] or directly.
readonlybooleanfalseMakes every cell read-only and also blocks pasting a new code and the cross-cell Backspace clear.
requiredbooleanfalseMarks the code required: the first cell, which the field label points at, gets the native required state. Also follows a required ancestor kui-field.
loadingbooleanfalseDisables every cell, blurs the code in place and shows a centered Loader without changing the group size.
invalidbooleanfalseMarks every cell invalid. Through [formField] the raw state is gated by touched before it shows.
errorsreadonly ValidationError[][]Current validation errors. Set by [formField].
touchedbooleanfalseWhether 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-blurCSS custom properties-Gap between cells, the square cell size, the digit font size and the loading blur (2px).
KuiOtpInputOptionsinterface-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 the ariaLabel or the otpInput.label message; each cell has its own Digit N of M name from the otpInput.digit message.
  • 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.
  • disabled and loading use the native attribute, so the cells leave the tab order. readonly also 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.