Ask AI about this page

OTP Input Component

Introduction

The otp component provides a secure and user-friendly way to handle One-Time Password (OTP) input. It features automatic focus progression, intelligent shift-left deletion, paste handling, validation with custom patterns, click-to-focus on disabled inputs, completion events, accessibility support, and seamless integration with both Livewire and Alpine.js. Perfect for authentication flows, verification codes, and any multi-digit input scenarios.

Installation

Use the sheaf artisan command to install the otp component easily:

php artisan sheaf:install otp

Basic Usage

<x-ui.otp 
    wire:model="verificationCode"
/>

Bind To Livewire

To use with Livewire you need to only use wire:model="code" to bind your OTP state:

<!--
    this assumes you have $verificationCode in your class component
-->

<div class="max-w-md mx-auto">
    <x-ui.otp 
        wire:model="verificationCode"
    />
</div>

For real-time synchronization, use the .live modifier:

<x-ui.otp 
    wire:model.live="verificationCode"
/>

Using it within Blade & Alpine

You can use it outside Livewire with just Alpine (with Blade):

<div class="max-w-md mx-auto" x-data="{ code: null }">
    <x-ui.otp 
        x-model="code" 
    />
</div>

Note: The component uses _state and _inputs internally, so avoid using these variable names in your Alpine scope.

Customization

Custom Length

Control the number of input fields with the length parameter.

<x-ui.otp 
    wire:model="code4"
    :length="4"
/>
<x-ui.otp 
    wire:model="code6"
    :length="6"
/>
<x-ui.otp 
    wire:model="code8"
    :length="8"
/>

Slotted Inputs

You can pass individual inputs as a slot and customize them as needed.

Note: Use <x-ui.otp.input> not the native input element.

<x-ui.otp
    wire:model="code"
    :length="5"
>
    <x-ui.otp.input class="rounded-full m-2" />
    <x-ui.otp.input class="rounded-full m-2"/>
    <x-ui.otp.input class="rounded-full m-2"/>
    <x-ui.otp.input class="rounded-full m-2"/>
    <x-ui.otp.input class="rounded-full m-2"/>
</x-ui.otp>    

Separator

Add visual separators between input groups for better readability (e.g., for phone numbers or formatted codes).

<x-ui.otp
    wire:model="code"
    :length="6"
>
    <x-ui.otp.input />
    <x-ui.otp.input/>
    
    <x-ui.otp.separator/>
    
    <x-ui.otp.input />
    <x-ui.otp.input />
    
    <x-ui.otp.separator/>
    
    <x-ui.otp.input />
    <x-ui.otp.input />
</x-ui.otp>    

Input Types and Validation

Control what characters are allowed with different input types and patterns.

<x-ui.otp 
    wire:model="numericCode"
    allowedPattern="[0-9]"
/>
<x-ui.otp 
    wire:model="alphanumericCode"
    allowedPattern="[A-Za-z0-9]"
/>
<x-ui.otp 
    wire:model="letterCode"
    allowedPattern="[A-Za-z]"
/>

AutoFocus

Automatically focus the first relevant input on component initialization:

<x-ui.otp 
    wire:model="code"
    autofocus
/>

Advanced Features

Intelligent Shift-Left Deletion

When you delete a digit from the middle of the OTP, all subsequent digits automatically shift left to fill the gap. This creates a natural editing experience similar to a text input field.

Place cursor on any digit and press backspace/delete to see the shift effect.

Example behavior:

  • Initial: [1][2][3][4]
  • Delete 2: [1][3][4][ ] (values shift left)
  • No gaps remain between digits

Click-to-Focus on Disabled Inputs

Click anywhere in the OTP input container, even on disabled inputs, to automatically focus the appropriate input box.

Try clicking on neutraled-out (disabled) inputs - focus will jump to the next available input.

How it works:

  • Click on an enabled input → focuses that input
  • Click on a disabled input → focuses the first empty input
  • Click on empty space → focuses the first empty input

Technical Note: This feature uses a CSS ::after pseudo-element overlay trick to capture click events on disabled inputs, which normally block all pointer events.

Completion Events

Listen for the otp-complete event when all digits are filled:

