InputOTP

The one-time code — Okami's login step. Squircle boxes in two even groups (login's eight, as four and four), over one real input, so paste, one-time-code autofill and password managers just work. Built on the vendored input-otp.

Playground

InputOTP
State

The login step

The code from the email, and a button to confirm it. onComplete submits the form as soon as the last digit lands; the button stays for anyone who pastes and waits. Login asks for eight digits in sm boxes, so the row fits a phone. In this demo the code is 48291357 — anything else shows the error.

Check your email

We sent a code to jana@studio-lumen.sk.

Sizes

Boxes are square at the Input heights: sm 32px, md 36px by default, lg 40px. That is their most: in a narrower column they shrink to fit, still square, so the row never runs past it — on a phone, six lg boxes come out a little smaller. Login uses sm: its eight boxes fit a phone's width as they are.

Length

length sets how many characters, split into two even groups. Match it to the auth provider's code length — Supabase sends 6 by default and can be set to 8. Eight boxes are wide: on a phone, keep them at sm.

Invalid & disabled

aria-invalid gives every box a destructive edge — pair it with a FieldError. Keep the code in place so it can be corrected; clear the error as soon as it changes. Disabled dims the whole row, e.g. while a new code is on its way.

4
8
2
9
3
1

API reference

PropTypeDefaultDescription
lengthnumber6How many characters. Split into two even groups with a dash between.
size"sm" | "md" | "lg""md"Box size: 32, 36 or 40px, like Input. Less in a narrower column: the boxes shrink to fit it.
valuestring—The code so far. Omit for an uncontrolled field.
onChange(value: string) => void—Called with the whole code on every change.
onComplete(value: string) => void—Called once every box is filled — submit the form here.
patternstringREGEXP_ONLY_DIGITSA regex source each character must match. Digits only by default.
namestring—Posts the code with its form — no hidden input needed.
aria-labelstring—Names the code for assistive tech. Required unless a label points at its id.
aria-invalidbooleanfalseMarks the code wrong: every box gets a destructive edge.
disabledbooleanfalseDisables the input and dims the row.
classNamestring—Layout for the row of boxes — margins, alignment.

Everything else passes through to the one real <input>: id, autoFocus, onBlur, ref, … It already sets autoComplete="one-time-code" and inputMode="numeric".

Guidelines

  • Say where the code went (“We sent a code to jana@studio-lumen.sk”) right above the boxes, and name the field with aria-label — the boxes alone don't say what they are.
  • Submit on onComplete, but keep a Sign in button: some people paste and look before they commit, and a button is where a loading state belongs.
  • On a wrong code, keep the digits, mark the field invalid and say what to do next. Offer a new code nearby; don't clear the boxes behind the person's back.
  • Keep length in step with the auth provider. A mismatch lets people finish typing a code the server will never accept.
  • Use it for short codes only. Anything a person chooses — a PIN they set, a promo code — is an Input.