For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Input Otp

The Input Otp component from this design system, built on Base UI.

Installation

$ pnpm dlx shadcn@latest add @cobalt/input-otp

Source

"use client";

import { RiSubtractLine } from "@remixicon/react";
import { cn } from "cn";
import { OTPInput, OTPInputContext } from "input-otp";
import * as React from "react";

const InputOTPStateContext = React.createContext({
  disabled: false,
  invalid: false,
});

function InputOTP({
  className,
  containerClassName,
  ...props
}: React.ComponentProps<typeof OTPInput> & { containerClassName?: string }) {
  return (
    <InputOTPStateContext.Provider
      value={{
        disabled: !!props.disabled,
        invalid:
          props["aria-invalid"] === true || props["aria-invalid"] === "true",
      }}
    >
      <OTPInput
        data-slot="input-otp"
        containerClassName={cn("flex items-center gap-2", containerClassName)}
        spellCheck={false}
        className={cn("disabled:cursor-not-allowed", className)}
        {...props}
      />
    </InputOTPStateContext.Provider>
  );
}

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

function InputOTPSlot({
  index,
  className,
  ...props
}: React.ComponentProps<"div"> & { index: number }) {
  const inputOTPContext = React.useContext(OTPInputContext);
  const { disabled, invalid } = React.useContext(InputOTPStateContext);
  const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};

  return (
    <div
      data-slot="input-otp-slot"
      data-active={isActive}
      data-disabled={disabled || undefined}
      data-invalid={invalid || undefined}
      className={cn(
        "relative flex size-12 items-center justify-center rounded-[10px] border border-border bg-popover font-mono text-[18px] leading-[26px] font-medium tracking-normal text-foreground tabular-nums shadow-[shadow:var(--shadow-control,0_1px_2px_0_rgb(0_0_0/0.05))] outline-none transition-[background-color,border-color,box-shadow] duration-150 ease-[ease] motion-reduce:transition-none",
        "hover:border-[var(--border-hover,var(--border))] data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-2 data-[active=true]:ring-ring",
        "data-invalid:border-[var(--field-invalid,var(--destructive))] data-invalid:bg-[var(--input-invalid-surface,color-mix(in_srgb,var(--destructive)_15%,transparent))] data-invalid:text-destructive data-invalid:hover:border-[var(--field-invalid,var(--destructive))]",
        "data-disabled:cursor-not-allowed data-disabled:bg-muted data-disabled:text-[color:var(--foreground-subtle,var(--muted-foreground))] data-disabled:shadow-none",
        className
      )}
      {...props}
    >
      {char}
      {hasFakeCaret && (
        <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
          <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000 motion-reduce:animate-none" />
        </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}
    >
      <RiSubtractLine />
    </div>
  );
}

export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };