Components/Input OTP

Input OTP

Four ways to break six digits into groups, each valid or invalid, plus the single slot on its own with the five states that belong to one box rather than to the whole code.

InputOTP · Components frameLive in the browser · drawn to match the file
Variant
State
1
2
With Separator · Default242 × 32 in the file
a joined strip has one border

Three of the four layouts butt their slots together, so the group carries a single base.input outline and 1px dividers sit absolutely between the boxes. Give every slot its own border and each seam would draw two lines instead of one.

With Spacing is the exception

Six separate 32 × 32 boxes, 8 apart, each with its own outline and radius.sm corners. It is the only layout where the slot is a whole shape on its own, which is also why it is the widest at 232.

Invalid reaches everything

Not just the outline: the dividers between slots and the dashes between groups all go to base.destructive too. A code that is wrong is wrong across its whole width, and leaving the seams grey would read as a partly-valid field.

The slot on its own

One box, five states, 32 square

Published separately because the states belong to a single digit rather than to the whole code: one slot is focused while the others are not, one is filled while the rest wait.

InputOTP / Slot · Components frameLive in the browser · drawn to match the file
State
State=Focus32 × 32 in the file
DefaultAn empty box: base.background on a 1px base.input outline.
FocusA 3px base.ring outside, the border left where it was.
FilledA digit at 14/400 in base.foreground. Nothing else moves.
InvalidThe outline goes to base.destructive.
Disabledbase.muted at 50%, matching Input and Input Group.
The usage frame

A verification screen, a rejection, and a code ready to go

1000 by 881, three sections. The card carries the source's own copy. The last section is the only place in the file that fills a code in, because a joined strip cannot take digits without detaching it from the component.

Section
Verify your login
Enter the verification code we sent to your email address: m@example.com.
Resend code
Having trouble signing in? Contact support
In the file

Two components, thirteen variants

Read straight out of the Penpot file. The axes below are the property names you will see in the variant dropdown.

InputOTPVariant × StateDigits Only · With Separator · Simple · With Spacing × Default · Invalid - 192, 242, 217 and 232 wide8
InputOTP / SlotStateDefault · Focus · Filled · Invalid · Disabled - 32 × 325

Using the Input OTP

Because butted boxes each carrying their own 1px outline would draw two lines at every seam. Three of the four layouts put a single base.input outline on the group and drop 1px dividers between the slots as absolute children, so every seam is exactly one line wide.

It is the only layout where a slot is a whole shape: six separate 32 × 32 boxes, 8 apart, each with its own outline and radius.sm corners. No group, no dividers. It is also the widest at 232 against Digits Only's 192.

Everything structural. The group outline, the dividers between slots and the dashes between groups all go to base.destructive. Leaving the seams grey would make a wrong code look partly valid, which is worse than not flagging it at all.

Because its states belong to one digit, not to the code. One slot is focused while the others wait, one is filled while the rest are empty. Those five states could not be an axis on InputOTP without multiplying eight variants by five for combinations nobody needs.

Three sections at 1000 wide. A verify-your-login card carrying the source's own copy, a code that did not match with the message under it, and a six-digit code filled in with a Verify button. The last one is the only place in the file where the Slot component's Filled state is used six times over, because the joined layouts cannot take digits without detaching.

Forms · 13 variants