Component

OTP Input

A one-time password input component using a single hidden input with visual slots. Provides excellent accessibility, native browser autofill, and mobile keyboard support.

Based on input-otp by Guilherme Rodz.

Demo

Current value: ""
-
-
-
-
-
-
-
1
2
3
4
5
6

Features

  • Single hidden input architecture for better screen reader support
  • Native browser paste and autofill support
  • Mobile keyboard optimized (numeric inputmode)
  • Pattern validation (digits only, alphanumeric, etc.)
  • Animated fake caret for active slot
  • OnComplete callback when all slots are filled
  • Placeholder character support
  • Password manager badge detection
  • EditContext/Form validation integration

Installation

bash
dotnet add package SummitUI

Anatomy

Import the components and structure them as follows:

razor
<SmOtpRoot @bind-Value="otpValue" MaxLength="6">
    @foreach (var slot in context.Slots)
    {
        <SmOtpSlot Slot="slot">
            <SmOtpCaret />
        </SmOtpSlot>
    }
</SmOtpRoot>

Sub-components

OtpRoot

The root container with a hidden input that manages state and provides render context.

OtpSlot

Visual slot component displaying a single character with active state.

OtpCaret

Animated blinking caret shown in active empty slots.

API Reference

OtpRoot

Property Type Default Description
MaxLengthrequired int - Number of slots for the OTP code
Value string? null Current OTP value
ValueChanged EventCallback<string?> - Callback when value changes
ValueExpression Expression<Func<string?>>? - Expression for form field identification
Disabled bool false Prevents interaction with the input
Pattern string? null Regex pattern to validate input (e.g., "[0-9]*")
Placeholder string? null Placeholder characters for empty slots
TextAlign OtpTextAlign Left Text alignment (Left, Center, Right)
InputMode string "numeric" Mobile keyboard type
AutoComplete string "one-time-code" Autofill hint for browsers
Name string? null Form field name
Id string? null ID for label association using 'for' attribute
AriaLabel string? null Accessible label when no visible label exists
AriaLabelledBy string? null ID of element that labels this input
OnComplete EventCallback<string> - Callback when all slots are filled
PasteTransformer Func<string, string>? - Transform pasted text (e.g., remove dashes)
ChildContent RenderFragment<OtpRenderContext>? - Custom slot rendering template

OtpSlot

Property Type Default Description
Slotrequired OtpSlotState - Slot state from render context
ChildContent RenderFragment? - Content for active empty slot (e.g., OtpCaret)

OtpCaret

Property Type Default Description
Class string? - CSS class for the caret element

Render Context

The OtpRoot component provides a render context with the following properties:

Property Type Description
Slots IReadOnlyList<OtpSlotState> List of slot states for rendering
IsFocused bool Whether the input is currently focused
IsHovering bool Whether the mouse is hovering over the input

OtpSlotState Properties

Property Type Description
Index int Zero-based index of this slot
Char char? The character in this slot, or null if empty
PlaceholderChar char? The placeholder character for this slot
IsActive bool Whether this slot is currently selected/active
HasFakeCaret bool Whether to show a fake caret (IsActive and no character)

Examples

Basic Usage

razor
<SmOtpRoot @bind-Value="code" MaxLength="6" />

With Fake Caret

Add a blinking caret animation for active empty slots.

razor
<SmOtpRoot @bind-Value="code" MaxLength="6" class="otp-container">
    @foreach (var slot in context.Slots)
    {
        <SmOtpSlot Slot="slot" class="otp-slot">
            <SmOtpCaret class="otp-caret" />
        </SmOtpSlot>
    }
</SmOtpRoot>

<style>
    .otp-caret {
        width: 2px;
        height: 1.5rem;
        background: currentColor;
        animation: caret-blink 1s ease-out infinite;
    }
    
    @@keyframes caret-blink {
        0%, 70%, 100% { opacity: 1; }
        20%, 50% { opacity: 0; }
    }
</style>

With Separator

Add visual separators between slot groups (e.g., 123-456).

razor
<SmOtpRoot @bind-Value="code" MaxLength="6" class="otp-container">
    @foreach (var slot in context.Slots.Take(3))
    {
        <SmOtpSlot Slot="slot" class="otp-slot" />
    }
    <span class="separator">-</span>
    @foreach (var slot in context.Slots.Skip(3))
    {
        <SmOtpSlot Slot="slot" class="otp-slot" />
    }
