Use input-otp with shadcn/ui
masterIf you are using shadcn/ui, you can add the pre-composed input-otp component which wraps the core library and uses <InputOTPSlot /> instead of a render prop.
npx shadcn@latest add input-otprepository·master·Indexed 25 days ago
https://github.com/guilhermerodz/input-otpAn accessible, unstyled, and fully featured one-time-password (OTP) component for React. It utilizes a single invisible text input to maintain native browser behaviors such as SMS autofill, screen reader support, and standard keyboard interactions while providing developers the flexibility to render custom UI for OTP slots via a render prop or the OTPInputContext.
If you are using shadcn/ui, you can add the pre-composed input-otp component which wraps the core library and uses <InputOTPSlot /> instead of a render prop.
npx shadcn@latest add input-otpTo start the development server for the playground, use one of the following package manager commands. Once running, you can view the playground at http://localhost:3000.
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun devTo start the development server for the website project, use one of the following package manager commands. Once running, you can view the site at http://localhost:3000. You can edit app/page.tsx to modify the landing page content.
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```埋The /api/refresh-stats route uses a CRON_SECRET to prevent unauthorized users from triggering cache invalidations and outbound requests to npm-stat.
openssl rand -hex 32vercel env add CRON_SECRET production| Environment | CRON_SECRET | Behaviour |
|---|---|---|
| Local | unset | Open, so you can call it while developing |
| Local | set | Enforced, same as production |
| Production | unset | 503 CRON_SECRET is not configured |
| Production | set | 401 unless the bearer token matches |
openssl rand -hex 32Install the input-otp package via npm to use the accessible, unstyled one-time-password component for React.
npm install input-otpThe default cron schedule is 0 0,12 * * * (twice daily). Because Vercel Hobby plans are limited to one cron trigger per day, you must modify vercel.json to use a daily schedule if you are on a Hobby plan:
// vercel.json
{
"crons": [
{
"schedule": "0 0 * * *"
}
]
}To use OTPInput, specify the maxLength (number of slots) and provide a render function. The render function receives an object containing slots, which you can map over to draw your custom UI. Each slot provides the necessary state to render the character, placeholder, active status, and fake caret.
'use client'
import { OTPInput } from 'input-otp'
export function VerificationCode() {
return (
<OTPInput
maxLength={6}
containerClassName="group flex items-center"
render={({ slots }) => (
<div className="flex">
{slots.map((slot, idx) => (
<Slot key={idx} {...slot} />
))}
</div>
)}
/>
)
}The OTPInput component accepts the following props:
| Prop | Type | Description |
|---|---|---|
maxLength | number | Required. The number of slots. |
render | (props: RenderProps) => React.ReactNode | Function to render the slots. |
children | React.ReactNode | Alternative to render; allows composing via OTPInputContext. |
value | string | Controlled value. |
onChange | (newValue: string) => unknown | Callback when value changes (returns the string, not an event). |
onComplete | (value: string) => unknown | Fires once when the input reaches maxLength. |
pattern | string | RegExp | Gates every change; no default. |
placeholder | string | Per-slot placeholder characters. |
pasteTransformer | (pasted: string) => string | Function to transform pasted text. |
containerClassName | string | Class name for the visible wrapper. |
className | string | Class name for the invisible real input. |
textAlign | 'left' | 'center' | 'right' | Text alignment (default: 'left'). |
inputMode | 'numeric' | 'text' | ... | Input mode (default: 'numeric'). |
pushPasswordManagerStrategy | 'increase-width' | 'none' | Strategy for handling password manager badges. |
noScriptCSSFallback | string | null | CSS for <noscript> fallback. |
nonce | string | For CSP style-src on injected <style> tags. |
All standard <input> attributes (e.g., name, required, disabled, autoFocus, aria-*, data-*) are forwarded to the real input. The ref points to the real input.
The SlotProps object is passed to the render function for each slot and contains the following properties:
char: string | null - The character in this slot.placeholderChar: string | null - The character to show if the slot is empty.isActive: boolean - Whether this slot is currently being edited.hasFakeCaret: boolean - Whether to show the custom fake caret (the real caret is transparent).The input-otp library does not render the individual cells of the OTP input. Instead, it provides the state (char, placeholderChar, isActive, and hasFakeCaret) to your custom components. You can use the following sub-components to build your own UI:
Slot: Represents a single visible cell. It receives props from input-otp to determine what to display and whether to show a fake caret.SlotGroup: A wrapper used to group multiple Slot components together (e.g., for creating segmented inputs).FakeCaret: A blinking bar component used to simulate a cursor, as the real input caret is typically made transparent in custom OTP implementations.FakeDash: A decorative component used to create visual separators (like dashes) between SlotGroup instances.OTPInputContext. The context provides a slots array, where each slot corresponds to a character position. This allows you to render individual boxes, circles, or any other UI element for each digit.When providing a render function to the OTP input, you receive RenderProps which contains an array of SlotProps. This allows for complete control over how each character slot is displayed.
interface SlotProps {
isActive: boolean
char: string | null
placeholderChar: string | null
hasFakeCaret: boolean
}
interface RenderProps {
slots: SlotProps[]
isFocused: boolean
isHovering: boolean
}
type InputOTPRenderFn = (props: RenderProps) => React.ReactNode