Mischief

87 / Controls

OTP Input

A one time code, one box per character, where pasting the whole code into any box fills the rest.

We sent a code to your phone.

0 of 6

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/otp-input
import { OtpInput } from "mischief-ui/otp-input"

Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.

registry/default/otp-input/otp-input.tsx
"use client" import * as React from "react"import { cn } from "@/lib/utils" export type OtpInputProps = Omit<  React.HTMLAttributes<HTMLDivElement>,  "onChange" | "defaultValue"> & {  length?: number  value?: string  defaultValue?: string  onChange?: (value: string) => void  /** Called once the last box is filled. */

Usage

export function Verify() {
  const [code, setCode] = React.useState("")

  return (
    <OtpInput value={code} onChange={setCode} onComplete={submit} />
  )
}

Pasting is the normal case

People do not type these codes. They copy the whole thing from a message and paste it, and they paste it into whichever box happens to have focus. So a paste is caught wherever it lands, filtered to the characters the pattern allows, spread across the boxes from that point, and focus is left on the first box still empty.

The first box also carries the one time code autocomplete hint, which is what lets a phone offer the code straight from the message without anyone touching the clipboard at all.

The keys people actually press

  • Backspace on a filled box clears it and stays. On an empty box it clears the one before and moves back, which is what people expect after overshooting.
  • Left and right arrows move between boxes without changing anything.
  • Typing into a filled box replaces its character rather than being ignored.

Every box is named as a character and its position, so moving between them announces where you are rather than repeating the same label six times.

API

lengthnumberHow many boxes. Defaults to 6.
valuestringControlled value.
defaultValuestringUncontrolled starting value.
onChange(value: string) => voidThe code so far.
onComplete(value: string) => voidCalled once the last box is filled.
patternRegExpWhich characters are allowed, tested one at a time. Digits by default.
labelstringNames the group and each box. Defaults to "One time code".

Accessibility

The boxes are a named group and each is labelled with its position in it. The numeric keyboard is requested when the pattern is digits, and the first box carries the one time code hint so a phone can offer it. The filled state is shown with a border and a small change of scale rather than colour alone, and that scale change is removed under reduced motion.