Westy Design Systemv0.2.7
OverviewComponentsExamplesStorybook

Foundations

  • Typography
  • Colors
  • Spacing
  • Radius
  • Shadows
  • Icons

Components

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Menubar
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Questionnaire
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toggle
  • Toggle Group
  • Tooltip

Chat

  • Attachment
  • Bubble
  • Marker
  • Message
  • Message Scroller

Components

Input OTP

Accessible one-time password component with copy paste functionality.

Playground

—maxLength
—children
—pattern
—aria-invalid
—disabled
Usage
Compose the field from InputOTP with one InputOTPSlot per character inside an InputOTPGroup. maxLength is required and must match the slot count — a hidden input drives every slot, so focus, paste and keyboard navigation work like a single field.
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@workspace/ui/components/input-otp"

<InputOTP maxLength={6}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>
Separator
Split the code into multiple InputOTPGroup blocks and place an InputOTPSeparator between them — slot indexes keep counting across groups. Each group gets its own rounded border, matching formats like 123-456.
<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>
Pattern
Pass a regular expression string as pattern to restrict which characters are accepted. The input-otp library ships REGEXP_ONLY_DIGITS, REGEXP_ONLY_CHARS and REGEXP_ONLY_DIGITS_AND_CHARS for the common cases.
import { REGEXP_ONLY_DIGITS } from "input-otp"

<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>
Controlled
Use value and onChange to control the state — unlike a native input, the callback receives the string directly. Add onComplete to react when all slots are filled, such as submitting the code automatically.

Enter your one-time password.

const [value, setValue] = React.useState("")

<InputOTP maxLength={6} value={value} onChange={setValue}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>
Disabled
Use the disabled prop to prevent interaction — the container dims to 50% opacity via has-disabled and the hidden input stops receiving focus. Any current value stays visible.
1
2
3
4
5
6
<InputOTP maxLength={6} disabled value="123456">
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
  </InputOTPGroup>
  <InputOTPSeparator />
  <InputOTPGroup>
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>
Invalid
Set aria-invalid on each InputOTPSlot to mark a rejected code — the slots switch their border to destructive and the active slot swaps its focus ring to the destructive tint.
1
2
3
4
5
6
<InputOTP maxLength={6} value="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>
RTL
Wrap the field in a dir="rtl" container or inherit the direction from a Direction provider — the slots flip visually with logical border and radius classes while indexes keep their logical order.
<div dir="rtl" className="space-y-3">
  <Label>رمز التحقق</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>
API Reference
InputOTP wraps the input-otp library and accepts all of its props plus the standard input attributes — leave it uncontrolled and read the finished code via onComplete, or control it with value + onChange. InputOTPSlot requires the index of the character it renders.
PropTypeDefault
maxLengthnumberrequired
valuestring—
onChange(value: string) => void—
onComplete(value: string) => void—
patternstring—
textAlign"left" | "center" | "right""left"
pasteTransformer(pasted: string) => string—
pushPasswordManagerStrategy"increase-width" | "none""increase-width"
containerClassNamestring—
disabledboolean—
InputOTPSlot indexnumberrequired

Tokens used

7 design tokens consumed by the Input OTP component.

  • --input

    Input

    border-inputdark:bg-input/30
  • --ring

    Ring

    data-[active=true]:border-ringdata-[active=true]:ring-ring/50
  • --destructive

    Destructive

    aria-invalid:border-destructivehas-aria-invalid:ring-destructive/20dark:has-aria-invalid:ring-destructive/40
  • --foreground

    Foreground

    bg-foreground
  • --text-sm

    Text Small (14px)

    text-sm
  • --radius-md

    Radius 0.8× (8px)

    rounded-mdfirst:rounded-s-mdlast:rounded-e-md
  • --spacing

    Spacing unit (4px)

    size-9h-4size-4