Skip to content
  1. Keyline UI
  2. OTP field
Keyline UIComponents
  • Get started
    • Installation
    • Theming
    • Modes
    • Configuration
    • Composition
  • Actions
    • Button
    • Button group
    • Split button
    • Toggle
    • Toggle group
    • Toolbar
    • Menu
    • Menubar
    • Context menu
  • Inputs
    • Field and form
    • Checkbox
    • Checkbox group
    • Radio group
    • Switch
    • Slider
    • Segmented
    • Select
    • Native select
    • Combobox
    • Multiselect
    • Number field
    • OTP field
    • Time field
    • Date picker and calendar
    • Date range
  • Data
    • Table
    • Chart
    • Stat and meter
    • Status and badge
    • Progress
    • Skeleton
    • Empty
    • Avatar
    • Kbd
    • Marker
  • Navigation
    • Tabs
    • Breadcrumb
    • Pagination
    • Nav
    • Navigation menu
    • Command menu
  • Overlays
    • Dialog
    • Alert dialog
    • Drawer
    • Sheet
    • Popover
    • Hover card
    • Tooltip
    • Toast
    • Notice
  • Layout
    • App shell
    • Sidebar
    • Panel
    • Resizable
    • Scroll area
    • Accordion
    • Collapsible
    • Carousel
    • Inspector
    • Item
    • Direction
  • Conversation
    • Message
    • Bubble
    • Message scroller
    • Attachment
    • Questionnaire
  • Blocks
    • Shipments

OTP field

A one-time code in one frame, a cell per character. While you check it, a segment runs along the bottom edge; a wrong code turns the frame red and puts the caret back at the start.

Loading the live example…

"use client"

import { useEffect, useRef, useState } from "react"
import { OtpField, type OtpSignal } from "@/components/keyline/ui/otp-field"

const CODE = "271828" // what your server would check

export function SignInCode() {
  const [code, setCode] = useState("")
  const [signal, setSignal] = useState<OtpSignal>("idle")
  const timer = useRef<ReturnType<typeof setTimeout>>(undefined)
  useEffect(() => () => clearTimeout(timer.current), [])

  return (
    <div className="grid max-w-[360px] gap-3">
      <OtpField
        label="Code from your phone"
        length={6}
        groups={[3, 3]}
        value={code}
        signal={signal}
        error="That code doesn't match. Codes last 10 minutes."
        description={`Sent to the number ending 4417. For this demo, it is ${CODE}.`}
        onValueChange={(v) => {
          // after a wrong code, the first new character starts a fresh code, so completing it checks again
          if (signal === "failed") {
            setSignal("idle")
            setCode(v.slice(0, 1))
          } else setCode(v)
        }}
        onValueComplete={(v) => {
          setSignal("pending")
          timer.current = setTimeout(() => setSignal(v === CODE ? "done" : "failed"), 900)
        }}
      />
      <p role="status" className="text-[14px] text-kl-ink-2">
        {signal === "done" ? "Signed in to Dunder Mifflin Infinity." : "Type or paste the six digits."}
      </p>
    </div>
  )
}

Installation

npx shadcn@latest add @keyline-ui/otp-field

Run it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.

Usage

import { OtpField } from "@/components/keyline/ui/otp-field"
<OtpField
  label="Code from your phone"
  length={6}
  groups={[3, 3]}
  signal={signal}
  error="That code doesn't match."
  onValueComplete={verify}
/>

Props

PropTypeDefaultDescription
labelReactNodeDefault —Names the first cell; the rest are “Character n of N”. Required.
lengthnumberDefault —How many characters. Required.
groupsnumber[]Default —Splits the cells, such as [3, 3]. Must add up to length.
onValueComplete(code: string) => voidDefault —Runs once every cell is filled, typed or pasted. Check the code here.
signal"idle" | "pending" | "done" | "failed"Default "idle"pending makes the cells read-only while you check. failed turns the frame red and returns the caret to the first cell; pair it with error.
validationType"numeric" | "alpha" | "alphanumeric" | "none"Default "numeric"Which characters a cell takes.
maskbooleanDefault falseHides the characters, for a PIN.
autoSubmitbooleanDefault falseSubmits the form once the code is complete.
errorReactNodeDefault —The message when a check fails. With invalid it always shows.
descriptionReactNodeDefault —A line under the field, read out with it.
layout"control" | "cell"Default "control"cell drops the frame and hides the label, to sit inside another frame.

Pasting a whole code fills every cell; Backspace in an empty cell goes back one. Built on Base UI OTP Field.

Sheet 28 of 73PreviousNumber fieldNextTime field
On this page