Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Buttons

Free

Countdown Button for React

A button that runs its action, then counts the seconds down in its label before it can be pressed again. For React with Base UI, for resending a code or retrying after a rate limit.

Framework
UI library

Press it and your handler runs, then the label counts down, Wait 30s, Wait 29s, while presses do nothing. A fill behind the label drains as the time runs out. When it's empty the label comes back, the button works again and onComplete fires.

Use it for a Resend code button after a text message, or a retry that a server limits. It wraps the Button, so variant and size work as usual. The digits are tabular and the longest count is laid out unseen, so the width never moves. The fill is the clock: a CSS keyframe animation the install adds to your stylesheet, lasting seconds, whose end is the count's end, so it can't drift; the digits tick once a second. With reduced motion the fill is hidden and the labels only fade.

Preview width

Installation

Sign in to get your install command
Free with an account. It carries your token, so the CLI installs everything in one go.

Usage

import { CountdownButton } from "@/components/ui/countdown-button";
<CountdownButton onClick={resendCode} seconds={30}>
  Resend code
</CountdownButton>

Demos

Controlled

Turn off autoStart and pass running to start the count yourself, here once the server answers that there were too many attempts. Set running back to false in onComplete. The handler ignores presses while the request is out, and the label says Checking….

Preview width

Without the Fill

showProgress={false} leaves out the draining fill, so only the label counts. Next to it, the same button with the fill.

Preview width

Accessibility

The fill and the counting label are hidden from assistive tech; the label at rest keeps naming the button. When the count starts, a polite status inside the button says the first count, such as Resend in 30s, once; it doesn't read every second. While counting, the button is marked aria-disabled and ignores presses, but keeps its focus.

Enter
Run the action and start the count, unless it's counting.
Space
Run the action and start the count, unless it's counting.

FAQ

How do I wire it to my resend call?

Pass the call as the press handler; the count starts with it. seconds is 30 by default. While it counts, the handler isn't called.

How do I show Resend in 30s?

Pass countingLabel, a function that gets the seconds left and returns the label: (s) => 'Resend in ' + s + 's'. The default says Wait 30s.

Can the count wait until my request succeeds?

Yes. Set autoStart to false and pass running when the response comes back, with the server's own wait in seconds if it sends one. The Controlled demo does this.

Does the count stay on time?

Yes. The draining fill is a CSS animation that lasts seconds, and the count ends when it does, so ticks running late can't add up. In a background tab, onComplete fires when you come back to it.

Why doesn't the label jump as the digits change?

The digits are tabular, and the label for the full count is laid out unseen behind the others, so the button stays as wide when 10 becomes 9.

Can it be pressed while it counts?

No. It's marked aria-disabled and ignores presses, and it keeps its focus, so a keyboard user can wait on it. It also keeps its full color, since the count says why it's waiting.

What happens with reduced motion?

The fill is hidden and the labels swap with a fade. The count still ticks and still ends on time.

API Reference

CountdownButton

data-slot="countdown-button-state"

PropType
autoStart?boolean

Start counting when the button is pressed. Turn it off to start the count yourself with running.

Default
true
children?ReactNode

The label at rest; it names the button while it counts too.

countingLabel?(remaining: number) → ReactNode

The label while it counts, from the seconds left. Ark default: (seconds) => Wait ${seconds}s``.

Default
waitFor
Parameters

remaining – number

Returns
ReactNode
focusableWhenDisabled?boolean

Whether the button should be focusable when disabled.

Default
false
nativeButton?boolean

Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).

Default
true
onComplete?() → void

Called when the count reaches zero and the button can be pressed again.

Returns
void
running?boolean

Whether it's counting, to start and stop the count yourself (from a server's retry-after, say). Set it back to false in onComplete.

seconds?number

How long the button waits before it can be pressed again, in seconds.

Default
30
showProgress?boolean

Show a fill behind the label that drains as the time runs out.

Default
true
render?ReactElement | ((props: object, state: object) => ReactElement)

Replace the host element or compose it with another component.

Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string

Merged with the part's classes.

AttributeType
data-state?string

"counting" when counting is true, otherwise "idle".

CSS variableType
--countdown-button-durationstring

The whole count, in seconds (30s).

All 35 buttons