shadcn.io is an independent project built on shadcn/ui
Buttons
FreeCountdown Button for Vue
Runs its action, then counts the seconds down in its label until it works again. For Vue with Ark UI, to resend a code or retry after a rate limit.
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
<script setup lang="ts">
import { CountdownButton } from "@/components/ui/countdown-button";
</script>
<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?VNode
The label at rest; it names the button while it counts too.
clickEffect?boolean
Apply a click effect to the button.
- Default
true
countingLabel?(remaining: number) → VNode
The label while it counts, from the seconds left. Ark default: (seconds) => Wait ${seconds}s``.
- Default
waitFor- Parameters
remaining– number- Returns
ReactNode
isLoading?boolean
Show a loading indicator.
- Default
false
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
asChild?boolean
Use the provided child element as the default rendered element, combining their props and behavior.
class?string
Merged with the part's classes.
@complete() → void
Called when the count reaches zero and the button can be pressed again.
- Returns
void
data-state?string
"counting" when counting is true, otherwise "idle".
--countdown-button-durationstring
The whole count, in seconds (30s).