</SmOtpRoot>

With Pattern Validation

Restrict input to specific characters using regex.

razor
@* Digits only *@
<SmOtpRoot @bind-Value="code" MaxLength="6" Pattern="[0-9]*" />

@* Alphanumeric *@
<SmOtpRoot @bind-Value="code" MaxLength="6" Pattern="[A-Za-z0-9]*" />

OnComplete Callback

Execute code when all slots are filled.

razor
@code {
    private string code = "";
    
    private async Task HandleComplete(string value)
    {
        // Auto-submit or verify the code
        await VerifyOtpAsync(value);
    }
}

<SmOtpRoot @bind-Value="code" MaxLength="6" OnComplete="HandleComplete" />

Form Integration

Use with EditForm for validation.

razor
@code {
    private VerificationModel model = new();
    
    private async Task HandleSubmit()
    {
        await VerifyCodeAsync(model.Code);
    }
    
    public class VerificationModel
    {
        [Required]
        [StringLength(6, MinimumLength = 6)]
        public string Code { get; set; } = "";
    }
}

<EditForm Model="model" OnValidSubmit="HandleSubmit">
    <DataAnnotationsValidator />
    
    <SmOtpRoot @bind-Value="model.Code" MaxLength="6" Name="verificationCode">
        @foreach (var slot in context.Slots)
        {
            <SmOtpSlot Slot="slot" class="otp-slot" />
        }
    </SmOtpRoot>
    
    <ValidationMessage For="() => model.Code" />
    
    <button type="submit">Verify</button>
</EditForm>

Styling

Data Attributes

Attribute Element Description
data-otp-root OtpRoot Root container element
data-otp-slot OtpSlot Visual slot element
data-otp-caret OtpCaret Fake caret element
data-otp-input OtpRoot Hidden input element
data-active OtpSlot Present when slot is selected
data-focused OtpRoot Present when input is focused
data-disabled OtpRoot Present when input is disabled
data-placeholder-shown OtpRoot Present when no value is entered

CSS Example

css
/* Container */
.otp-container {
    display: inline-flex;
    gap: 0.5rem;
}

/* Individual slot */
.otp-slot {
    width: 2.5rem;
    height: 3rem;
    border: 2px solid #ccc;
    border-radius: 0.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.25rem;
    font-weight: 600;
}

/* Active slot */
.otp-slot[data-active] {
    border-color: #0066cc;
    box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
}

/* Placeholder text */
.otp-slot [data-otp-placeholder] {
    color: #999;
}

/* Disabled state */
[data-disabled] .otp-slot {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Fake caret */
.otp-caret {
    width: 2px;
    height: 1.5rem;
    background: currentColor;
    animation: caret-blink 1s ease-out infinite;
}

@@keyframes caret-blink {
    0%, 70%, 100% { opacity: 1; }
    20%, 50% { opacity: 0; }
}

Accessibility

Screen Reader Support

Unlike multi-input OTP implementations, this component uses a single hidden input element. This means screen readers see one text field instead of multiple, providing a much better user experience for assistive technology users.

Keyboard Navigation

Key Action
0-9 / a-z Enter character at current position
Backspace Delete character before cursor
Delete Delete character after cursor
ArrowLeft / ArrowRight Move cursor position
Ctrl/Cmd + V Paste from clipboard
Ctrl/Cmd + A Select all

Label Association

For proper screen reader support, always associate a label with the OTP input using one of these patterns:

razor
@* Recommended: Use for+id for clickable labels *@
<label for="verification-code">Enter verification code</label>
<SmOtpRoot Id="verification-code" MaxLength="6" />

@* Alternative: Use aria-labelledby for non-clickable labels *@
<span id="otp-label">Enter verification code</span>
<SmOtpRoot AriaLabelledBy="otp-label" MaxLength="6" />

@* For hidden labels (icon-only UI) *@
<SmOtpRoot AriaLabel="Enter 6-digit verification code" MaxLength="6" />

HTML Attributes

  • autocomplete: Set to one-time-code by default for browser/SMS autofill support
  • inputmode: Set to numeric by default to show numeric keyboard on mobile
  • maxlength: Set to MaxLength parameter value
An unhandled error has occurred. Reload X