Forms form

OTPInput

Professional form control.

Preview

Component preview not available for: otpinput

Ensure the filename matches Otpinput.astro

Usage

Copy the full block below to use this component with its imports.

astro
---
import { OTPInput } from 'astro-component-kit';
---

<OTPInput />
--- import { OTPInput } from 'astro-component-kit'; --- <OTPInput />

Manual Installation

If you are not using the npm package, create a new file src/components/lib/OTPInput.astro and paste the following code:

astro
---
/**
 * OTPInput — A specialized input group for One-Time Password verification codes.
 * 
 * @param {string} label - Optional grouping label title.
 * @param {string} id - Base HTML ID for the inputs.
 * @param {number} length - Number of OTP digits. Default is 4.
 * @param {string} name - Base name for form submission (will be indexed).
 */
interface Props {
  label?: string;
  id: string;
  length?: number;
  name?: string;
}

const { label, id, length = 4, name = "otp" } = Astro.props;
---

<div class="otp-container">
  {label && <label class="otp-label">{label}</label>}
  <div class="otp-wrap" data-otp-group>
    {Array.from({ length }).map((_, i) => (
      <input 
        type="text" 
        inputmode="numeric"
        maxlength="1" 
        pattern="[0-9]*"
        autocomplete="one-time-code"
        id={`${id}-${i}`}
        name={`${name}[]`}
        class="otp-field"
        data-otp-index={i}
      />
    ))}
  </div>
</div>

<style>
  .otp-container { display: flex; flex-direction: column; align-items: center; gap: var(--sp-4, 1rem); }
  .otp-label { font-size: 0.9rem; font-weight: 600; color: var(--c-text-2, #94a3b8); }
  
  .otp-wrap { display: flex; gap: 0.75rem; justify-content: center; }
  
  .otp-field { 
    width: 50px; height: 60px; 
    background: var(--c-bg-elev, rgba(255,255,255,0.05)); 
    border: 1px solid var(--c-border, rgba(255,255,255,0.1)); 
    border-radius: var(--r-md, 12px); 
    text-align: center; color: var(--c-text-1, #fff); 
    font-size: 1.75rem; font-weight: 800; 
    outline: none; transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1); 
  }
  
  .otp-field:focus { 
    border-color: var(--c-primary, #6366f1); 
    background: rgba(99, 102, 241, 0.1); 
    box-shadow: 0 0 15px rgba(99, 102, 241, 0.3), 0 0 0 3px rgba(99, 102, 241, 0.15); 
    transform: translateY(-2px);
  }
</style>

<script>
  document.querySelectorAll('[data-otp-group]').forEach(group => {
    const inputs = group.querySelectorAll('input') as NodeListOf<HTMLInputElement>;
    
    inputs.forEach((input, idx) => {
      // Auto-advance
      input.addEventListener('input', (e) => {
        const val = (e.target as HTMLInputElement).value;
        if (val && idx < inputs.length - 1) {
          inputs[idx + 1].focus();
        }
      });

      // Backspace logic
      input.addEventListener('keydown', (e) => {
        if (e.key === 'Backspace' && !input.value && idx > 0) {
          inputs[idx - 1].focus();
        }
      });

      // Paste logic
      input.addEventListener('paste', (e) => {
        e.preventDefault();
        const data = e.clipboardData?.getData('text').slice(0, inputs.length).split('') || [];
        data.forEach((val, i) => {
          if (inputs[i]) inputs[i].value = val;
        });
        if (inputs[data.length - 1]) inputs[data.length - 1].focus();
      });
    });
  });
</script>
--- /** * OTPInput — A specialized input group for One-Time Password verification codes. * * @param {string} label - Optional grouping label title. * @param {string} id - Base HTML ID for the inputs. * @param {number} length - Number of OTP digits. Default is 4. * @param {string} name - Base name for form submission (will be indexed). */ interface Props { label?: string; id: string; length?: number; name?: string; } const { label, id, length = 4, name = "otp" } = Astro.props; --- <div class="otp-container"> {label && <label class="otp-label">{label}</label>} <div class="otp-wrap" data-otp-group> {Array.from({ length }).map((_, i) => ( <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]*" autocomplete="one-time-code" id={`${id}-${i}`} name={`${name}[]`} class="otp-field" data-otp-index={i} /> ))} </div> </div> <style> .otp-container { display: flex; flex-direction: column; align-items: center; gap: var(--sp-4, 1rem); } .otp-label { font-size: 0.9rem; font-weight: 600; color: var(--c-text-2, #94a3b8); } .otp-wrap { display: flex; gap: 0.75rem; justify-content: center; } .otp-field { width: 50px; height: 60px; background: var(--c-bg-elev, rgba(255,255,255,0.05)); border: 1px solid var(--c-border, rgba(255,255,255,0.1)); border-radius: var(--r-md, 12px); text-align: center; color: var(--c-text-1, #fff); font-size: 1.75rem; font-weight: 800; outline: none; transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .otp-field:focus { border-color: var(--c-primary, #6366f1); background: rgba(99, 102, 241, 0.1); box-shadow: 0 0 15px rgba(99, 102, 241, 0.3), 0 0 0 3px rgba(99, 102, 241, 0.15); transform: translateY(-2px); } </style> <script> document.querySelectorAll('[data-otp-group]').forEach(group => { const inputs = group.querySelectorAll('input') as NodeListOf<HTMLInputElement>; inputs.forEach((input, idx) => { // Auto-advance input.addEventListener('input', (e) => { const val = (e.target as HTMLInputElement).value; if (val && idx < inputs.length - 1) { inputs[idx + 1].focus(); } }); // Backspace logic input.addEventListener('keydown', (e) => { if (e.key === 'Backspace' && !input.value && idx > 0) { inputs[idx - 1].focus(); } }); // Paste logic input.addEventListener('paste', (e) => { e.preventDefault(); const data = e.clipboardData?.getData('text').slice(0, inputs.length).split('') || []; data.forEach((val, i) => { if (inputs[i]) inputs[i].value = val; }); if (inputs[data.length - 1]) inputs[data.length - 1].focus(); }); }); }); </script>

Quick Info

Category
Forms
Filename
OTPInput.astro
Dependencies
None — pure Astro + CSS
Tags
form