OTP Field

One-time passcode entry composed on top of InputOTP. Adds a countdown to expiry, a resend button with cooldown, a multi-channel switcher (Email / WhatsApp / SMS), and an error state for the TpEMIS dual-identity verification flow.

Default — phone OTP

Five-minute expiry, channel switch, resend cooldown of 60 seconds. Try changing the channel; the masked target stays the same so the applicant can switch delivery without re-entering their number.

Code sent to +232 76 ***** 56 via SMS.
3 attempts remaining
Submitted: (none)Resend count: 0

Error state

Pre-filled with an incorrect code and an error message. Slots get a destructive border; the error renders inside a role="alert" so screen readers announce it.

Code sent to appli***@university.edu.sl via Email.
1
2
3
4
5
6
2 attempts remaining

Single channel

Omit onChannelChange (or pass a one-element channels array) to render the field without the channel switcher.

Code sent to appli***@university.edu.sl via Email.

SSG-safety

The countdown reads Date.now(), which differs between prerender and hydration. OtpField only reads time inside a useEffect tick; the initial render emits a deterministic empty timer so the prerendered HTML matches what hydration produces. Pass expiresAt via state set inside useEffectrather than at module / render time.

Accessibility

  • Inherits the OTP input's native one-character-per-slot keyboard nav.
  • The ticking timer sits in an aria-live="off" node — second-by-second changes stay silent so screen readers aren't spammed. Only the one-shot expiry state flips into a role="status" region.
  • Errors render inside role="alert" for instant SR announcement.
  • The channel switcher button carries an explicit aria-label.
  • The field itself carries aria-invalid when error is set; slots get a destructive border in the same state.