OtpTimer
MD · 1950B gzip budgetCountdown timer for OTP resend with accessible remaining time.
- Category
- Forms & input
- Budget
- md, several states or a live subscription
- External runtime
- React / React DOM only
axe fixtureSSR rendersource scanbundle budget
Preview
Live fixtureDidn't receive a code?
Code expires in 1:00
Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.
Install
npx shadcn@latest add https://gear5-ui.vercel.app/r/otp-timer.jsonCopies the source into your project. Pulls in 2 shared registry items: announce, cn.
New here? Set up Tailwind and import aliases first →Source & props
Edit on GitHub ↗Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.
"use client";
import { useId, useEffect, useMemo, useState } from "react";
import { announce } from "../lib/announce";
import { cn } from "../lib/cn";
export interface OtpTimerProps {
/** Duration in seconds. */
duration: number;
onResend: () => void;
label?: string;
className?: string;
}
export function OtpTimer({ duration, onResend, label = "Didn't receive a code?", className }: OtpTimerProps) {
const id = useId();
const [remaining, setRemaining] = useState(duration);
const canResend = useMemo(() => remaining <= 0, [remaining]);
useEffect(() => {
if (remaining <= 0) {
return;
}
const timer = setInterval(() => {
setRemaining((r) => {
if (r <= 1) {
clearInterval(timer);
return 0;
}
return r - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [remaining]);
const handleResend = () => {
setRemaining(duration);
onResend();
announce("New code sent", "polite");
};
const minutes = Math.floor(remaining / 60);
const seconds = remaining % 60;
const timeStr = `${minutes}:${seconds.toString().padStart(2, "0")}`;
return (
<div className={cn("flex flex-col gap-1.5 text-start", className)}>
<span id={id} className="text-sm text-neutral-600 dark:text-neutral-400">
{label}
</span>
<div className="flex items-center gap-3">
{!canResend ? (
<div aria-live="polite" className="flex items-center gap-2">
<span aria-hidden="true" className="font-mono text-lg tabular-nums">
{timeStr}
</span>
<span className="sr-only">Code expires in {timeStr}</span>
</div>
) : (
<button
type="button"
onClick={handleResend}
className="text-sm font-medium text-blue-600 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:text-blue-400"
>
Resend code
</button>
)}
</div>
</div>
);
}
What CI checks
Quality contract- Bundled, minified and gzipped against its tier budget
- Audited by axe in the state previewed above
- Rendered through react-dom/server with no browser globals
- Scanned for network calls, dangerous sinks, and unguarded animation