Lightweight, accessible, and flexible React OTP (one-time passcode) input component with a slots API. Use it as-is for a simple, styled OTP row or supply custom slot components for deep UI-library integration (ShadCN, AntD, MUI, etc.).
If you found this project helpful, a ⭐ on GitHub would mean a lot — it helps others discover it too!
- Controlled API (pass
valueandonChange). - Slots-based customization: replace
Container,Input, orSeparatorelements. - Keyboard-friendly: auto-advance on input, backspace moves back and clears.
- Paste support: paste a full code into any input and it will populate the inputs (truncated to length).
- Minimal CSS included (in
src/component/react-otp/ReactOtp.css) and easy to theme.
import React, { useState } from "react"
import { ReactOtp } from "react-smart-otp"
// CSS required only when using default input slot and container
import "react-smart-otp/dist/index.css"
function Verify() {
const [code, setCode] = useState("")
return <ReactOtp length={6} value={code} onChange={setCode} />
}import { useState } from "react"
import { ReactOtp } from "react-smart-otp"
import { Button } from "./components/ui/button"
import { Card, CardContent } from "./components/ui/card"
import { Input } from "./components/ui/input"
import "react-smart-otp/dist/index.css"
function App() {
const [otp, setOtp] = useState("")
return (
<>
<ReactOtp
value={otp}
length={4}
onChange={setOtp}
defaultFocus={true}
slots={{
Container: ({ children }) => (
<Card>
<CardContent
style={{
display: "flex",
justifyContent: "center",
flexDirection: "row",
alignItems: "center",
gap: "10px"
}}
>
{children}
</CardContent>
</Card>
),
Input: (props) => <Input className="w-10 text-center" {...props} />,
Separator: (props) => <span {...props}>-</span>
}}
/>
<br />
<Button onClick={() => setOtp("")}>Clear</Button>
</>
)
}
export default App| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| value | string |
✅ Yes | — | The full concatenated OTP value (controlled mode). |
| length | number |
✅ Yes | — | Number of digits/inputs to render. |
| onChange | (val: string) => void |
✅ Yes | — | Called with the concatenated value whenever any digit changes. |
| inputType | 'text' | 'password' | 'number' |
❌ No | 'text' |
Input type attribute applied to each input. |
| defaultFocus | boolean |
❌ No | false |
Focus the first input on mount when true. |
| slots | { Container?: ElementType; Input?: ElementType; Separator?: ElementType } |
❌ No | — | Optional slot components to replace internal elements. |
| Slot | Receives | Description |
|---|---|---|
| Container | HTMLAttributes<HTMLElement> |
Used to wrap all inputs. |
| Input | InputHTMLAttributes<HTMLInputElement> |
Rendered for each digit. Component spreads ref, value, onChange, onKeyDown, onPaste, onFocus, maxLength, type, id, data-testid, and aria-label. |
| Separator | HTMLAttributes<HTMLElement> |
Rendered between each input, except after the last one. |
When providing a custom Input slot, do not override the following props: ref, value, onChange, onFocus, onKeyDown, onPaste, type, maxLength, id, data-testid, or aria-label.
These are managed internally by the ReactOtp component. Overriding them may cause broken focus handling, incorrect value updates, or other unexpected behavior.
- The component is controlled — you must pass
valueand update it viaonChangefor the UI to reflect changes. onChangereceives the full concatenated string, e.g."1234".
Minimal CSS is included in src/component/react-otp/ReactOtp.css — the primary class names are:
.otp-container— wrapper element..otp-input— default class applied to each input element.
You can style the component by:
- Passing slot components that render library inputs or custom inputs.
- Overriding or extending
.otp-inputin your app CSS. - Wrapping the component and applying utilities (Tailwind) to the custom
Containerslot.
Example CSS (already included):
.otp-container {
display: flex;
gap: 10px;
align-items: center;
justify-content: center;
}
.otp-input {
width: 40px;
height: 40px;
text-align: center;
border-radius: 8px;
border: 1px solid #bebebe;
}
.otp-input:focus {
border-color: var(--otp-color, #007bff);
outline: none;
}- Each input receives an
aria-labelin the formatOTP input {index + 1} of {length}by default. You can replace inputs via slots and provide your own accessible labels if you prefer. autoComplete="off"is set on inputs to avoid unwanted autofill. If you need browser or SMS autofill support, consider addingautoComplete="one-time-code"on the first input in a customInputslot.
We welcome contributions from the community!
You can contribute in two ways:
Feel free to open issues and pull requests!