react-smart-otp
Version:
A lightweight, accessible, and slot based fully customizable React OTP input component
174 lines (129 loc) • 7.57 kB
Markdown
# React Smart Otp
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.).
# Demo
[Demo](https://codesandbox.io/p/sandbox/sp2jvf)
##### If you found this project helpful, a ⭐ on GitHub would mean a lot — it helps others discover it too!
[](https://github.com/amitpatil321/react-smart-otp)
## Key features
- Controlled API (pass `value` and `onChange`).
- Slots-based customization: replace `Container`, `Input`, or `Separator` elements.
- 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.
## Usage
### Simple Example
```tsx
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} />
}
```
### Using Slots (Shadcn)
```tsx
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
```
## ReactOtp Component Props
| 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. |
---
## Slots API
| 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. |
---
## ⚠️ Warning
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.
## Behavioral Contract
- The component is **controlled** — you must pass `value` and update it via `onChange` for the UI to reflect changes.
- `onChange` receives the **full concatenated string**, e.g. `"1234"`.
## Styling / Theming
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-input` in your app CSS.
- Wrapping the component and applying utilities (Tailwind) to the custom `Container` slot.
Example CSS (already included):
```css
.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;
}
```
## Accessibility
- Each input receives an `aria-label` in the format `OTP 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 adding `autoComplete="one-time-code"` on the first input in a custom `Input` slot.
## 🤝 Contributing
We welcome contributions from the community!
You can contribute in two ways:
Feel free to [open issues](https://github.com/amitpatil321/react-smart-otp/issues/new/choose) and [pull requests](https://github.com/amitpatil321/react-smart-otp/pulls)!
## License
[License](https://github.com/amitpatil321/react-smart-otp/blob/main/LICENSE)