<x-ui.otp 
    wire:model="code"
    x-on:otp-complete="handleComplete($event.detail.code)"
/>

Auto-submit example:

<form wire:submit="verify">
    <x-ui.otp 
        wire:model.live="code"
        x-on:otp-complete="$wire.submit()"
    />
</form>

Pre-filled Values

The component automatically handles external state synchronization and can display pre-filled values.

with Livewire

in your component class make sure you have defined $prefilledCode property with inital 1234 as initial value

Current value:

<div>
    <x-ui.otp wire:model.live="prefilledCode" />
    <p class="text-sm text-neutral-600 mt-2">Current value: <span wire:text="prefilledCode"></span></p>
</div>

with Alpine

Current value:

<div x-data="{ code: '1234' }">
    <x-ui.otp x-model="code" />
    <p class="text-sm text-neutral-600 mt-2">Current value: <span x-text="prefilledCode"></span></p>
</div>

Paste Handling

The component intelligently handles pasted content, filtering valid characters, clearing subsequent inputs, and auto-filling from the paste position.

The component will extract valid digits and fill the inputs automatically.

Paste behavior:

  • Filters characters by allowedPattern
  • Clears all inputs from paste position onward
  • Fills sequentially with valid characters
  • Focuses next empty or last filled input

Real-time Validation

Monitor input changes in real-time with Alpine.js effects.

<div 
    x-data="{
        code: null,
        isValid: false,
    }"
    x-init="
        $nextTick(() => {
            Alpine.effect(() => {
                isValid = code && code.length === 4;
            });
        });
    "
>
    <label class="block text-sm font-medium mb-2">Enter 4-digit code</label>
    <x-ui.otp x-model="code" />
    <div class="mt-2">
        <span x-show="isValid" class="text-green-600 text-sm">✓ Valid code entered</span>
        <span x-show="!isValid" class="text-red-600 text-sm">Please enter a complete 4-digit code</span>
    </div>
</div>

Public API

The component exposes methods that can be called from outside:

Clear All Inputs

<button x-on:click="$dispatch('otp-clear')">Clear OTP</button>

<x-ui.otp wire:model="code" />

Focus First Empty Input

<button x-on:click="$dispatch('otp-focus')">Focus OTP</button>

<x-ui.otp wire:model="code" />

Accessibility

The component includes comprehensive accessibility features:

  • ARIA labels: Each input has a descriptive label (e.g., "Digit 1 of 4")
  • Autocomplete: autocomplete="one-time-code" for better mobile support
  • Keyboard navigation: Arrow keys move between inputs
  • Screen reader friendly: Proper roles and labels
  • Focus management: Clear visual focus indicators

Component Props

Prop Name Type Default Required Description
length integer 4 No Number of input fields to render
type string 'text' No HTML input type attribute
allowedPattern string '[0-9]' No Regex pattern for allowed characters
autofocus boolean false No Auto-focus first input on mount
wire:model string - Yes* Livewire property to bind to
x-model string - Yes* Alpine.js property to bind to
class string - No Additional CSS classes for container

*Either wire:model or x-model is required

Events

Event Name Payload Description
otp-complete { code: string } Fired when all inputs are filled
otp-clear - Clears all inputs (listen with window)
otp-focus - Focuses first empty input (listen with window)

Technical Notes

Livewire Integration

  • Uses Livewire.hook('morphed') to handle DOM diffing
  • Refreshes data-order attributes after each morph
  • Supports both regular and .live model bindings
  • State synchronization happens automatically

Browser Compatibility

  • Works across Chrome, Firefox, Safari, and Edge
  • Uses requestAnimationFrame() for consistent focus behavior
  • Firefox-specific workarounds for select() timing issues

Performance

  • Minimal re-renders with smart state management
  • Uses $nextTick() to prevent race conditions
  • Efficient DOM queries with cached input references

Edge Cases Handled

  • Partial paste content
  • Invalid character filtering
  • Multi-character input (takes last character)
  • Clicking disabled inputs (::after overlay trick)
  • Livewire morph attribute stripping
  • External state changes (SSR-safe)
  • Delete key treated as backspace for consistency

© SheafUI Copyright 2024-2025. All rights reserved.