PinInput (Kode OTP)
PinInput memecah kode verifikasi SMS / WhatsApp menjadi 6 kotak masukan terpisah, memudahkan warga memverifikasi angka tanpa salah ketik (Hukum Miller & Chunking).
Pratinjau Komponen
Penggunaan CSS Murni
<div class="mjl-pin-input">
<input type="text" maxlength="1" inputmode="numeric" class="mjl-pin-input__box" autofocus>
<input type="text" maxlength="1" inputmode="numeric" class="mjl-pin-input__box">
<input type="text" maxlength="1" inputmode="numeric" class="mjl-pin-input__box">
<input type="text" maxlength="1" inputmode="numeric" class="mjl-pin-input__box">
<input type="text" maxlength="1" inputmode="numeric" class="mjl-pin-input__box">
<input type="text" maxlength="1" inputmode="numeric" class="mjl-pin-input__box">
</div>
Penggunaan di Berbagai Teknologi
Salin contoh sesuai teknologi proyek Anda. Semua versi memakai class dari @majalengka/css. Komponen siap pakai tersedia di @majalengka/vue; untuk React, Angular, Laravel, dan PHP, contoh di bawah sudah berupa pembungkus kecil yang bisa langsung disalin ke proyek.
<div class="mjl-pin-input" role="group" aria-label="Kode OTP dari SMS">
<input class="mjl-pin-input__box" inputmode="numeric" maxlength="1" autocomplete="one-time-code" aria-label="Karakter 1 dari 6">
<input class="mjl-pin-input__box" inputmode="numeric" maxlength="1" aria-label="Karakter 2 dari 6">
<input class="mjl-pin-input__box" inputmode="numeric" maxlength="1" aria-label="Karakter 3 dari 6">
<input class="mjl-pin-input__box" inputmode="numeric" maxlength="1" aria-label="Karakter 4 dari 6">
<input class="mjl-pin-input__box" inputmode="numeric" maxlength="1" aria-label="Karakter 5 dari 6">
<input class="mjl-pin-input__box" inputmode="numeric" maxlength="1" aria-label="Karakter 6 dari 6">
</div>
<script setup lang="ts">
import { ref } from 'vue';
import { MjlPinInput } from '@majalengka/vue';
const otp = ref('');
function verifikasi(kode: string) {
// kirim kode ke server
}
</script>
<template>
<MjlPinInput v-model="otp" :length="6" label="Kode OTP dari SMS" @complete="verifikasi" />
</template>
import { useRef, useState } from 'react';
export function PinInput({ length = 6, onComplete }: { length?: number; onComplete: (kode: string) => void }) {
const [nilai, setNilai] = useState<string[]>(Array(length).fill(''));
const refs = useRef<(HTMLInputElement | null)[]>([]);
function ubah(i: number, ch: string) {
if (!/^\d?$/.test(ch)) return;
const next = [...nilai];
next[i] = ch;
setNilai(next);
if (ch && i < length - 1) refs.current[i + 1]?.focus();
if (next.every(Boolean)) onComplete(next.join(''));
}
return (
<div className="mjl-pin-input" role="group" aria-label="Kode OTP dari SMS">
{nilai.map((ch, i) => (
<input key={i} ref={(el) => { refs.current[i] = el; }} className="mjl-pin-input__box" inputMode="numeric" maxLength={1}
autoComplete={i === 0 ? 'one-time-code' : 'off'} aria-label={`Karakter ${i + 1} dari ${length}`}
value={ch} onChange={(e) => ubah(i, e.target.value)}
onKeyDown={(e) => e.key === 'Backspace' && !ch && refs.current[i - 1]?.focus()} />
))}
</div>
);
}
import { Component, ElementRef, output, viewChildren } from '@angular/core';
@Component({
selector: 'mjl-pin-input',
standalone: true,
template: `
<div class="mjl-pin-input" role="group" aria-label="Kode OTP dari SMS">
@for (i of slots; track i) {
<input #box class="mjl-pin-input__box" inputmode="numeric" maxlength="1" [attr.aria-label]="'Karakter ' + (i + 1) + ' dari 6'"
(input)="onInput(i, $event)" (keydown.backspace)="onBack(i, $event)" />
}
</div>
`,
})
export class MjlPinInput {
slots = [0, 1, 2, 3, 4, 5];
complete = output<string>();
boxes = viewChildren<ElementRef<HTMLInputElement>>('box');
onInput(i: number, e: Event) {
const el = e.target as HTMLInputElement;
el.value = el.value.replace(/\D/g, '');
if (el.value && i < 5) this.boxes()[i + 1].nativeElement.focus();
const kode = this.boxes().map((b) => b.nativeElement.value).join('');
if (kode.length === 6) this.complete.emit(kode);
}
onBack(i: number, e: Event) {
if (!(e.target as HTMLInputElement).value && i > 0) this.boxes()[i - 1].nativeElement.focus();
}
}
{{-- Satu input tersembunyi menampung kode utuh untuk dikirim ke server --}}
<input type="hidden" name="otp" id="otp">
<div class="mjl-pin-input" role="group" aria-label="Kode OTP dari SMS">
@for ($i = 1; $i <= 6; $i++)
<input class="mjl-pin-input__box" inputmode="numeric" maxlength="1"
aria-label="Karakter {{ $i }} dari 6" @if ($i === 1) autocomplete="one-time-code" @endif>
@endfor
</div>
<input type="hidden" name="otp" id="otp">
<div class="mjl-pin-input" role="group" aria-label="Kode OTP dari SMS">
<?php for ($i = 1; $i <= 6; $i++): ?>
<input class="mjl-pin-input__box" inputmode="numeric" maxlength="1"
aria-label="Karakter <?= $i ?> dari 6" <?= $i === 1 ? 'autocomplete="one-time-code"' : '' ?>>
<?php endfor; ?>
</div>
Mendukung paste kode utuh, Backspace mundur, panah kiri/kanan, serta autocomplete="one-time-code" agar kode SMS bisa diisi otomatis di HP. Pakai mask untuk PIN rahasia.
Untuk HTML, Laravel, dan PHP, perpindahan fokus antarkotak dan pengisian input tersembunyi otp perlu skrip kecil di sisi browser (contoh lengkap ada di versi React/Angular).