Input OTP
An input for one-time passwords and verification codes, rendered as individual character slots
<sa-input-otp max-length="6"/>Usage
<sa-input-otp> renders a segmented input for one-time passwords and verification codes. It is backed by a single real <input> that holds the whole code, made transparent and laid over the character slots, so it receives focus and keystrokes and model-binds and posts back like any other text field. A small sel-input-otp web component distributes what you type into the slots, moves the active slot and caret, and enforces the allowed character pattern.
Use the max-length attribute to control how many characters the code has.
<sa-input-otp max-length="6"/>Input OTP only submits a value when you set asp-for or name. Just like a native input without a name, an Input OTP without one (such as most of the examples on this page) renders and is fully interactive but posts nothing. Use asp-for to model bind, or set name to post a value without binding.
Examples
Number of digits
Use the max-length attribute to set the number of slots. It defaults to 6.
<sa-input-otp max-length="4"/>Groups
Use the groups attribute to split the slots into groups, separated by a separator. Provide a comma-separated list of group sizes — the sizes must add up to max-length (when both are set), otherwise the total is taken from the groups.
<sa-input-otp groups="3,3"/>Separator
A separator is rendered automatically between groups. Supply more than two groups to add multiple separators.
<sa-input-otp groups="2,2,2"/>Custom separator
Compose the slots by hand and supply child content to <sa-input-otp-separator> to replace the default separator glyph. The example below uses <sa-icon> to render a different icon between the groups.
<sa-input-otp max-length="6">
<sa-input-otp-group>
<sa-input-otp-slot/>
<sa-input-otp-slot/>
<sa-input-otp-slot/>
</sa-input-otp-group>
<sa-input-otp-separator>
<sa-icon name="dot" class="size-4"/>
</sa-input-otp-separator>
<sa-input-otp-group>
<sa-input-otp-slot/>
<sa-input-otp-slot/>
<sa-input-otp-slot/>
</sa-input-otp-group>
</sa-input-otp>Size
Increase the size of the slots by applying utility classes to the groups. Target the slots from the group with the *:data-[slot=input-otp-slot]: variant so the sizing applies to every slot inside it.
<sa-input-otp max-length="6">
<sa-input-otp-group class="*:data-[slot=input-otp-slot]:h-12 *:data-[slot=input-otp-slot]:w-11 *:data-[slot=input-otp-slot]:text-xl">
<sa-input-otp-slot/>
<sa-input-otp-slot/>
<sa-input-otp-slot/>
</sa-input-otp-group>
<sa-input-otp-separator/>
<sa-input-otp-group class="*:data-[slot=input-otp-slot]:h-12 *:data-[slot=input-otp-slot]:w-11 *:data-[slot=input-otp-slot]:text-xl">
<sa-input-otp-slot/>
<sa-input-otp-slot/>
<sa-input-otp-slot/>
</sa-input-otp-group>
</sa-input-otp>Pattern
Use the pattern attribute to constrain the allowed characters with a regular expression — any input that would make the value fail to match is rejected. It defaults to digits only (^\d*$). Pair it with inputmode to control the on-screen keyboard.
<sa-input-otp max-length="6" pattern="^[a-zA-Z0-9]*$" inputmode="text"/>Composition
For full control over the layout, compose the slots by hand with the <sa-input-otp-group>, <sa-input-otp-slot>, and <sa-input-otp-separator> Tag Helpers instead of letting groups generate them. Slots take their position automatically in document order.
<sa-input-otp max-length="6">
<sa-input-otp-group>
<sa-input-otp-slot/>
<sa-input-otp-slot/>
<sa-input-otp-slot/>
</sa-input-otp-group>
<sa-input-otp-separator/>
<sa-input-otp-group>
<sa-input-otp-slot/>
<sa-input-otp-slot/>
<sa-input-otp-slot/>
</sa-input-otp-group>
</sa-input-otp>Model binding
Supports ASP.NET Core model binding via asp-for. Bind it to a string and the whole code posts straight back to the property.
When using model binding, StellarAdmin will automatically wrap the input inside a Field with the correct label and description derived from data attributes. You can opt out of this behavior by setting the render-field attribute to false.
<sa-input-otp asp-for="OneTimePassword" groups="3,3"/>Validation
Validation works like any other model-bound field. Add validation attributes to your model and the slots reflect the invalid state, with the error message rendered below.
<sa-input-otp asp-for="OneTimePassword" groups="3,3"/>Manual validation
If you're not using ASP.NET Core model binding, you can manually indicate validation errors by setting the aria-invalid attribute to true. This applies the destructive styling to the slots.
The error message can be displayed using either a <sa-field-error> Tag Helper (when using explicit fields) or the error attribute (when using implicit fields).
<sa-field>
<sa-label>One-time password</sa-label>
<sa-input-otp aria-invalid="true" groups="3,3"/>
<sa-field-error>Enter the complete 6-digit code</sa-field-error>
<sa-field-description>
Enter the 6-digit code we sent to your phone.
</sa-field-description>
</sa-field>Disabled
Set disabled="true" to prevent the user from interacting with the input.
<sa-input-otp max-length="6" value="123" disabled="true"/>Accessibility
<sa-input-otp> renders a single native <input> behind the visible slots, so typing, Backspace, the arrow keys and pasting behave like a normal text input. It sets inputmode="numeric" by default so mobile keyboards show digits, and autocomplete="one-time-code" so browsers can offer a code they received. With asp-for or the label attribute the label is wired to the input for you.
When the field has a model state error, or when you set aria-invalid="true", the input and every slot render aria-invalid="true".
Part classes
Pass an InputOtpClassNames instance from the StellarAdmin.TagHelpers namespace to <sa-input-otp> using class-names. Each property accepts an optional string of space-separated CSS classes, which are added to the part's existing classes.
@{
var partClasses = new InputOtpClassNames
{
Root = "travel-field",
Label = "travel-label",
Description = "travel-help",
Control = "travel-input-otp-control",
Group = "travel-input-otp-group",
Slot = "travel-input-otp-slot",
Separator = "travel-input-otp-separator",
};
}
<sa-input-otp id="classes-code" label="Booking verification" description="Enter the six-digit code we sent you." groups="3,3" class-names="@partClasses" />| Property | Target |
|---|---|
Root | The generated field wrapper, when the input renders an implicit field. |
Label | The generated field label, when rendered. |
Description | The generated help text, when rendered. |
Error | The generated validation message element, when rendered. |
Content | The generated container around the label and supporting text in a horizontal implicit field. |
Control | The <sel-input-otp> container. |
Group | Every group of character slots. |
Slot | Every character slot. |
Separator | Every separator between groups. |
Root, Label, Description, Error, and Content are inherited from FieldClassNames. They only style generated field elements; with render-field="false", apply class directly to any field elements you compose yourself. The control-specific parts still apply. See Field part classes for the shared behavior and a styled example.
Group, Slot, and Separator apply to both automatically generated parts and child Tag Helpers you compose inside the OTP input. Classes on individual child elements are added alongside these shared classes.
API Reference
<sa-input-otp>
Renders a <sel-input-otp> web component containing the slot cells and a single transparent <input> element that holds the whole code and posts it as one form value. When asp-for is set, it delegates to the built-in ASP.NET Core Input Tag Helper to resolve the input's name, value, and validation attributes.
Prop
Type
<sa-input-otp-group>
Renders a <div> element that groups a run of slots.
<sa-input-otp-slot>
Renders a <div> element that displays one character of the code and the active caret.
Prop
Type
<sa-input-otp-separator>
Renders a <div> element with role="separator", containing a minus icon by default — supply child content to override it.