# A verification code is six slots in shadcn, so plan paste and submit

> shadcn's OTP input is six slots with paste built in. Here is the code field, the verify step and the sign-in checks for an email code.
> By Dave · 2026-10-13
> Source: https://otf-kit.dev/blog/shadcn-input-otp-howto

A verification-code field looks like one input. In shadcn it is an `InputOTP` built from slots, and the paste behavior comes with the component, so the real work is in the submit and resend steps.

## What the shadcn page covers

The page describes the component as "Accessible one-time password component with copy-paste functionality." The install command is `pnpm dlx shadcn@latest add input-otp`, with tabs for npm, yarn and bun. The usage example imports `InputOTP`, `InputOTPGroup`, `InputOTPSeparator` and `InputOTPSlot` from `@/components/ui/input-otp`, and sets `maxLength={6}` on the root.

The page lists examples for a pattern, a digits-only pattern, a separator, a disabled state, a controlled value, an invalid state, four digits, an alphanumeric code, a form and a right-to-left layout. Its form example shows a line that asks for the verification code sent to an email address, with a Resend Code button and a Verify button. The code for that field is cut off on the page, so the field code below is ours, built from the parts the page names.

The page does not mention autofill. Do not promise autofill from your browser or your password manager. Test the paste path instead, which the intro does promise.

## Six slots in two groups

A six-digit code reads better in two groups of three, with a separator in between. Put three `InputOTPSlot` components in each `InputOTPGroup`, and give each slot an index from zero to five:

```tsx
<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>
```

The separator is a visual break only. The code still has six characters, and the root's `maxLength` decides when the field is full. Keep the slot count in step with the number of digits your email actually sends, so the field never waits for characters that will not come.

## A controlled code and a verify button

The page says to use `value` and `onChange` to control the input. Control the value in React state, so the form can check the code before it submits. The page's form example pairs the field with a Verify button, and the Verify button stays disabled until six characters are present:

```tsx
import * as React from "react"
import { REGEXP_ONLY_DIGITS } from "input-otp"
import { Button } from "@/components/ui/button"
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export function EmailCodeField({
  onVerify,
}: {
  onVerify: (code: string) => void
}) {
  const [code, setCode] = React.useState("")

  return (
    <div className="flex flex-col gap-3">
      <InputOTP
        maxLength={6}
        pattern={REGEXP_ONLY_DIGITS}
        value={code}
        onChange={setCode}
      >
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
        </InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup>
          <InputOTPSlot index={3} />
          <InputOTPSlot index={4} />
          <InputOTPSlot index={5} />
        </InputOTPGroup>
      </InputOTP>
      <Button disabled={code.length !== 6} onClick={() => onVerify(code)}>
        Verify
      </Button>
    </div>
  )
}
```

The `pattern` prop limits input to digits. The page's Digits Only example uses `REGEXP_ONLY_DIGITS` for that. Check the page's View Code panel for the exact import, because the page's own examples import pattern helpers from `input-otp`, and the code above follows that.

![A hand holds a stylus at the first of six glowing code boxes](https://cdn.otf-kit.dev/blog/shadcn-input-otp-howto/inbody1-20261013.png)

## Show an error on the slots

The page says to use `aria-invalid` on the slots to show an error state. Set it on every slot when the server rejects the code, and clear it when the user edits the field again. A single slot marked invalid can look like one bad digit, and the user is not wrong to read it that way.

```tsx
const [invalid, setInvalid] = React.useState(false)

<InputOTPSlot index={0} aria-invalid={invalid} />
```

Repeat that prop on each slot. Show the error text under the field, not only as a red outline, so the message reaches users who do not read colour.

![Dex and Luna at a split panel: an email envelope on one side, a row of code boxes on the other](https://cdn.otf-kit.dev/blog/shadcn-input-otp-howto/inbody2-20261013.png)

## Build the sign-in flow

The field is the easy part. The flow around it decides whether the code works. These steps are our advice, not the docs':

- Send the code, then show the address it went to. The page's form example shows that pattern with the sentence about an email address.
- Expire the code after a fixed time, and say so on the screen. A user who waits ten minutes should see a clear message, not a generic failure.
- Give Resend Code a short cooldown. A button that can be pressed without limit sends a new email every time someone clicks it.
- Limit attempts on the server. Client checks do not stop someone who guesses at speed.
- Clear the field after a wrong code, and keep the email address on the screen so the user can request a new one.

Keep the attempt limit and the expiry on the server. The client shows the result, but the server decides it.

## Test before you ship

Run these checks on the real flow, not only on the component:

- Paste a six-digit code into the first slot. All six slots should fill, and the Verify button should enable.
- Paste a code with a space or a dash in it. Decide what happens, then make the field do the same thing every time.
- Type a letter into a digits-only field. Nothing should appear in the slot.
- Press Backspace in a middle slot. Focus should move back, and the digit before it should clear.
- Use the arrow keys to move between slots, then press Enter on Verify. The code should submit once.
- On a 390px phone, check that the six slots, the separator and the Verify button all fit with no horizontal overflow.

Run the phone check last. Six slots with a separator are wider than they look on a desktop, and a wrapped row confuses a code entry.

## Where OTF stands

If you want a verification-code sign-in already built into a product, we cannot point you to one. We checked the SaaS Dashboard, Booking, Fitness and Arcade kit pages for one-time password, verification code, magic link and passwordless terms. None of them lists one. The SaaS Dashboard page describes email and password sign-in and sign-up. The Booking page describes an email sign-in with a one-tap demo, and the Arcade page says it has no backend authentication by default. So this article does not say any OTF kit ships a code-based sign-in.

The pricing page lists the Full-stack kits at $99 each and the Everything Bundle at $149, both as one-time payments, and it offers a live demo before you buy. Check each kit's repository for the sign-in and the components it ships before you rely on any of them. For a combobox in the same sign-up form, see the [combobox guide](/blog/shadcn-combobox-howto), and for the dialog that often confirms a sign-out or a delete, see the [dialog and sheet guide](/blog/shadcn-dialog-vs-sheet-howto). If you are choosing a starting point for a product, the pricing page is the place to compare kits side by side: [OTF pricing](/pricing).

## Sources

- shadcn/ui, Input OTP: https://ui.shadcn.com/docs/components/input-otp
- OTF pricing: [otf-kit.dev/pricing](/pricing)
