Vela UI Docsvenizia.ai

Input OTP

Accessible one-time password component with copy paste functionality.

npx shadcn@latest add @velaui/input-otp
npx shadcn@latest add @velaui/input-otp

Examples

  • Default
  • Fill
  • With separator
  • Disabled
  • Invalid
  • Pattern

View code

Input OTP · Default

Installation

pnpm dlx velaui add @velaui/input-otp
pnpm dlx velaui add @velaui/input-otp

Code

input-otp-default.tsx
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";

export default function Example() {
  return (
    <div className="space-y-3">
      <Label>Outline</Label>
      <InputOTP maxLength={6}>
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
          <InputOTPSlot index={3} />
          <InputOTPSlot index={4} />
          <InputOTPSlot index={5} />
        </InputOTPGroup>
      </InputOTP>
    </div>
  );
}

View code

Input OTP · Fill

Installation

pnpm dlx velaui add @velaui/input-otp
pnpm dlx velaui add @velaui/input-otp

Code

input-otp-fill.tsx
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";

export default function Example() {
  return (
    <div className="space-y-3">
      <Label>Fill</Label>
      <InputOTP maxLength={4} variant="fill" defaultValue="1234">
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
          <InputOTPSlot index={3} />
        </InputOTPGroup>
      </InputOTP>
    </div>
  );
}

View code

Input OTP · With separator

Installation

pnpm dlx velaui add @velaui/input-otp
pnpm dlx velaui add @velaui/input-otp

Code

input-otp-with-separator.tsx
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
  InputOTPSeparator,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";

export default function Example() {
  return (
    <div className="space-y-3">
      <Label>With separator</Label>
      <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>
    </div>
  );
}

View code

Input OTP · Disabled

Installation

pnpm dlx velaui add @velaui/input-otp
pnpm dlx velaui add @velaui/input-otp

Code

input-otp-disabled.tsx
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";

export default function Example() {
  return (
    <div className="space-y-3">
      <Label>Disabled</Label>
      <InputOTP maxLength={4} disabled defaultValue="12">
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
          <InputOTPSlot index={3} />
        </InputOTPGroup>
      </InputOTP>
    </div>
  );
}

View code

Input OTP · Invalid

Installation

pnpm dlx velaui add @velaui/input-otp
pnpm dlx velaui add @velaui/input-otp

Code

input-otp-invalid.tsx
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";

export default function Example() {
  return (
    <div className="space-y-3">
      <Label>Invalid</Label>
      <InputOTP maxLength={6} defaultValue="123456">
        <InputOTPGroup>
          <InputOTPSlot index={0} aria-invalid />
          <InputOTPSlot index={1} aria-invalid />
          <InputOTPSlot index={2} aria-invalid />
          <InputOTPSlot index={3} aria-invalid />
          <InputOTPSlot index={4} aria-invalid />
          <InputOTPSlot index={5} aria-invalid />
        </InputOTPGroup>
      </InputOTP>
    </div>
  );
}

View code

Input OTP · Pattern

Installation

pnpm dlx velaui add @velaui/input-otp
pnpm dlx velaui add @velaui/input-otp

Code

input-otp-pattern.tsx
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
  InputOTPSeparator,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";
import { REGEXP_ONLY_DIGITS } from "input-otp";

export default function Example() {
  return (
    <div className="space-y-3">
      <Label>With pattern</Label>
      <InputOTP maxLength={6} variant="fill" pattern={REGEXP_ONLY_DIGITS}>
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
        </InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup>
          <InputOTPSlot index={3} />
          <InputOTPSlot index={4} />
          <InputOTPSlot index={5} />
        </InputOTPGroup>
      </InputOTP>
    </div>
  );
}

Usage

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";

export default function Example() {
  return (
    <div className="space-y-3">
      <Label>Outline</Label>
      <InputOTP maxLength={6}>
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
          <InputOTPSlot index={3} />
          <InputOTPSlot index={4} />
          <InputOTPSlot index={5} />
        </InputOTPGroup>
      </InputOTP>
    </div>
  );
}

Props

InputOTP

PropTypeDefault
containerClassNamestring—
variantInputOTPVariant"outlined"

Takes these and every prop of InputOTPProps.

InputOTPGroup

Takes every prop of React.ComponentProps<"div">.

InputOTPSlot

PropTypeDefault
indexrequirednumber—
variant"fill" | "outlined"—

Takes these and every prop of InputOTPSlotProps.

