Input OTP
One-time password input component with prop-config-driven API and composable components
Overview
The Input OTP component provides a styled one-time password input field with two usage patterns:
- Prop-config-driven: Simple API for common use cases
- Composable: Full control over layout and structure
It includes automatic styling for focus, disabled, and error states, with proper accessibility attributes and a visual caret indicator. Built on top of input-otp by @guilhermerodz.
Import
Prop-config-driven API
import { OtpInput } from "@px-ui/core";Composable API
import {
OTPInput,
OtpInputGroup,
OtpInputSlot,
OtpInputSeparator,
OTPInputContext,
} from "@px-ui/core";Usage
Prop-Config-Driven API
The simplest way to use the OTP input is with the prop-config-driven OtpInput component. It handles the layout automatically based on props.
Basic Usage
import { OtpInput } from "@px-ui/core";
<OtpInput length={6} />With Separator
Add separators at specific positions using the separatorAfter prop (0-indexed positions).
<OtpInput length={6} separatorAfter={[2]} />
// Adds separator after the 3rd digit (index 2)Sizes
The OtpInput component supports two size variants for different interface densities.
<OtpInput size="sm" length={6} />
<OtpInput size="default" length={6} />Different Lengths
Configure the OTP input for different use cases.
// 4-digit PIN
<OtpInput length={4} />
// 6-digit OTP
<OtpInput length={6} />
// 8-digit code
<OtpInput length={8} separatorAfter={[2, 5]} />Controlled Value
Use controlled value and onChange handlers.
const [value, setValue] = React.useState("");
<OtpInput
length={6}
value={value}
onChange={(value) => setValue(value)}
/>Validation States
The OtpInput component supports validation states through the invalid prop, which applies error styling and sets the appropriate ARIA attributes.
// Valid state (default)
<OtpInput length={6} />
// Invalid state
<OtpInput length={6} invalid />
// Disabled state
<OtpInput length={6} disabled />
// Invalid and disabled
<OtpInput length={6} invalid disabled />Composable API
For advanced use cases where you need full control over the layout, use the composable API with OTPInput from input-otp. You'll need to create your own styled Group, Slot, and Separator components using the OTPInputContext.
Creating Styled Components
First, create your own styled components using OTPInputContext:
import { OTPInput, OTPInputContext } from "@px-ui/core";
import * as React from "react";
function OtpInputGroup({ className, ...props }: React.ComponentProps<"div">) {
return <div className={`flex items-center ${className || ""}`} {...props} />;
}
function OtpInputSlot({
index,
className,
...props
}: React.ComponentProps<"div"> & { index: number }) {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
return (
<div
data-active={isActive}
className={`data-[active=true]:border-ring border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm first:rounded-l-md first:border-l last:rounded-r-md ${className || ""}`}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
);
}
function OtpInputSeparator({ className, ...props }: React.ComponentProps<"div">) {
return (
<div className={`flex items-center ${className || ""}`} {...props}>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className="h-4 w-4">
<path d="M2 6h8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</div>
);
}Basic Composable Usage
import { OTPInput } from "@px-ui/core";
<OTPInput maxLength={6}>
<OtpInputGroup>
<OtpInputSlot index={0} />
<OtpInputSlot index={1} />
<OtpInputSlot index={2} />
<OtpInputSlot index={3} />
<OtpInputSlot index={4} />
<OtpInputSlot index={5} />
</OtpInputGroup>
</OTPInput>With Separator
You can use the <OtpInputSeparator /> component to add a separator between the input groups.
import { OTPInput } from "@px-ui/core";
// Note: You need to create your own OtpInputGroup, OtpInputSlot, and OtpInputSeparator components
<OTPInput maxLength={6}>
<OtpInputGroup>
<OtpInputSlot index={0} />
<OtpInputSlot index={1} />
<OtpInputSlot index={2} />
</OtpInputGroup>
<OtpInputSeparator />
<OtpInputGroup>
<OtpInputSlot index={3} />
<OtpInputSlot index={4} />
<OtpInputSlot index={5} />
</OtpInputGroup>
</OTPInput>Custom Layouts
Create any custom layout you need.
// 8-digit code with custom grouping (3-3-2)
<OTPInput maxLength={8}>
<OtpInputGroup>
<OtpInputSlot index={0} />
<OtpInputSlot index={1} />
<OtpInputSlot index={2} />
</OtpInputGroup>
<OtpInputSeparator />
<OtpInputGroup>
<OtpInputSlot index={3} />
<OtpInputSlot index={4} />
<OtpInputSlot index={5} />
</OtpInputGroup>
<OtpInputSeparator />
<OtpInputGroup>
<OtpInputSlot index={6} />
<OtpInputSlot index={7} />
</OtpInputGroup>
</OTPInput>Pattern
Use the pattern prop to define a custom pattern for the OTP input.
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
import { OTPInput } from "@px-ui/core";
// Note: OtpInputGroup and OtpInputSlot are custom components you create
<OTPInput maxLength={6} pattern={REGEXP_ONLY_DIGITS_AND_CHARS}>
<OtpInputGroup>
<OtpInputSlot index={0} />
<OtpInputSlot index={1} />
<OtpInputSlot index={2} />
<OtpInputSlot index={3} />
<OtpInputSlot index={4} />
<OtpInputSlot index={5} />
</OtpInputGroup>
</OTPInput>Controlled
You can use the value and onChange props to control the input value.
import { OTPInput } from "@px-ui/core";
// Note: OtpInputGroup and OtpInputSlot are custom components you create
const [value, setValue] = React.useState("");
<OTPInput
maxLength={6}
value={value}
onChange={(value) => setValue(value)}
>
<OtpInputGroup>
<OtpInputSlot index={0} />
<OtpInputSlot index={1} />
<OtpInputSlot index={2} />
<OtpInputSlot index={3} />
<OtpInputSlot index={4} />
<OtpInputSlot index={5} />
</OtpInputGroup>
</OTPInput>Advanced: Using Context
Access the OTP input context for custom slot implementations.
import { OTPInputContext } from "@px-ui/core";
function CustomSlot({ index }: { index: number }) {
const context = React.useContext(OTPInputContext);
const slot = context?.slots[index];
return (
<div>
{slot?.char || ""}
{slot?.isActive && <span>Active</span>}
</div>
);
}API Reference
OtpInput (Prop-Config-Driven)
A simple, prop-config-driven component for common OTP input use cases.
| Prop | Type | Default | Description |
|---|---|---|---|
length | number | 6 | Number of OTP digits |
size | "default" | "sm" | "default" | Size variant of the OTP input |
separatorAfter | number[] | [] | Positions after which to insert separators (0-indexed) |
value | string | - | Controlled value |
onChange | (value: string) => void | - | Callback fired when value changes |
disabled | boolean | false | When true, disables the input |
invalid | boolean | false | When true, applies error styling and sets aria-invalid attribute |
maxLength | number | - | Maximum length (overrides length if provided) |
className | string | - | Additional CSS classes for the input |
containerClassName | string | - | Additional CSS classes for the container |
slotClassName | string | - | Additional CSS classes for slots |
separatorClassName | string | - | Additional CSS classes for separator |
Inherited Props: Inherits all props from the underlying input-otp library's OTPInput component (except children and render).
OTPInput (Composable)
The base component from input-otp library. Use this for composable layouts. You'll need to create your own styled Group, Slot, and Separator components using OTPInputContext.
| Prop | Type | Default | Description |
|---|---|---|---|
maxLength | number | - | Required. Maximum number of characters |
value | string | - | Controlled value |
onChange | (value: string) => void | - | Callback fired when value changes |
disabled | boolean | false | When true, disables the input |
pattern | RegExp | - | Pattern to validate input against |
className | string | - | Additional CSS classes |
containerClassName | string | - | Additional CSS classes for the container |
Inherited Props: See input-otp documentation for full API.
Note: input-otp doesn't provide styled Group, Slot, or Separator components. You need to create your own styled components using OTPInputContext to access slot state. See the "Creating Styled Components" section above for examples.
OTPInputContext
React context from input-otp library. Use this to access slot state for custom implementations.
Context Value:
{
slots: Array<{
char: string
isActive: boolean
hasFakeCaret: boolean
}>
// ... other internal state
}When to Use Which API
Use Prop-Config-Driven (OtpInput) when:
- You need a simple, standard OTP input
- You want less boilerplate code
- Standard layouts (4, 6, or 8 digits) are sufficient
- You need quick implementation
Use Composable API (OTPInput from input-otp) when:
- You need custom layouts that don't fit standard patterns
- You want full control over slot rendering
- You need to integrate with custom UI components
- You're building a complex authentication flow
- You need pattern validation or other advanced features
- You want to create your own styled components
Notes
- The prop-driven
OtpInputcomponent uses theinput-otplibrary under the hood for core functionality - For composable usage, you need to create your own styled components using
OTPInputContext - Each slot component must have a unique
indexprop matching its position (0, 1, 2, etc.) - The number of slots should match the
maxLengthprop value - The component automatically handles focus management and character input
- Error states are controlled via the
invalidprop (setsaria-invalidattribute automatically) - The
separatorAfterprop in the config-driven API uses 0-indexed positions (e.g.,[2]adds a separator after the 3rd digit) - For pattern validation, import regex patterns from
input-otp(e.g.,REGEXP_ONLY_DIGITS_AND_CHARS) - All
input-otpexports are available from@px-ui/corefor composable usage