Forms
Input OTP.
A row of single-character slots for pairing codes and one-time passwords, with paste support.
Run lantern pair on your computer to get a code.
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
export default function InputOTPDemo() {
return (
<div className="grid gap-3">
<InputOTP maxLength={6} aria-label="Pairing code">
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<p className="text-xs text-muted-foreground">
Run <code className="font-mono text-primary">lantern pair</code> on your computer to get a code.
</p>
</div>
);
}Installation#
With the CLI
$npx lanterncn add input-otpThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/input-otp. See Installation if your project is not set up yet.
Manually
Install the dependencies
npm install input-otp lucide-reactCopy the source into your project
components/ui/input-otp.tsx"use client"; import * as React from "react"; import { OTPInput, OTPInputContext } from "input-otp"; import { MinusIcon } from "lucide-react"; import { cn } from "@/lib/utils"; function InputOTP({ className, containerClassName, ...props }: React.ComponentProps<typeof OTPInput> & { containerClassName?: string }) { return ( <OTPInput data-slot="input-otp" containerClassName={cn("flex items-center gap-2 has-disabled:opacity-50", containerClassName)} className={cn("disabled:cursor-not-allowed", className)} {...props} /> ); } function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="input-otp-group" className={cn("flex items-center", className)} {...props} />; } function InputOTPSlot({ index, className, ...props }: React.ComponentProps<"div"> & { index: number }) { const inputOTPContext = React.useContext(OTPInputContext); const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}; return ( <div data-slot="input-otp-slot" data-active={isActive} className={cn( "relative flex h-11 w-10 items-center justify-center border-y border-r border-input bg-background/40 font-mono text-base text-foreground transition-[border-color,box-shadow] first:rounded-l-md first:border-l last:rounded-r-md", "data-[active=true]:z-10 data-[active=true]:border-primary data-[active=true]:ring-2 data-[active=true]:ring-ring/25", "aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20", className, )} {...props} > {char} {hasFakeCaret && ( <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> <div className="h-5 w-2 animate-lantern-blink bg-primary" /> </div> )} </div> ); } function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) { return ( <div data-slot="input-otp-separator" role="separator" className="text-muted-foreground" {...props}> <MinusIcon className="size-4" /> </div> ); } export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };Update the import paths to match your project
The source imports
cnfrom@/lib/utils.
Usage#
import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "@/components/ui/input-otp";<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Built on the input-otp package by Guilherme Rodz. Use the pattern prop to limit which characters are accepted.
Examples#
With separator
6 characters left
"use client";
import * as React from "react";
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@/components/ui/input-otp";
export default function InputOTPWithSeparator() {
const [value, setValue] = React.useState("");
return (
<div className="grid gap-3">
<InputOTP
maxLength={6}
value={value}
onChange={(v) => setValue(v.toUpperCase())}
pattern="^[a-zA-Z0-9]+$"
aria-label="Hub invite code"
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<p className="font-mono text-[10px] tracking-[0.2em] text-muted-foreground uppercase">
{value.length === 6 ? "Code ready" : `${6 - value.length} characters left`}
</p>
</div>
);
}