InputOTPSeparator

Takes every prop of React.ComponentProps<"div">.

Dependencies

npm
class-variance-authority@0.7.1input-otp@1.4.2
Installs with it
1 icon

Source

components/ui/input-otp.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { cva, type VariantProps } from "class-variance-authority";
import { Dash } from "@/components/icons/dash";

import { cn } from "@/lib/utils";

const inputOTPSlotVariants = cva(
  // The box never changes size between states — every state difference is a
  // colour, a ring or a border colour, so entering one never reflows the row.
  "relative flex size-14 shrink-0 items-center justify-center rounded-md text-xl leading-[1.3] font-medium text-text-muted transition-all outline-none group-has-disabled/otp:text-text-disabled data-[active=true]:z-10 data-[active=true]:border-ring-neutral-outline data-[active=true]:ring-3 data-[active=true]:ring-ring-neutral-shadow aria-invalid:border-negative-middle data-[active=true]:aria-invalid:border-negative-middle data-[active=true]:aria-invalid:ring-negative-middle/25",
  {
    variants: {
      variant: {
        outlined:
          "border border-border-default bg-bg-main hover:border-border-hover hover:bg-input-hover data-[active=true]:bg-input-default group-has-disabled/otp:border-border-disabled group-has-disabled/otp:bg-input-disabled",
        // Fill draws no border at rest, but focus and invalid both draw a 1px
        // one — reserve it as transparent so entering a state never reflows.
        fill: "border border-transparent bg-input-default hover:bg-input-hover group-has-disabled/otp:border-transparent group-has-disabled/otp:bg-input-disabled",
      },
    },
    defaultVariants: {
      variant: "outlined",
    },
  },
);

type InputOTPVariant = NonNullable<
  VariantProps<typeof inputOTPSlotVariants>["variant"]
>;

/** Set once on the root so every slot inherits it instead of each call site repeating it. */
const InputOTPVariantContext = React.createContext<InputOTPVariant>("outlined");

export type InputOTPProps = React.ComponentProps<typeof OTPInput> & {
  containerClassName?: string;
  variant?: InputOTPVariant;
};

function InputOTP({
  className,
  containerClassName,
  variant = "outlined",
  value,
  defaultValue,
  onChange,
  ...props
}: InputOTPProps) {
  // input-otp seeds its state from `defaultValue` yet also spreads it onto its
  // controlled <input>, which React rejects; hold the uncontrolled value here
  // and always hand input-otp a controlled one.
  const [uncontrolled, setUncontrolled] = React.useState(
    defaultValue == null ? "" : String(defaultValue),
  );
  const controlled = value !== undefined;

  return (
    <InputOTPVariantContext value={variant}>
      <OTPInput
        data-slot="input-otp"
        data-variant={variant}
        containerClassName={cn(
          "group/otp flex items-center",
          containerClassName,
        )}
        spellCheck={false}
        className={cn("disabled:cursor-not-allowed", className)}
        value={controlled ? value : uncontrolled}
        onChange={(next: string) => {
          if (!controlled) {
            setUncontrolled(next);
          }
          onChange?.(next);
        }}
        {...props}
      />
    </InputOTPVariantContext>
  );
}

function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="input-otp-group"
      className={cn("flex items-center gap-1", className)}
      {...props}
    />
  );
}

export type InputOTPSlotProps = React.ComponentProps<"div"> &
  VariantProps<typeof inputOTPSlotVariants> & {
    index: number;
  };

function InputOTPSlot({
  index,
  className,
  variant,
  ...props
}: InputOTPSlotProps) {
  const inputOTPContext = React.useContext(OTPInputContext);
  const inheritedVariant = React.useContext(InputOTPVariantContext);
  const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};

  return (
    <div
      data-slot="input-otp-slot"
      data-active={isActive}
      className={cn(
        inputOTPSlotVariants({ variant: variant ?? inheritedVariant }),
        className,
      )}
      {...props}
    >
      {char}
      {hasFakeCaret && (
        <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
          <div className="h-5 w-px animate-caret-blink bg-text-muted duration-1000" />
        </div>
      )}
    </div>
  );
}

function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="input-otp-separator"
      className="flex items-center [&_svg:not([class*='size-'])]:size-4"
      role="separator"
      {...props}
    >
      <Dash />
    </div>
  );
}

export {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
  InputOTPSeparator,
  inputOTPSlotVariants,
};