OTP Input
beui/otp-inputFreeBlock
One-time-code input with a gliding focus ring, digits that roll in per slot, error shake and a success check draw.
Install it
npx shadcn@latest add https://beui.dev/r/otp-input.jsonSource
1"use client";2// beui.dev/components/blocks/otp-input34import {5 AnimatePresence,6 animate,7 motion,8 useReducedMotion,9} from "motion/react";10import { useEffect, useId, useRef, useState } from "react";11import { EASE_OUT } from "@/lib/ease";12import { cn } from "@/lib/utils";1314export type OTPStatus = "idle" | "error" | "success";1516export interface OTPInputProps {17 /** Number of slots. Default 6. */18 length?: number;19 value?: string;20 defaultValue?: string;21 onChange?: (value: string) => void;22 /** Fires once every slot is filled. */23 onComplete?: (value: string) => void;24 /** Optional label rendered above the slots. */25 label?: string;26 /** Helper text shown below the slots while idle. */27 hint?: string;28 /** Message shown below the slots when status is "success". */29 successMessage?: string;30 /** Message shown below the slots when status is "error". */31 errorMessage?: string;32 /** External validation feedback. "error" shakes, "success" draws a check. */33 status?: OTPStatus;34 /** Render dots instead of the typed digits. */35 mask?: boolean;36 disabled?: boolean;37 autoFocus?: boolean;38 /** Accessible label for the underlying input. */39 "aria-label"?: string;40 className?: string;41}4243export function OTPInput({44 length = 6,45 value: controlledValue,46 defaultValue = "",47 onChange,48 onComplete,49 label,50 hint,51 successMessage,52 errorMessage,53 status = "idle",54 mask = false,55 disabled = false,56 autoFocus = false,57 "aria-label": ariaLabel = "One-time passcode",58 className,59}: OTPInputProps) {60 const uid = useId();61 const reduce = useReducedMotion();62 const inputRef = useRef<HTMLInputElement>(null);63 const slotsRef = useRef<HTMLDivElement>(null);6465 const controlled = controlledValue !== undefined;6667 // Source of truth is a fixed-length array, so a cleared middle slot stays an68 // in-place hole instead of collapsing the digits after it to the left.69 const [slots, setSlots] = useState<string[]>(() =>70 toSlots(controlled ? controlledValue : defaultValue, length),71 );72 const [focused, setFocused] = useState(false);73 const [active, setActive] = useState(0);7475 const joined = slots.join("");76 const joinedRef = useRef(joined);7778 useEffect(() => {79 joinedRef.current = joined;80 }, [joined]);8182 // Pull in external value changes; skip when the parent is just echoing our own83 // onChange, so internal holes survive the controlled round-trip.84 useEffect(() => {85 if (!controlled) return;86// … truncated
What it pulls in
npm packages
Files it writes
More from beui
All 104 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| File Upload Attachment Uploadattachment-upload | beui | Blocks | Free | 4 deps · 5 files | |
| Availability Scheduleravailability-scheduler | beui | Blocks | Free | 4 deps · 15 files | |
| Bloom Menubloom-menu | beui | Blocks | Free | 4 deps · 3 files | |
| Command Palettecommand-palette | beui | Blocks | Free | 4 deps · 3 files | |
| Dynamic Islanddynamic-island | beui | Blocks | Free | 3 deps · 3 files | |
| Expandable Action Barexpandable-action-bar | beui | Blocks | Free | 3 deps · 2 files | |
| Expandable Tabsexpandable-tabs | beui | Blocks | Free | 3 deps · 3 files | |
| Feedback Widgetfeedback-widget | beui | Blocks | Free | 4 deps · 9 files |
