shadcn.io is not affiliated with official shadcn/ui
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.
Installation
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….
Without the Fill
showProgress={false} leaves out the draining fill, so only the label counts. Next to it, the same button with the fill.
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.
API Reference
CountdownButton
data-slot="countdown-button-state"
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.
data-state?string
"counting" when counting is true, otherwise "idle".
--countdown-button-durationstring
The whole count, in seconds (30s).
Related
Loading Button
Progress & resultsWhile the request itself runs
- Pro
Download Button
Progress & resultsA fill driven by real progress
- Pro
Success Button
Progress & resultsConfirm the code was sent
- Pro
Long Press Button
ControlsTime-based, but held by the user
- Pro
Counter Button
ControlsNumbers that tick up instead
- Pro
Shake Button
Motion & feedbackSay the attempt was refused