Input OTP
A one-time code field for sign-in and verification codes. Each character gets its own slot, and slots can be split into groups such as123–456.
Six-digit codes
Type or focus an example. Codes stay local and are never verified or sent.
In progress
2 of 6 digits entered.
Incorrect code
That code didn’t match. Try again.
Disabled
With a label
A native label names the code; a helper is read when the group is entered.
Enter the 6-digit code from your authenticator app. Paste fills every slot.
Usage
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@tulipfarm/sprout/input-otp";
const [code, setCode] = useState("194278");
const [showError, setShowError] = useState(true);
<InputOTP
maxLength={6}
value={code}
onChange={(value) => {
setCode(value);
setShowError(false);
}}
aria-label="Verification code"
aria-invalid={showError}
aria-describedby="otp-error"
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<p id="otp-error" role="alert">That code didn’t match. Try again.</p>Parts
InputOTP is built on Base UI OTPField.Root.maxLength sets the number of slots. UsedefaultValue for an uncontrolled code, orvalue with onChange(value) to control it.onValueChange, onValueComplete,disabled, readOnly, required,name, mask, and autoSubmit pass through to Base UI. Codes are numeric by default; setvalidationType="alphanumeric" for letters.
InputOTPGroup joins its slots into one bordered segment with rounded outer corners. InputOTPSlot is a real input; pass its zero-basedindex and render slots in order.InputOTPSeparator draws a dash between groups.
Slots are 32px square with 14px text. On coarse pointers they grow to 44px with 16px text so each slot is an easy touch target. Groups sit 8px apart.
States
The focused slot gets a focus border and a soft 2px ring above its neighbors. aria-invalid on InputOTP marks every slot invalid; you can also set it per slot. Invalid slots draw a danger border, and their group gets a faint danger ring. Pointaria-describedby at the error text.disabled dims the field to 50% and blocks edits.
Keyboard and accessibility
Typing moves to the next slot. Backspace clears and moves back; Delete clears in place. Arrow keys move between slots, and Home and End jump to the first slot and the last filled one. Paste or one-time-code autofill fills every slot at once. Only the active slot is in the tab order.
aria-label names the field and its first slot; later slots are announced as “Character 2 of 6”. Alternatively, passid and use a Label htmlFor. Color changes take 120ms and stop when reduced motion is requested. In forced colors mode slots keep a system border and a highlight focus ring.
Theme tokens
Slot borders use --sprout-input, the fill--sprout-surface, text --sprout-text, and the separator --sprout-muted. Focus uses--sprout-focus and invalid --sprout-danger. Outer corners follow --sprout-radius-control. The opt-in TulipFarm theme tints the focus ring through these tokens.