Skip to content

OTP Input

Controlled/uncontrolled one-time-code input with numeric normalization and per-value completion callbacks.

  • Category: Forms & selection
  • Status: stable public Registry/export-map component family
  • Targets: iOS · Android · Web, subject to the compatibility contract
  • Prerequisites: @beemvp/beeui-ui installed (or this component’s source copied via the Registry CLI below) and, on Web, the BeeUI Tailwind/Uniwind theme CSS loaded — see Start for full platform setup.
  • Source: packages/ui/src/components/otp-input.tsx
import { OTPInput } from '@beemvp/beeui-ui';

There is no documented deep/private source import. For source ownership from a BeeUI checkout:

Terminal window
pnpm beeui add otp-input

Registry (used throughout this page) is BeeUI’s source-ownership manifest — registry/registry.json — that the pnpm beeui/@beemvp/beeui-cli CLI reads to copy a component’s real source into your app and rewrite its internal imports; it is not an npm package index. This component’s own Registry entry: registry/registry.json.

  • Primary export: OTPInput
    • Also routed here, outside the Registry family:
      • otp-input
    • Package export subpath: @beemvp/beeui-ui/otp-input

Exported types: OTPInputProps

The generated API inventory is mechanically joined to packages/ui/src/index.ts, Registry metadata, and the component reference contract. Each type’s field table below is parsed directly from that source, not a second hand-maintained copy; for the fuller behavior narrative see the canonical component behavior catalog.

Controlled/uncontrolled one-time-code input; entered text is normalized to digits only when mode is 'numeric' (the default) — mode: 'text' accepts any character unnormalized — and the completion callback fires exactly once per completed value — it does not re-fire on further keystrokes while already complete, only after the value becomes incomplete again. appearance ('joined' default, or 'segmented') is a pure rendering switch: 'segmented' draws one visually separate box per character over the same single hidden input — one value, one caret (hidden; the active box shows a focus-ring border instead), one change contract — it does not change the value/completion/normalization behavior above.

Prop Type Default Description
appearance 'joined' | 'segmented' 'joined' 'joined' (the default, unchanged) renders one plain text field with letter-spacing — no per-character caret or box. 'segmented' renders length visually separate boxes over the exact same single hidden input: one real caret, one value, one onChangeText — the boxes are pure decoration, positioned over the input, never a second source of truth for the code. The active (next-to-fill) box gets a focus-ring border while the hidden input is focused. Reuses only existing semantic tokens (bg-input, border-control-border/border-destructive, border-focus-ring).
defaultValue string '' Initial code for uncontrolled usage; normalized (digits-only in 'numeric' mode, truncated to length) like any other value. Defaults to ''.
length number 6 Number of characters the code must reach before onComplete fires. Also sets the underlying input’s maxLength. Defaults to 6.
mode 'numeric' | 'text' 'numeric' 'numeric' strips non-digit characters as they are typed and shows a numeric keyboard; 'text' accepts any character. Defaults to 'numeric'.
onComplete (value: string) => void — Called once when the code reaches length characters; not called again for the same value until it changes and comes back to full length.
onValueChange (value: string) => void — Called with the normalized value on every change, including partial (incomplete) codes.
value string — Controlled code value; normalized (digits-only in 'numeric' mode, truncated to length) before being displayed.

Also carries every prop of Omit<InputProps, 'defaultValue' | 'inputMode' | 'keyboardType' | 'maxLength' | 'onChangeText' | 'value'> — documented on the Input page, not reproduced here.

The executable fixtures below are the source-grounded usage examples; consumers should not infer state ownership from DOM structure or another UI library.

  • No additional provider is required by this family. BeeUIProvider remains the recommended application root.
  • Peer/native dependencies visible to this Registry item: react, react-native
  • Registry dependency closure: core-cn, input, theme
  • Safe-area ownership remains explicit: shell surfaces touching system edges opt into SafeArea; components do not silently invent app-shell insets.
  • Web consumers load the BeeUI semantic theme CSS as documented in Web onboarding.

This family ships no platform-specific file, and its own source takes no Platform branch.

The same public family is exposed across the supported target matrix; meaningful platform differences remain governed by the compatibility contract.

Evidence classes are not equal and this page does not blur them: Web behavior is exercised in a real browser, while iOS and Android carry package/export and native-compile evidence, which is not device-runtime proof. The compatibility contract records which class each claim rests on.

  • Roles this family assigns: none set in otp-input.tsx.
  • Accessibility states and properties it sets: none set in otp-input.tsx.

Keyboard/focus behavior, announcements, Dynamic Type/Web zoom, RTL and reduced-motion expectations are not derived here — see Accessibility overview, Keyboard & focus, RTL/localization and Large text & zoom. BeeUI does not claim universal accessibility certification from automated tests.

  • Style axes: size (3 values, inherited from InputProps).
  • Class-name surfaces: className.

Colors, spacing and typography come from semantic tokens rather than from values written here — see Theming and Density. A className is an escape hatch for source-owned and application work, not a cross-engine portability guarantee.

Each link below opens the Showcase at that exact example, not at the top of the gallery:

  • basic — smallest meaningful complete usage

The Showcase links demonstrate Web behavior; use the native-preview guide for real simulator/emulator/device paths.

This frame loads the real BeeUI Web Showcase on demand; it is not a second docs-only implementation. It proves browser behavior only. Use native preview for iOS/Android simulator, emulator or device paths.

  • Family root / primary export: OTPInput
    • Exported type surface:
      • OTPInputProps

The tree above is ordinary document structure so it remains readable with keyboard and assistive technology; it is derived from the real public export family rather than a canvas-only diagram.

These are the parts of the typechecked runtime Showcase fixture behind this live preview — apps/showcase/component-gallery/component-gallery.tsx, 1103 lines — where OTP Input is actually used: 5 lines in 1 place. Each block is copied verbatim from the line range named above it, so it is the same executable source, not a retelling of it. The rest of that file exercises other families and is not reproduced here.

Imports the examples below need (a filtered subset of the fixture’s own top-level imports):

import { OTPInput } from '@beemvp/beeui-ui';
import * as React from 'react';

Fixture state this block reads (same file, line 464):

const [otp, setOtp] = React.useState('');

lines 607–611:

<OTPInput
accessibilityLabel="Verification code"
onValueChange={setOtp}
value={otp}
/>

Open the fixture itself for the full surrounding component. For a smaller app-specific example, start from the imports and fixture-state blocks above and keep only the state your screen owns.

One text field, not one box per digit: there is no per-character slot, caret or focus movement — appearance="segmented" only changes what is drawn, not this input model. The length limit, keyboard type and change handler are owned here and not accepted from outside.

Implementation note: OTPInputProps omits onChangeText from InputProps; the only change callback is the inherited React Native onChange, which receives a NativeSyntheticEvent<TextInputChangeEventData>, not a plain string — read the new value from event.nativeEvent.text.