Input OTP
Free, copy-and-go Input OTP components built on the SevenUI Input OTP primitive.Read the primitive docs.
28px slots for dense tables and toolbars.
32px slots that sit next to standard inputs.
44px slots for sign-in screens and touch devices.
"use client";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
const LENGTH = 6;
const positions = [1, 2, 3, 4, 5, 6];
const sizes = [
{
name: "compact",
label: "Compact",
hint: "28px slots for dense tables and toolbars.",
slot: "size-7 text-xs",
},
{
name: "default",
label: "Default",
hint: "32px slots that sit next to standard inputs.",
slot: "",
},
{
name: "large",
label: "Large",
hint: "44px slots for sign-in screens and touch devices.",
slot: "size-11 text-lg font-medium first:rounded-l-xl last:rounded-r-xl",
},
];
export default function InputOtp01() {
return (
<div className="flex w-full max-w-sm flex-col gap-6">
{sizes.map((size) => (
<Field key={size.name} name={`code-${size.name}`}>
<FieldLabel>{size.label}</FieldLabel>
<InputOTP length={LENGTH} autoComplete="one-time-code">
<InputOTPGroup
className={size.name === "large" ? "rounded-xl" : undefined}
>
{positions.map((position) => (
<InputOTPSlot
key={position}
aria-label={position === 1 ? undefined : `Digit ${position} of ${LENGTH}`}
className={size.slot}
/>
))}
</InputOTPGroup>
</InputOTP>
<FieldDescription>{size.hint}</FieldDescription>
</Field>
))}
</div>
);
}
npx shadcn@latest add @sevenui/component/input-otp-01pnpm dlx shadcn@latest add @sevenui/component/input-otp-01yarn dlx shadcn@latest add @sevenui/component/input-otp-01bunx --bun shadcn@latest add @sevenui/component/input-otp-01Joined cells read as one control.
Separate boxes give each digit room to breathe.
Muted fills without borders for quiet surfaces.
A single stroke per digit, like a paper form.
"use client";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
const LENGTH = 6;
const positions = [1, 2, 3, 4, 5, 6];
const styles = [
{
name: "segmented",
label: "Segmented",
hint: "Joined cells read as one control.",
group: "",
slot: "",
},
{
name: "detached",
label: "Detached",
hint: "Separate boxes give each digit room to breathe.",
group: "gap-2",
slot: "rounded-lg border first:rounded-lg last:rounded-lg",
},
{
name: "soft",
label: "Soft",
hint: "Muted fills without borders for quiet surfaces.",
group: "gap-2",
slot: "rounded-lg border border-transparent bg-muted first:rounded-lg last:rounded-lg dark:bg-muted",
},
{
name: "underline",
label: "Underline",
hint: "A single stroke per digit, like a paper form.",
group: "gap-3",
slot: "rounded-none border-0 border-b-2 bg-transparent first:rounded-none first:border-l-0 last:rounded-none focus:ring-0 dark:bg-transparent",
},
];
export default function InputOtp02() {
return (
<div className="flex w-full max-w-xs flex-col gap-6">
{styles.map((style) => (
<Field key={style.name} name={`code-${style.name}`}>
<FieldLabel>{style.label}</FieldLabel>
<InputOTP length={LENGTH} defaultValue="4829">
<InputOTPGroup className={style.group}>
{positions.map((position) => (
<InputOTPSlot
key={position}
aria-label={position === 1 ? undefined : `Digit ${position} of ${LENGTH}`}
className={style.slot}
/>
))}
</InputOTPGroup>
</InputOTP>
<FieldDescription>{style.hint}</FieldDescription>
</Field>
))}
</div>
);
}
npx shadcn@latest add @sevenui/component/input-otp-02pnpm dlx shadcn@latest add @sevenui/component/input-otp-02yarn dlx shadcn@latest add @sevenui/component/input-otp-02bunx --bun shadcn@latest add @sevenui/component/input-otp-02Required for transfers above $5,000. Digits stay hidden unless you reveal them.
"use client";
import { Eye, EyeOff } from "lucide-react";
import { useState } from "react";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Toggle } from "@/components/ui/toggle";
const LENGTH = 4;
const positions = [1, 2, 3, 4];
export default function InputOtp03() {
const [revealed, setRevealed] = useState(false);
return (
<div className="w-full max-w-xs">
<Field name="approvalPin">
<div className="flex items-center justify-between gap-3">
<FieldLabel>Payout approval PIN</FieldLabel>
<Toggle
size="sm"
pressed={revealed}
onPressedChange={setRevealed}
aria-label={revealed ? "Hide PIN" : "Show PIN"}
>
{revealed ? (
<EyeOff aria-hidden="true" />
) : (
<Eye aria-hidden="true" />
)}
</Toggle>
</div>
<InputOTP
length={LENGTH}
mask={!revealed}
defaultValue="73"
autoComplete="off"
>
<InputOTPGroup className="gap-3">
{positions.map((position) => (
<InputOTPSlot
key={position}
aria-label={position === 1 ? undefined : `PIN digit ${position} of ${LENGTH}`}
className="size-12 rounded-full border text-lg font-medium first:rounded-full last:rounded-full"
/>
))}
</InputOTPGroup>
</InputOTP>
<FieldDescription>
Required for transfers above $5,000. Digits stay hidden unless you
reveal them.
</FieldDescription>
</Field>
</div>
);
}
npx shadcn@latest add @sevenui/component/input-otp-03pnpm dlx shadcn@latest add @sevenui/component/input-otp-03yarn dlx shadcn@latest add @sevenui/component/input-otp-03bunx --bun shadcn@latest add @sevenui/component/input-otp-03Use one of the 8-character codes you saved when you turned on two-step verification, like K7QD-2MXP. Letters are not case sensitive.
"use client";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
const GROUP_SIZE = 4;
const LENGTH = GROUP_SIZE * 2;
// Recovery codes are case-insensitive, so show them the way they are printed.
function toUpperCase(value: string) {
return value.toUpperCase();
}
function renderSlots(offset: number) {
return Array.from({ length: GROUP_SIZE }).map((_, index) => {
const position = offset + index + 1;
return (
<InputOTPSlot
key={position}
aria-label={position === 1 ? undefined : `Character ${position} of ${LENGTH}`}
className="h-10 w-8 font-mono text-sm uppercase sm:w-9"
/>
);
});
}
export default function InputOtp04() {
return (
<div className="w-full max-w-sm">
<Field name="recoveryCode">
<FieldLabel>Recovery code</FieldLabel>
<InputOTP
length={LENGTH}
validationType="alphanumeric"
normalizeValue={toUpperCase}
autoComplete="off"
className="gap-1.5"
>
<InputOTPGroup>{renderSlots(0)}</InputOTPGroup>
<InputOTPSeparator className="text-muted-foreground" />
<InputOTPGroup>{renderSlots(GROUP_SIZE)}</InputOTPGroup>
</InputOTP>
<FieldDescription>
Use one of the 8-character codes you saved when you turned on
two-step verification, like K7QD-2MXP. Letters are not case
sensitive.
</FieldDescription>
</Field>
</div>
);
}
npx shadcn@latest add @sevenui/component/input-otp-04pnpm dlx shadcn@latest add @sevenui/component/input-otp-04yarn dlx shadcn@latest add @sevenui/component/input-otp-04bunx --bun shadcn@latest add @sevenui/component/input-otp-04Waiting for the code from your app.
Share this pairing code with the TV.
Locked for 5 minutes after 3 attempts.
Phone number confirmed.
"use client";
import { CircleCheck, Lock } from "lucide-react";
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
const LENGTH = 6;
const positions = [1, 2, 3, 4, 5, 6];
function Slots({ className }: { className?: string }) {
return (
<InputOTPGroup>
{positions.map((position) => (
<InputOTPSlot
key={position}
aria-label={position === 1 ? undefined : `Digit ${position} of ${LENGTH}`}
className={className}
/>
))}
</InputOTPGroup>
);
}
export default function InputOtp05() {
return (
<div className="grid w-full max-w-md gap-x-8 gap-y-6 sm:grid-cols-2">
<Field name="codeEmpty">
<FieldLabel>Empty</FieldLabel>
<InputOTP length={LENGTH}>
<Slots />
</InputOTP>
<FieldDescription>Waiting for the code from your app.</FieldDescription>
</Field>
<Field name="codeReadOnly">
<FieldLabel>Read-only</FieldLabel>
<InputOTP length={LENGTH} defaultValue="482913" readOnly>
<Slots className="bg-muted/50 dark:bg-muted/50" />
</InputOTP>
<FieldDescription>Share this pairing code with the TV.</FieldDescription>
</Field>
<Field name="codeDisabled" disabled>
<FieldLabel>Disabled</FieldLabel>
<InputOTP length={LENGTH} defaultValue="19" disabled>
<Slots />
</InputOTP>
<FieldDescription className="flex items-center gap-1.5">
<Lock aria-hidden="true" className="size-3.5 shrink-0" />
Locked for 5 minutes after 3 attempts.
</FieldDescription>
</Field>
<Field name="codeInvalid" invalid>
<FieldLabel>Error</FieldLabel>
<InputOTP length={LENGTH} defaultValue="550271">
<Slots />
</InputOTP>
<FieldError>That code expired. Request a new one.</FieldError>
</Field>
<Field name="codeVerified" className="sm:col-span-2">
<FieldLabel>Verified</FieldLabel>
<InputOTP length={LENGTH} defaultValue="306148" readOnly>
<Slots className="border-success/50 bg-success/5 text-foreground dark:bg-success/10" />
</InputOTP>
<FieldDescription className="flex items-center gap-1.5 text-success">
<CircleCheck aria-hidden="true" className="size-3.5 shrink-0" />
Phone number confirmed.
</FieldDescription>
</Field>
</div>
);
}
npx shadcn@latest add @sevenui/component/input-otp-05pnpm dlx shadcn@latest add @sevenui/component/input-otp-05yarn dlx shadcn@latest add @sevenui/component/input-otp-05bunx --bun shadcn@latest add @sevenui/component/input-otp-056 digits to go. Sent to maya@northwind.io.
"use client";
import { useState } from "react";
import { cn } from "cn";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
const LENGTH = 6;
const positions = [1, 2, 3, 4, 5, 6];
export default function InputOtp06() {
const [value, setValue] = useState("");
const remaining = LENGTH - value.length;
return (
<div className="w-full max-w-xs">
<Field name="emailCode">
<FieldLabel>Email confirmation code</FieldLabel>
<InputOTP
length={LENGTH}
value={value}
onValueChange={setValue}
autoComplete="one-time-code"
>
<InputOTPGroup className="gap-2">
{positions.map((position) => (
<InputOTPSlot
key={position}
aria-label={position === 1 ? undefined : `Digit ${position} of ${LENGTH}`}
className="h-11 w-10 rounded-lg border text-base font-medium first:rounded-lg last:rounded-lg data-filled:border-primary/40 data-filled:bg-primary/5 motion-safe:transition-[background-color,border-color,box-shadow,transform] motion-safe:duration-200 motion-safe:ease-out motion-safe:data-filled:-translate-y-0.5 dark:data-filled:bg-primary/10"
/>
))}
</InputOTPGroup>
</InputOTP>
<div aria-hidden="true" className="flex gap-2">
{positions.map((position) => (
<span
key={position}
className="h-1 w-10 overflow-hidden rounded-full bg-muted"
>
<span
className={cn(
"block h-full origin-left rounded-full bg-primary motion-safe:transition-transform motion-safe:duration-300 motion-safe:ease-out",
position <= value.length ? "scale-x-100" : "scale-x-0",
)}
/>
</span>
))}
</div>
<FieldDescription aria-live="polite">
{remaining === 0
? "All 6 digits entered."
: `${remaining} ${remaining === 1 ? "digit" : "digits"} to go. Sent to maya@northwind.io.`}
</FieldDescription>
</Field>
</div>
);
}
npx shadcn@latest add @sevenui/component/input-otp-06pnpm dlx shadcn@latest add @sevenui/component/input-otp-06yarn dlx shadcn@latest add @sevenui/component/input-otp-06bunx --bun shadcn@latest add @sevenui/component/input-otp-06Waiting for the text message…
Sent to (415) •••-0192. On a phone, the keyboard offers the code as soon as it arrives.
"use client";
import { CircleCheck, MessageSquareText } from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Spinner } from "@/components/ui/spinner";
const LENGTH = 6;
const SMS_CODE = "604218";
const SMS_DELAY_MS = 1800;
const positions = [1, 2, 3, 4, 5, 6];
export default function InputOtp07() {
const [value, setValue] = useState("");
const [arrived, setArrived] = useState(false);
const [confirmed, setConfirmed] = useState(false);
// Simulate the text message arriving shortly after the code was sent.
useEffect(() => {
if (arrived) {
return;
}
const timeout = window.setTimeout(() => setArrived(true), SMS_DELAY_MS);
return () => window.clearTimeout(timeout);
}, [arrived]);
const autofilled = value === SMS_CODE;
function handleChange(next: string) {
setValue(next);
setConfirmed(false);
}
return (
<div className="flex w-full max-w-xs flex-col gap-5">
<div
aria-live="polite"
className="flex min-h-16 items-center gap-3 rounded-xl border bg-card p-3 text-card-foreground shadow-sm"
>
{!arrived ? (
<p className="flex items-center gap-2 text-sm text-muted-foreground">
<Spinner aria-hidden="true" role="presentation" />
Waiting for the text message…
</p>
) : autofilled ? (
<p className="flex items-center gap-2 text-sm text-muted-foreground">
<CircleCheck aria-hidden="true" className="size-4 text-success" />
Code filled in from Messages.
</p>
) : (
<>
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted">
<MessageSquareText aria-hidden="true" className="size-4" />
</span>
<div className="flex min-w-0 flex-1 flex-col">
<p className="text-xs text-muted-foreground">Messages · now</p>
<p className="text-sm">
Lumen code:{" "}
<span className="font-medium tabular-nums">{SMS_CODE}</span>
</p>
</div>
<Button size="sm" onClick={() => handleChange(SMS_CODE)}>
Fill code
</Button>
</>
)}
</div>
<Field name="phoneCode">
<FieldLabel>Phone verification code</FieldLabel>
<InputOTP
length={LENGTH}
value={value}
onValueChange={handleChange}
autoComplete="one-time-code"
>
<InputOTPGroup className="w-full">
{positions.map((position) => (
<InputOTPSlot
key={position}
aria-label={
position === 1 ? undefined : `Digit ${position} of ${LENGTH}`
}
className="h-11 min-w-0 flex-1 text-lg tabular-nums"
/>
))}
</InputOTPGroup>
</InputOTP>
<FieldDescription aria-live="polite">
{confirmed
? "Phone number confirmed. Sign-in alerts will go to this number."
: "Sent to (415) •••-0192. On a phone, the keyboard offers the code as soon as it arrives."}
</FieldDescription>
</Field>
<Button
disabled={value.length < LENGTH || confirmed}
onClick={() => setConfirmed(true)}
>
{confirmed ? "Confirmed" : "Confirm phone number"}
</Button>
</div>
);
}
npx shadcn@latest add @sevenui/component/input-otp-07pnpm dlx shadcn@latest add @sevenui/component/input-otp-07yarn dlx shadcn@latest add @sevenui/component/input-otp-07bunx --bun shadcn@latest add @sevenui/component/input-otp-07Production secret key
Created Aug 12 by Priya Shah. Last used 2 hours ago.
ak_live_••••••••••••••••Enter a code from your authenticator app. Demo code: 314159.
"use client";
import { Check, Copy, EyeOff, KeyRound } from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Spinner } from "@/components/ui/spinner";
const LENGTH = 6;
const VALID_CODE = "314159";
const VERIFY_DELAY_MS = 1200;
const SECRET_KEY = "ak_live_51Hq8vT2eKxPa0mR7cNw4Yd";
const positions = [1, 2, 3, 4, 5, 6];
type Status = "idle" | "verifying" | "error" | "revealed";
export default function InputOtp08() {
const [value, setValue] = useState("");
const [status, setStatus] = useState<Status>("idle");
const [copied, setCopied] = useState(false);
// Simulated server check; the timeout is cleared if the status changes.
useEffect(() => {
if (status !== "verifying") {
return;
}
const timeout = window.setTimeout(() => {
setStatus(value === VALID_CODE ? "revealed" : "error");
}, VERIFY_DELAY_MS);
return () => window.clearTimeout(timeout);
}, [status, value]);
// Reset the copy confirmation after a moment so the button can be reused.
useEffect(() => {
if (!copied) {
return;
}
const timeout = window.setTimeout(() => setCopied(false), 2000);
return () => window.clearTimeout(timeout);
}, [copied]);
// Verify on every complete value, not only on the first completion, so
// fixing one digit after an error submits the corrected code again.
function handleChange(next: string) {
setValue(next);
setStatus(next.length === LENGTH ? "verifying" : "idle");
}
function hideKey() {
setValue("");
setCopied(false);
setStatus("idle");
}
async function copyKey() {
try {
await navigator.clipboard.writeText(SECRET_KEY);
setCopied(true);
} catch {
setCopied(false);
}
}
const revealed = status === "revealed";
return (
<div className="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground">
<div className="flex flex-col gap-1">
<h2 className="flex items-center gap-2 font-semibold">
<KeyRound aria-hidden="true" className="size-4" />
Production secret key
</h2>
<p className="text-sm text-muted-foreground">
Created Aug 12 by Priya Shah. Last used 2 hours ago.
</p>
</div>
<div className="flex items-center gap-1 rounded-md border bg-muted/50 py-1 pr-1 pl-2.5">
<code className="min-w-0 flex-1 truncate font-mono text-xs">
{revealed ? SECRET_KEY : `ak_live_${"•".repeat(16)}`}
</code>
{revealed ? (
<>
<Button
variant="ghost"
size="icon-xs"
aria-label={copied ? "Secret key copied" : "Copy secret key"}
onClick={copyKey}
>
{copied ? <Check aria-hidden="true" /> : <Copy aria-hidden="true" />}
</Button>
<Button
variant="ghost"
size="icon-xs"
aria-label="Hide secret key"
onClick={hideKey}
>
<EyeOff aria-hidden="true" />
</Button>
</>
) : null}
</div>
{revealed ? (
<p role="status" className="text-sm text-muted-foreground">
Visible for this session only. Store it in your secrets manager, not
in source control.
</p>
) : (
<Field name="revealCode" invalid={status === "error"}>
<FieldLabel>Confirm it’s you to reveal</FieldLabel>
<InputOTP
length={LENGTH}
value={value}
onValueChange={handleChange}
readOnly={status === "verifying"}
autoComplete="one-time-code"
aria-busy={status === "verifying"}
>
<InputOTPGroup>
{positions.map((position) => (
<InputOTPSlot
key={position}
aria-label={
position === 1
? undefined
: `Digit ${position} of ${LENGTH}`
}
className="size-9 tabular-nums"
/>
))}
</InputOTPGroup>
</InputOTP>
<div aria-live="polite" className="min-h-5 text-sm">
{status === "idle" ? (
<FieldDescription>
Enter a code from your authenticator app. Demo code:{" "}
{VALID_CODE}.
</FieldDescription>
) : null}
{status === "verifying" ? (
<p className="flex items-center gap-2 text-muted-foreground">
<Spinner aria-hidden="true" role="presentation" />
Checking code…
</p>
) : null}
</div>
{status === "error" ? (
<FieldError>
That code didn’t match. Codes refresh every 30 seconds.
</FieldError>
) : null}
</Field>
)}
</div>
);
}
npx shadcn@latest add @sevenui/component/input-otp-08pnpm dlx shadcn@latest add @sevenui/component/input-otp-08yarn dlx shadcn@latest add @sevenui/component/input-otp-08bunx --bun shadcn@latest add @sevenui/component/input-otp-08"use client";
import { useEffect, useState } from "react";
import { CircleCheck, Smartphone } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/spinner";
const CODE_LENGTH = 6;
const RECOVERY_LENGTH = 8;
type Status = "idle" | "verifying" | "verified";
export default function InputOtp09() {
const [code, setCode] = useState("");
const [trustDevice, setTrustDevice] = useState(true);
const [status, setStatus] = useState<Status>("idle");
const [recovery, setRecovery] = useState(false);
useEffect(() => {
if (status !== "verifying") return;
const timeout = setTimeout(() => setStatus("verified"), 1200);
return () => clearTimeout(timeout);
}, [status]);
const length = recovery ? RECOVERY_LENGTH : CODE_LENGTH;
const half = length / 2;
const unit = recovery ? "Character" : "Digit";
const complete = code.length === length;
if (status === "verified") {
return (
<div className="flex w-full max-w-sm flex-col items-center gap-3 rounded-xl border bg-card p-6 text-center">
<CircleCheck aria-hidden="true" className="size-8 text-success" />
<div className="flex flex-col gap-1">
<p className="font-medium">Signed in as maya@northwind.io</p>
<p className="text-sm text-muted-foreground">
{trustDevice
? "We won't ask for a code on this browser for 30 days."
: "You'll be asked for a code next time you sign in."}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
setCode("");
setRecovery(false);
setStatus("idle");
}}
>
Sign out
</Button>
</div>
);
}
return (
<form
className="flex w-full max-w-sm flex-col gap-5 rounded-xl border bg-card p-6"
onSubmit={(event) => {
event.preventDefault();
if (complete) setStatus("verifying");
}}
>
<div className="flex flex-col gap-1.5">
<div className="flex size-9 items-center justify-center rounded-lg bg-muted">
<Smartphone aria-hidden="true" className="size-4" />
</div>
<h2 className="mt-2 font-semibold">Two-step verification</h2>
<p className="text-sm text-muted-foreground">
{recovery
? "Enter one of the 8-character recovery codes you saved when you set up two-step verification."
: "Open your authenticator app and enter the 6-digit code for Northwind."}
</p>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="sign-in-code">
{recovery ? "Recovery code" : "Authentication code"}
</Label>
<InputOTP
key={recovery ? "recovery" : "app"}
id="sign-in-code"
length={length}
value={code}
onValueChange={setCode}
validationType={recovery ? "alphanumeric" : "numeric"}
normalizeValue={recovery ? (value) => value.toUpperCase() : undefined}
autoComplete={recovery ? "off" : "one-time-code"}
disabled={status === "verifying"}
>
<InputOTPGroup>
{Array.from({ length: half }, (_, index) => index + 1).map(
(position) => (
<InputOTPSlot
key={position}
className={
recovery
? "h-10 w-8 font-mono text-sm uppercase sm:w-9"
: "size-10 text-base"
}
aria-label={
position === 1
? undefined
: `${unit} ${position} of ${length}`
}
/>
),
)}
</InputOTPGroup>
<InputOTPSeparator className="px-1 text-muted-foreground" />
<InputOTPGroup>
{Array.from({ length: half }, (_, index) => half + index + 1).map(
(position) => (
<InputOTPSlot
key={position}
className={
recovery
? "h-10 w-8 font-mono text-sm uppercase sm:w-9"
: "size-10 text-base"
}
aria-label={`${unit} ${position} of ${length}`}
/>
),
)}
</InputOTPGroup>
</InputOTP>
</div>
<Label className="gap-2 font-normal">
<Checkbox
checked={trustDevice}
onCheckedChange={(checked) => setTrustDevice(checked)}
/>
Trust this browser for 30 days
</Label>
<div className="flex flex-col gap-2">
<Button
type="submit"
disabled={!complete || status === "verifying"}
className="w-full"
>
{status === "verifying" ? <Spinner /> : null}
{status === "verifying" ? "Verifying" : "Verify and sign in"}
</Button>
<Button
type="button"
variant="link"
size="sm"
className="self-center"
disabled={status === "verifying"}
onClick={() => {
setCode("");
setRecovery((value) => !value);
}}
>
{recovery
? "Use your authenticator app instead"
: "Use a recovery code instead"}
</Button>
</div>
</form>
);
}
npx shadcn@latest add @sevenui/component/input-otp-09pnpm dlx shadcn@latest add @sevenui/component/input-otp-09yarn dlx shadcn@latest add @sevenui/component/input-otp-09bunx --bun shadcn@latest add @sevenui/component/input-otp-09- Account
- Verify email
- Workspace
Check your inbox
We sent a 4-digit code to jordan.lee@fieldnote.app. It expires in 10 minutes.
Didn't get it? You can request a new code in 0:30
"use client";
import { useEffect, useState } from "react";
import { CircleCheck, MailCheck } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const DEFAULT_EMAIL = "jordan.lee@fieldnote.app";
const COOLDOWN_SECONDS = 30;
const steps = ["Account", "Verify email", "Workspace"];
type Stage = "verify" | "edit" | "verified";
export default function InputOtp10() {
const [code, setCode] = useState("");
const [cooldown, setCooldown] = useState(COOLDOWN_SECONDS);
const [resends, setResends] = useState(0);
const [email, setEmail] = useState(DEFAULT_EMAIL);
const [draft, setDraft] = useState(DEFAULT_EMAIL);
const [stage, setStage] = useState<Stage>("verify");
const current = stage === "verified" ? 2 : 1;
useEffect(() => {
if (stage !== "verify" || cooldown <= 0) return;
const timeout = setTimeout(() => setCooldown((value) => value - 1), 1000);
return () => clearTimeout(timeout);
}, [cooldown, stage]);
return (
<div className="flex w-full max-w-md flex-col gap-6 rounded-xl border bg-card p-6">
<ol aria-label="Onboarding progress" className="flex items-center gap-2">
{steps.map((step, index) => (
<li
key={step}
aria-current={index === current ? "step" : undefined}
className="flex flex-1 flex-col gap-1.5"
>
<span
className={
index <= current
? "h-1 rounded-full bg-primary"
: "h-1 rounded-full bg-muted"
}
/>
<span
className={
index === current
? "text-xs font-medium"
: "text-xs text-muted-foreground"
}
>
{step}
</span>
</li>
))}
</ol>
{stage === "verified" ? (
<div role="status" className="flex flex-col gap-3">
<p className="flex items-center gap-2 font-semibold">
<CircleCheck aria-hidden="true" className="size-4 text-success" />
Email verified
</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">{email}</span> is
confirmed. Next, name your workspace.
</p>
<Button
variant="outline"
size="sm"
className="self-start"
onClick={() => {
setCode("");
setResends(0);
setCooldown(COOLDOWN_SECONDS);
setStage("verify");
}}
>
Start over
</Button>
</div>
) : null}
{stage === "edit" ? (
<form
className="flex flex-col gap-4"
onSubmit={(event) => {
event.preventDefault();
const next = draft.trim();
if (!next) return;
setEmail(next);
setCode("");
setResends(0);
setCooldown(COOLDOWN_SECONDS);
setStage("verify");
}}
>
<div className="flex flex-col gap-2">
<Label htmlFor="signup-email">Email address</Label>
<Input
id="signup-email"
type="email"
value={draft}
onChange={(event) => setDraft(event.target.value)}
autoComplete="email"
/>
</div>
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-between">
<Button
type="button"
variant="ghost"
onClick={() => setStage("verify")}
>
Cancel
</Button>
<Button type="submit" disabled={!draft.trim()}>
Send new code
</Button>
</div>
</form>
) : null}
{stage === "verify" ? (
<>
<div className="flex flex-col gap-1.5">
<h2 className="flex items-center gap-2 font-semibold">
<MailCheck aria-hidden="true" className="size-4" />
Check your inbox
</h2>
<p className="text-sm text-muted-foreground">
We sent a 4-digit code to{" "}
<span className="font-medium text-foreground">{email}</span>. It
expires in 10 minutes.
</p>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="email-code">Verification code</Label>
<InputOTP
id="email-code"
length={4}
value={code}
onValueChange={setCode}
className="gap-2"
>
{[1, 2, 3, 4].map((position) => (
<InputOTPGroup key={position}>
<InputOTPSlot
className="h-12 w-11 rounded-lg border text-lg font-medium"
aria-label={
position === 1 ? undefined : `Digit ${position} of 4`
}
/>
</InputOTPGroup>
))}
</InputOTP>
<p aria-live="polite" className="text-xs text-muted-foreground">
{cooldown > 0 ? (
<>
Didn't get it? You can request a new code in{" "}
<span className="tabular-nums">0:{String(cooldown).padStart(2, "0")}</span>
</>
) : (
<>
Didn't get it?{" "}
<button
type="button"
className="rounded-sm font-medium text-foreground underline underline-offset-4 outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
onClick={() => {
setCode("");
setResends((value) => value + 1);
setCooldown(COOLDOWN_SECONDS);
}}
>
Resend code
</button>
</>
)}
{resends > 0 && cooldown > 0 ? " · New code sent" : null}
</p>
</div>
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-between">
<Button
variant="ghost"
onClick={() => {
setDraft(email);
setStage("edit");
}}
>
Change email
</Button>
<Button
disabled={code.length < 4}
onClick={() => setStage("verified")}
>
Continue
</Button>
</div>
</>
) : null}
</div>
);
}
npx shadcn@latest add @sevenui/component/input-otp-10pnpm dlx shadcn@latest add @sevenui/component/input-otp-10yarn dlx shadcn@latest add @sevenui/component/input-otp-10bunx --bun shadcn@latest add @sevenui/component/input-otp-10Connect a device
Enter the code shown in your terminal to sign in the Launchpad CLI.
$ launchpad login ! First copy your one-time code: WDJB-MJHT Waiting for authorization…
8 letters. Codes expire 15 minutes after they're issued.
"use client";
import { useState } from "react";
import { Check, Terminal } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";
const DEVICE_CODE = "WDJBMJHT";
const scopes = [
{ name: "projects:read", description: "List projects and environments" },
{ name: "deployments:write", description: "Create and promote deployments" },
{ name: "logs:read", description: "Stream build and runtime logs" },
];
type Decision = "pending" | "authorized" | "denied";
export default function InputOtp11() {
const [code, setCode] = useState("");
const [decision, setDecision] = useState<Decision>("pending");
const complete = code.length === DEVICE_CODE.length;
const matches = code === DEVICE_CODE;
const invalid = complete && !matches;
return (
<div className="flex w-full max-w-md flex-col gap-5 rounded-xl border bg-card p-5">
<div className="flex flex-col gap-1.5">
<h2 className="font-semibold">Connect a device</h2>
<p className="text-sm text-muted-foreground">
Enter the code shown in your terminal to sign in the Launchpad CLI.
</p>
</div>
<div className="overflow-hidden rounded-lg border bg-muted/60 font-mono text-xs">
<div className="flex items-center gap-1.5 border-b px-3 py-2 text-muted-foreground">
<Terminal aria-hidden="true" className="size-3.5" />
zsh — ~/apps/storefront
</div>
<pre className="overflow-x-auto p-3 leading-relaxed whitespace-pre-wrap">
<span className="text-muted-foreground">$</span> launchpad login{"\n"}
<span className="text-muted-foreground">
! First copy your one-time code:
</span>{" "}
<span className="font-semibold">WDJB-MJHT</span>
{"\n"}
<span className="text-muted-foreground">
Waiting for authorization…
</span>
</pre>
</div>
{decision === "pending" ? (
<>
<div className="flex flex-col gap-2">
<Label htmlFor="device-code">Device code</Label>
<InputOTP
id="device-code"
length={DEVICE_CODE.length}
value={code}
onValueChange={setCode}
validationType="alpha"
normalizeValue={(value) => value.toUpperCase()}
autoComplete="off"
aria-describedby="device-code-hint"
className="w-full"
>
<InputOTPGroup className="min-w-0 flex-1">
{[1, 2, 3, 4].map((position) => (
<InputOTPSlot
key={position}
aria-invalid={invalid || undefined}
className="h-10 min-w-0 flex-1 font-mono text-base"
aria-label={
position === 1 ? undefined : `Character ${position} of 8`
}
/>
))}
</InputOTPGroup>
<InputOTPSeparator className="px-0.5 text-muted-foreground" />
<InputOTPGroup className="min-w-0 flex-1">
{[5, 6, 7, 8].map((position) => (
<InputOTPSlot
key={position}
aria-invalid={invalid || undefined}
className="h-10 min-w-0 flex-1 font-mono text-base"
aria-label={`Character ${position} of 8`}
/>
))}
</InputOTPGroup>
</InputOTP>
<p
id="device-code-hint"
aria-live="polite"
className={
invalid
? "text-xs text-destructive"
: "text-xs text-muted-foreground"
}
>
{invalid
? "That code doesn't match a waiting device. Check your terminal."
: "8 letters. Codes expire 15 minutes after they're issued."}
</p>
</div>
{matches ? (
<div className="flex flex-col gap-3 rounded-lg border p-3">
<p className="text-sm">
<span className="font-medium">Launchpad CLI</span> on
MacBook-Pro.local is requesting access to:
</p>
<ul className="flex flex-col gap-2">
{scopes.map((scope) => (
<li key={scope.name} className="flex items-start gap-2">
<Check
aria-hidden="true"
className="mt-0.5 size-3.5 shrink-0 text-muted-foreground"
/>
<div className="flex min-w-0 flex-col gap-0.5">
<Badge variant="outline" className="font-mono">
{scope.name}
</Badge>
<span className="text-xs text-muted-foreground">
{scope.description}
</span>
</div>
</li>
))}
</ul>
</div>
) : null}
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<Button
variant="outline"
onClick={() => setDecision("denied")}
>
Deny
</Button>
<Button
disabled={!matches}
onClick={() => setDecision("authorized")}
>
Authorize device
</Button>
</div>
</>
) : (
<div role="status" className="flex flex-col gap-3">
<p className="text-sm">
{decision === "authorized"
? "Device connected. You can return to your terminal — the CLI is now signed in as @priya."
: "Request denied. The CLI will stop waiting and no access was granted."}
</p>
<Button
variant="outline"
size="sm"
className="self-start"
onClick={() => {
setCode("");
setDecision("pending");
}}
>
Connect another device
</Button>
</div>
)}
</div>
);
}
npx shadcn@latest add @sevenui/component/input-otp-11pnpm dlx shadcn@latest add @sevenui/component/input-otp-11yarn dlx shadcn@latest add @sevenui/component/input-otp-11bunx --bun shadcn@latest add @sevenui/component/input-otp-11Enter passcode
Unlock Harbor to view your accounts
"use client";
import { useState } from "react";
import { Delete, Lock, ScanFace, Wallet } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
const PIN_LENGTH = 4;
const keys = [
{ digit: "1", letters: "" },
{ digit: "2", letters: "ABC" },
{ digit: "3", letters: "DEF" },
{ digit: "4", letters: "GHI" },
{ digit: "5", letters: "JKL" },
{ digit: "6", letters: "MNO" },
{ digit: "7", letters: "PQRS" },
{ digit: "8", letters: "TUV" },
{ digit: "9", letters: "WXYZ" },
];
export default function InputOtp12() {
const [pin, setPin] = useState("");
const [unlocked, setUnlocked] = useState(false);
const [showReset, setShowReset] = useState(false);
const press = (digit: string) => {
if (pin.length >= PIN_LENGTH) return;
const next = pin + digit;
setPin(next);
// Keypad presses set the value directly, so completion is checked here.
if (next.length === PIN_LENGTH) setUnlocked(true);
};
if (unlocked) {
return (
<div className="flex w-full max-w-75 flex-col gap-4 rounded-[2rem] border bg-card p-6">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Wallet aria-hidden="true" className="size-4" />
Everyday checking
</div>
<div className="flex flex-col gap-1">
<p className="text-sm text-muted-foreground">Available balance</p>
<p className="text-3xl font-semibold tabular-nums">$3,482.19</p>
</div>
<ul className="flex flex-col divide-y text-sm">
<li className="flex justify-between py-2">
<span>Blue Bottle Coffee</span>
<span className="tabular-nums">−$6.75</span>
</li>
<li className="flex justify-between py-2">
<span>Payroll · Acme Corp</span>
<span className="text-success tabular-nums">+$2,140.00</span>
</li>
</ul>
<Button
variant="outline"
onClick={() => {
setPin("");
setUnlocked(false);
}}
>
<Lock aria-hidden="true" data-icon="inline-start" />
Lock app
</Button>
</div>
);
}
return (
<div className="flex w-full max-w-75 flex-col items-center gap-8 rounded-[2rem] border bg-card px-6 pt-10 pb-6">
<div className="flex flex-col items-center gap-2 text-center">
<div className="flex size-11 items-center justify-center rounded-full bg-muted">
<Lock aria-hidden="true" className="size-5" />
</div>
<h2 id="passcode-label" className="font-semibold">
Enter passcode
</h2>
<p className="text-xs text-muted-foreground">
Unlock Harbor to view your accounts
</p>
</div>
<InputOTP
length={PIN_LENGTH}
mask
value={pin}
onValueChange={setPin}
onValueComplete={() => setUnlocked(true)}
aria-labelledby="passcode-label"
className="gap-4"
>
{[1, 2, 3, 4].map((position) => (
<InputOTPGroup key={position}>
<InputOTPSlot
aria-labelledby={position === 1 ? "passcode-label" : undefined}
aria-label={
position === 1
? undefined
: `Passcode digit ${position} of ${PIN_LENGTH}`
}
className="size-4 rounded-full border bg-muted text-[8px] text-transparent caret-transparent selection:bg-transparent data-filled:border-primary data-filled:bg-primary"
/>
</InputOTPGroup>
))}
</InputOTP>
<fieldset
aria-label="Passcode keypad"
className="m-0 grid w-full min-w-0 grid-cols-3 justify-items-center gap-3 border-0 p-0"
>
{keys.map((key) => (
<Button
key={key.digit}
variant="ghost"
aria-label={key.digit}
className="size-16 flex-col gap-0 rounded-full bg-muted/60 text-xl font-medium"
onClick={() => press(key.digit)}
>
{key.digit}
<span
aria-hidden="true"
className="h-3 text-[0.55rem] font-semibold tracking-widest text-muted-foreground"
>
{key.letters}
</span>
</Button>
))}
<Button
variant="ghost"
aria-label="Unlock with Face ID"
className="size-16 rounded-full"
onClick={() => setUnlocked(true)}
>
<ScanFace aria-hidden="true" className="size-6" />
</Button>
<Button
variant="ghost"
aria-label="0"
className="size-16 rounded-full bg-muted/60 text-xl font-medium"
onClick={() => press("0")}
>
0
</Button>
<Button
variant="ghost"
aria-label="Delete last digit"
className="size-16 rounded-full"
disabled={pin.length === 0}
onClick={() => setPin((current) => current.slice(0, -1))}
>
<Delete aria-hidden="true" className="size-6" />
</Button>
</fieldset>
{showReset ? (
<p role="status" className="text-center text-xs text-muted-foreground">
Sign in with your Harbor password to set a new passcode.
</p>
) : (
<Button variant="link" size="sm" onClick={() => setShowReset(true)}>
Forgot passcode?
</Button>
)}
</div>
);
}
npx shadcn@latest add @sevenui/component/input-otp-12pnpm dlx shadcn@latest add @sevenui/component/input-otp-12yarn dlx shadcn@latest add @sevenui/component/input-otp-12bunx --bun shadcn@latest add @sevenui/component/input-otp-12Authenticator app
Require a code from 1Password, Authy, or Google Authenticator when you sign in.
1. Scan the QR code with your authenticator app.
Can't scan? Enter this setup key manually:
JBSW Y3DP EHPK 3PXP"use client";
import { useEffect, useState } from "react";
import { Check, Copy, KeyRound, ShieldCheck } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";
const SETUP_KEY = "JBSW Y3DP EHPK 3PXP";
const recoveryCodes = [
"7f3k-92qd",
"m2px-4c8h",
"q9rt-1bzn",
"v6ws-8je3",
"k4an-7ty2",
"h8ue-5lm0",
];
type Step = "scan" | "verify" | "done";
export default function InputOtp13() {
const [enabled, setEnabled] = useState(false);
const [step, setStep] = useState<Step>("scan");
const [code, setCode] = useState("");
const [copied, setCopied] = useState(false);
const [saved, setSaved] = useState(false);
// Reset the copy confirmation after a moment so the button can be reused.
useEffect(() => {
if (!copied) return;
const timeout = setTimeout(() => setCopied(false), 2000);
return () => clearTimeout(timeout);
}, [copied]);
const copyKey = async () => {
try {
await navigator.clipboard.writeText(SETUP_KEY.replace(/\s/g, ""));
setCopied(true);
} catch {
setCopied(false);
}
};
return (
<section
aria-labelledby="two-factor-title"
className="flex w-full max-w-md flex-col gap-5 rounded-xl border bg-card p-5"
>
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-1">
<h2 id="two-factor-title" className="font-semibold">
Authenticator app
</h2>
<p className="text-sm text-muted-foreground">
Require a code from 1Password, Authy, or Google Authenticator when
you sign in.
</p>
</div>
<Badge variant={enabled ? "default" : "outline"}>
{enabled ? "On" : "Off"}
</Badge>
</div>
{step === "scan" ? (
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-4 sm:flex-row sm:items-start">
<img
src="/placeholder.svg"
alt="QR code for adding Relay to your authenticator app"
width={128}
height={128}
className="size-32 shrink-0 rounded-lg border bg-muted object-cover"
/>
<div className="flex w-full min-w-0 flex-col gap-2 text-sm">
<p>
<span className="font-medium">1.</span> Scan the QR code with
your authenticator app.
</p>
<p className="text-muted-foreground">
Can't scan? Enter this setup key manually:
</p>
<div className="flex items-center gap-1 rounded-md border bg-muted/50 py-1 pr-1 pl-2.5">
<code className="min-w-0 flex-1 truncate font-mono text-xs">
{SETUP_KEY}
</code>
<Button
variant="ghost"
size="icon-xs"
aria-label={copied ? "Setup key copied" : "Copy setup key"}
onClick={copyKey}
>
{copied ? (
<Check aria-hidden="true" />
) : (
<Copy aria-hidden="true" />
)}
</Button>
</div>
</div>
</div>
<Button className="self-end" onClick={() => setStep("verify")}>
Next: enter code
</Button>
</div>
) : null}
{step === "verify" ? (
<form
className="flex flex-col gap-4"
onSubmit={(event) => {
event.preventDefault();
if (code.length === 6) setStep("done");
}}
>
<div className="flex flex-col gap-2">
<Label htmlFor="setup-code">
2. Enter the 6-digit code shown for Relay
</Label>
<InputOTP
id="setup-code"
length={6}
value={code}
onValueChange={setCode}
aria-describedby="setup-code-hint"
>
<InputOTPGroup>
{[1, 2, 3].map((position) => (
<InputOTPSlot
key={position}
className="size-10 text-base"
aria-label={
position === 1 ? undefined : `Digit ${position} of 6`
}
/>
))}
</InputOTPGroup>
<InputOTPSeparator className="px-1 text-muted-foreground" />
<InputOTPGroup>
{[4, 5, 6].map((position) => (
<InputOTPSlot
key={position}
className="size-10 text-base"
aria-label={`Digit ${position} of 6`}
/>
))}
</InputOTPGroup>
</InputOTP>
<p id="setup-code-hint" className="text-xs text-muted-foreground">
Codes refresh every 30 seconds. Enter the current one.
</p>
</div>
<div className="flex justify-between gap-2">
<Button
type="button"
variant="ghost"
onClick={() => {
setCode("");
setStep("scan");
}}
>
Back
</Button>
<Button type="submit" disabled={code.length < 6}>
Verify code
</Button>
</div>
</form>
) : null}
{step === "done" ? (
<div className="flex flex-col gap-4">
<p
role="status"
className="flex items-center gap-2 text-sm font-medium"
>
<ShieldCheck aria-hidden="true" className="size-4 text-success" />
Code accepted. Save your recovery codes to finish.
</p>
<div className="flex flex-col gap-2 rounded-lg border bg-muted/40 p-3">
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
<KeyRound aria-hidden="true" className="size-3.5" />
Each code works once if you lose your phone.
</p>
<ul className="grid grid-cols-2 gap-x-4 gap-y-1 font-mono text-sm tabular-nums">
{recoveryCodes.map((recoveryCode) => (
<li key={recoveryCode}>{recoveryCode}</li>
))}
</ul>
</div>
<Label className="gap-2 font-normal">
<Checkbox
checked={saved}
onCheckedChange={(checked) => setSaved(checked)}
/>
I've stored these codes somewhere safe
</Label>
<Button
className="self-end"
disabled={!saved || enabled}
onClick={() => setEnabled(true)}
>
{enabled ? "Two-factor enabled" : "Turn on two-factor"}
</Button>
</div>
) : null}
</section>
);
}
npx shadcn@latest add @sevenui/component/input-otp-13pnpm dlx shadcn@latest add @sevenui/component/input-otp-13yarn dlx shadcn@latest add @sevenui/component/input-otp-13bunx --bun shadcn@latest add @sevenui/component/input-otp-13Nora Pham
Parcelwise Support · usually replies in 2 min
Nora will never ask for your password.
"use client";
import { useState } from "react";
import { BadgeCheck, ShieldCheck } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import { Button } from "@/components/ui/button";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import {
Message,
MessageAvatar,
MessageContent,
MessageGroup,
} from "@/components/ui/message";
const history = [
{
id: "m1",
from: "customer",
text: "Hi, I need to change the shipping address on order PW-48213.",
},
{
id: "m2",
from: "agent",
text: "Happy to help. Before I open account details, I need to confirm it's you.",
},
] as const;
function AgentAvatar() {
return (
<MessageAvatar>
<Avatar size="sm">
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback>NP</AvatarFallback>
</Avatar>
</MessageAvatar>
);
}
export default function InputOtp14() {
const [code, setCode] = useState("");
const [verified, setVerified] = useState(false);
return (
<div className="flex w-full max-w-sm flex-col overflow-hidden rounded-xl border bg-card">
<div className="flex items-center gap-3 border-b px-4 py-3">
<Avatar>
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback>NP</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-col">
<p className="text-sm font-medium">Nora Pham</p>
<p className="text-xs text-muted-foreground">
Parcelwise Support · usually replies in 2 min
</p>
</div>
</div>
<MessageGroup
role="log"
aria-label="Support conversation"
aria-live="polite"
className="gap-3 p-4"
>
{history.map((message) =>
message.from === "agent" ? (
<Message key={message.id}>
<AgentAvatar />
<MessageContent>
<Bubble variant="muted">
<BubbleContent>{message.text}</BubbleContent>
</Bubble>
</MessageContent>
</Message>
) : (
<Message key={message.id} align="end">
<MessageContent>
<Bubble>
<BubbleContent>{message.text}</BubbleContent>
</Bubble>
</MessageContent>
</Message>
),
)}
<Message>
<AgentAvatar />
<MessageContent>
<div className="flex w-full max-w-64 flex-col gap-3 rounded-2xl border bg-background p-3">
<div className="flex items-center gap-2 text-sm font-medium">
<ShieldCheck aria-hidden="true" className="size-4" />
<span id="chat-code-label">Identity check</span>
</div>
<p
id="chat-code-hint"
className="text-xs text-muted-foreground"
>
Enter the 5-digit code texted to{" "}
<span className="whitespace-nowrap">(•••) •••-0455</span>.
</p>
<InputOTP
length={5}
value={code}
onValueChange={setCode}
disabled={verified}
aria-labelledby="chat-code-label"
aria-describedby="chat-code-hint"
className="w-full"
>
<InputOTPGroup className="w-full">
{[1, 2, 3, 4, 5].map((position) => (
<InputOTPSlot
key={position}
aria-labelledby={
position === 1 ? "chat-code-label" : undefined
}
aria-label={
position === 1 ? undefined : `Code digit ${position} of 5`
}
className="h-9 min-w-0 flex-1 bg-background text-base"
/>
))}
</InputOTPGroup>
</InputOTP>
{verified ? (
<p className="flex items-center gap-1.5 text-xs font-medium text-success">
<BadgeCheck aria-hidden="true" className="size-3.5" />
Verified
</p>
) : (
<Button
size="sm"
disabled={code.length < 5}
onClick={() => setVerified(true)}
>
Share code with Nora
</Button>
)}
</div>
</MessageContent>
</Message>
{verified ? (
<Message>
<AgentAvatar />
<MessageContent>
<Bubble variant="muted">
<BubbleContent>
Thanks, you're verified. Order PW-48213 hasn't shipped yet, so
I can update the address now. What's the new one?
</BubbleContent>
</Bubble>
</MessageContent>
</Message>
) : null}
</MessageGroup>
<div className="border-t p-3">
<p className="text-center text-xs text-muted-foreground">
{verified
? "Account details are now shared with this agent."
: "Nora will never ask for your password."}
</p>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/input-otp-14pnpm dlx shadcn@latest add @sevenui/component/input-otp-14yarn dlx shadcn@latest add @sevenui/component/input-otp-14bunx --bun shadcn@latest add @sevenui/component/input-otp-14