Introduction
Components
- Accordion
- Action Sheet
- Alert
- Audio Player
- Audio Recorder
- Audio Waveform
- Avatar
- Badge
- BottomSheet
- Button
- Camera
- Camera Preview
- Card
- Carousel
- Checkbox
- Collapsible
- Color Picker
- Combobox
- Date Picker
- File Picker
- Gallery
- Hello Wave
- Icon
- Image
- Input
- Input OTP
- Link
- MediaPicker
- Mode Toggle
- Onboarding
- ParallaxScrollView
- Picker
- Popover
- Progress
- Radio
- ScrollView
- SearchBar
- Separator
- Share
- Sheet
- Skeleton
- Spinner
- Switch
- Table
- Tabs
- Text
- Toast
- Toggle
- Video
- View
Charts
import { InputOTP } from '@/components/ui/input-otp';
import React, { useState } from 'react';
export function InputOTPDemo() {
const [otp, setOtp] = useState('');
return (
<InputOTP
length={6}
value={otp}
onChangeText={setOtp}
onComplete={(value) => {
console.log('OTP Complete:', value);
}}
/>
);
}
Installation
pnpm dlx bna-ui add input-otp
Usage
import { InputOTP, InputOTPWithSeparator } from '@/components/ui/input-otp';
<InputOTP
length={6}
value={otp}
onChangeText={setOtp}
onComplete={(value) => console.log('OTP Complete:', value)}
/>
Examples
Default
import { InputOTP } from '@/components/ui/input-otp';
import React, { useState } from 'react';
export function InputOTPDemo() {
const [otp, setOtp] = useState('');
return (
<InputOTP
length={6}
value={otp}
onChangeText={setOtp}
onComplete={(value) => {
console.log('OTP Complete:', value);
}}
/>
);
}
Different Lengths
import { InputOTP } from '@/components/ui/input-otp';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';
export function InputOTPLengths() {
const [otp4, setOtp4] = useState('');
const [otp6, setOtp6] = useState('');
return (
<View style={{ gap: 20 }}>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>4 Digits</Text>
<InputOTP length={4} value={otp4} onChangeText={setOtp4} />
</View>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>
6 Digits (Default)
</Text>
<InputOTP length={6} value={otp6} onChangeText={setOtp6} />
</View>
</View>
);
}
With Separator
import { InputOTP, InputOTPWithSeparator } from '@/components/ui/input-otp';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useThemeColor } from '@/hooks/useThemeColor';
import React, { useState } from 'react';
export function InputOTPSeparator() {
const [otp1, setOtp1] = useState('');
const [otp2, setOtp2] = useState('');
const [otp3, setOtp3] = useState('');
const muted = useThemeColor({}, 'textMuted');
return (
<View style={{ gap: 24 }}>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>
With Dash Separator
</Text>
<InputOTPWithSeparator length={6} value={otp1} onChangeText={setOtp1} />
</View>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>
With Dot Separator
</Text>
<InputOTP
length={6}
value={otp2}
onChangeText={setOtp2}
separator={
<Text style={{ fontSize: 16, color: muted, fontWeight: 'bold' }}>
•
</Text>
}
/>
</View>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>
With Custom Separator
</Text>
<InputOTP
length={4}
value={otp3}
onChangeText={setOtp3}
separator={
<View
style={{
width: 8,
height: 2,
backgroundColor: muted,
marginHorizontal: 4,
}}
/>
}
/>
</View>
</View>
);
}
Masked Input
import { InputOTP } from '@/components/ui/input-otp';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';
export function InputOTPMasked() {
const [normalOtp, setNormalOtp] = useState('');
const [maskedOtp, setMaskedOtp] = useState('');
return (
<View style={{ gap: 24 }}>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>
Normal (Visible Digits)
</Text>
<InputOTP length={6} value={normalOtp} onChangeText={setNormalOtp} />
{normalOtp && (
<Text style={{ fontSize: 12, opacity: 0.7 }}>
Current value: {normalOtp}
</Text>
)}
</View>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>
Masked (Hidden Digits)
</Text>
<InputOTP
length={6}
value={maskedOtp}
onChangeText={setMaskedOtp}
masked={true}
/>
{maskedOtp && (
<Text style={{ fontSize: 12, opacity: 0.7 }}>
Current value: {maskedOtp}
</Text>
)}
</View>
</View>
);
}
Error State
import { Button } from '@/components/ui/button';
import { InputOTP } from '@/components/ui/input-otp';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';
export function InputOTPError() {
const [otp, setOtp] = useState('');
const [error, setError] = useState('');
const validateOtp = (value: string) => {
if (value.length === 6) {
// Simulate validation - reject if all digits are the same
if (value === '111111' || value === '000000') {
setError('Invalid verification code. Please try again.');
} else {
setError('');
}
} else {
setError('');
}
};
const handleOtpChange = (value: string) => {
setOtp(value);
validateOtp(value);
};
const simulateError = () => {
setError('Verification code has expired. Please request a new one.');
};
const clearError = () => {
setError('');
setOtp('');
};
return (
<View style={{ gap: 16, alignItems: 'center' }}>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>
Enter Verification Code
</Text>
<Text style={{ fontSize: 12, opacity: 0.7, textAlign: 'center' }}>
Try entering "111111" or "000000" to see error state
</Text>
</View>
<InputOTP
length={6}
value={otp}
onChangeText={handleOtpChange}
error={error}
onComplete={(value) => {
if (!error) {
console.log('Valid OTP:', value);
}
}}
/>
<View style={{ flexDirection: 'row', gap: 12 }}>
<Button variant='outline' size='sm' onPress={simulateError}>
Simulate Error
</Button>
<Button variant='outline' size='sm' onPress={clearError}>
Clear
</Button>
</View>
</View>
);
}
Disabled State
import { Button } from '@/components/ui/button';
import { InputOTP } from '@/components/ui/input-otp';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';
export function InputOTPDisabled() {
const [otp, setOtp] = useState('123');
const [disabled, setDisabled] = useState(true);
return (
<View style={{ gap: 16, alignItems: 'center' }}>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>Disabled State</Text>
<Text style={{ fontSize: 12, opacity: 0.7, textAlign: 'center' }}>
Toggle the button below to enable/disable the input
</Text>
</View>
<InputOTP
length={6}
value={otp}
onChangeText={setOtp}
disabled={disabled}
/>
<Button
variant={disabled ? 'default' : 'outline'}
size='sm'
onPress={() => setDisabled(!disabled)}
>
{disabled ? 'Enable Input' : 'Disable Input'}
</Button>
{!disabled && (
<Text style={{ fontSize: 12, opacity: 0.7 }}>Current value: {otp}</Text>
)}
</View>
);
}
Custom Styling
import { InputOTP } from '@/components/ui/input-otp';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useThemeColor } from '@/hooks/useThemeColor';
import React, { useState } from 'react';
export function InputOTPStyled() {
const [otp1, setOtp1] = useState('');
const [otp2, setOtp2] = useState('');
const [otp3, setOtp3] = useState('');
const primary = useThemeColor({}, 'primary');
const success = '#10B981';
const purple = '#8B5CF6';
return (
<View style={{ gap: 24 }}>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>Rounded Style</Text>
<InputOTP
length={6}
value={otp1}
onChangeText={setOtp1}
slotStyle={{
borderRadius: 25,
borderWidth: 2,
borderColor: primary,
}}
/>
</View>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>Success Theme</Text>
<InputOTP
length={4}
value={otp2}
onChangeText={setOtp2}
slotStyle={{
borderColor: success,
backgroundColor: success + '10',
borderRadius: 8,
}}
/>
</View>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>Large & Purple</Text>
<InputOTP
length={4}
value={otp3}
onChangeText={setOtp3}
slotStyle={{
width: 70,
height: 70,
borderColor: purple,
borderWidth: 2,
borderRadius: 12,
backgroundColor: purple + '05',
}}
containerStyle={{
gap: 12,
}}
/>
</View>
</View>
);
}
Without Cursor
import { InputOTP } from '@/components/ui/input-otp';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';
export function InputOTPNoCursor() {
const [otpWithCursor, setOtpWithCursor] = useState('');
const [otpWithoutCursor, setOtpWithoutCursor] = useState('');
return (
<View style={{ gap: 24 }}>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>
With Cursor (Default)
</Text>
<InputOTP
length={6}
value={otpWithCursor}
onChangeText={setOtpWithCursor}
showCursor={true}
/>
</View>
<View style={{ alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, fontWeight: '500' }}>Without Cursor</Text>
<InputOTP
length={6}
value={otpWithoutCursor}
onChangeText={setOtpWithoutCursor}
showCursor={false}
/>
</View>
<Text style={{ fontSize: 12, opacity: 0.7, textAlign: 'center' }}>
Tap on the inputs above to see the difference in cursor behavior
</Text>
</View>
);
}
API Reference
InputOTP
The main OTP input component for handling one-time passwords and verification codes.
Prop | Type | Default | Description |
---|---|---|---|
length | number | 6 | Number of OTP digits to display. |
value | string | '' | Current OTP value. |
onChangeText | (value: string) => void | - | Called when OTP value changes. |
onComplete | (value: string) => void | - | Called when OTP is complete (all digits filled). |
error | string | - | Error message to display below the input. |
disabled | boolean | false | Whether the input is disabled. |
masked | boolean | false | Whether to mask digits with dots for security. |
showCursor | boolean | true | Whether to show cursor in the active slot. |
separator | ReactNode | - | Custom separator component between slots. |
containerStyle | ViewStyle | - | Additional styles for the container. |
slotStyle | ViewStyle | - | Additional styles for individual digit slots. |
errorStyle | TextStyle | - | Additional styles for the error message. |
InputOTPWithSeparator
A preset variant of InputOTP that includes dash separators between digits.
Prop | Type | Default | Description |
---|---|---|---|
All props from InputOTP except separator | - | - | Inherits all InputOTP props except separator which is preset to a dash. |
InputOTPRef
Reference object that provides programmatic control over the InputOTP component.
Method | Type | Description |
---|---|---|
focus | () => void | Focuses the input. |
blur | () => void | Blurs the input. |
clear | () => void | Clears all entered digits. |
getValue | () => string | Returns the current OTP value. |
Usage with Ref
import { useRef } from 'react';
import { InputOTP, InputOTPRef } from '@/components/ui/input-otp';
export function MyComponent() {
const otpRef = useRef<InputOTPRef>(null);
const handleClear = () => {
otpRef.current?.clear();
};
const handleFocus = () => {
otpRef.current?.focus();
};
return (
<InputOTP
ref={otpRef}
length={6}
onComplete={(value) => {
console.log('OTP entered:', value);
}}
/>
);
}
Accessibility
The InputOTP component is built with accessibility in mind:
- Uses a hidden TextInput for proper keyboard handling and screen reader support
- Supports dynamic text sizing for better readability
- Provides proper focus management between slots
- Error messages are announced by screen readers
- Maintains proper contrast ratios for all states
- Supports keyboard navigation and input
Security Considerations
- Use the
masked
prop when dealing with sensitive verification codes - Always validate OTP values on the server side
- Consider implementing rate limiting for OTP attempts
- Clear sensitive OTP values from memory when no longer needed