- Accordion
- Action Bar
- Action Toast
- Alert Banner
- Alert Dialog
- Alert
- Alphabet Index
- Amount Field
- Aspect Ratio
- Attachment
- Availability List
- Avatar
- Badge
- Bank Card
- Breadcrumb
- Bubble
- Button Group
- Button
- Calendar
- Camera Frame
- Card Design Picker
- Card Details
- Card Peek
- Card Stack
- Card
- Carousel
- Chart
- Chat Thread
- Checkbox
- Chip Group
- Choice Tile
- Code Block
- Code Dots
- Collapsible
- Combobox
- Command
- Context Menu
- Copy Field
- Countdown
- Country Select
- Data Table
- Date Field
- Date Picker
- Date Range Picker
- Dialog
- Direction
- Document Paper
- Drawer
- Dropdown Menu
- Empty
- Entity Avatar
- Field
- File Trigger
- File Upload Item
- Hover Card
- Input Group
- Input OTP
- Input
- Item
- Kbd
- Key Value
- Keypad
- Label
- Live Region
- Marker
- Message Scroller
- Message
- Money Amount
- Native Select
- Number Field
- Pagination
- Percent Split Editor
- Phone Field
- Popover
- Progress
- QR Code
- Questionnaire
- Radio Group
- Range Histogram
- Rating
- Resizable
- Scroll Area
- Search Field
- Select
- Separator
- Share Bar
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Sortable List
- Spinner
- Status Timeline
- Step Header
- Steps List
- Swipe Row
- Switch
- Table
- Tabs
- Text Field
- Textarea
- Tier Ladder
- Toggle Group
- Toggle
- Token Search Field
- Tooltip
- Trace
- Transaction Row
- Usage Meter
"use client"
import * as React from "react"The Countdown component shows the time left until a deadline. Use it for rate locks, code resends, deposit address lifetimes, approvals and lockouts. It counts down once per second, shows 0:00 muted when the time is up and speaks only at a few milestones, so screen reader users are not interrupted every second. Pick it over Progress when the limit is time rather than an amount.
Installation#
pnpm dlx shadcn@latest add @dots/countdown
Usage#
import { Countdown } from "@/components/ui/countdown"<Countdown endsAt={rateLockEndsAt} duration={30} variant="ring" label="Rate lock" />Set the deadline in an effect or from server data, not with Date.now() during render. The component reads the clock only in effects, so the server and the first client render both show a placeholder.
const [endsAt, setEndsAt] = React.useState<number | null>(null)
React.useEffect(() => {
setEndsAt(Date.now() + 30 * 1000)
}, [])Features#
- Accepts a
Dateor a timestamp in milliseconds, andnullwhile loading - Formats as
0:42,1:02:05or2d 04:13:07 - An optional ring that empties over
duration - Five sizes, from inline text to a hero timer
- A
warningtone for deadlines where money can be lost onExpirefires once per deadline- Sparse polite announcements, configurable with
announce - A
useCountdownhook for driving the rest of the screen, such as enabling a button
Ring#
Set variant="ring" and pass the whole window as duration in seconds. The ring starts full and empties as time runs out.
"use client"
import * as React from "react"Sizes#
Use size to switch between inline, sm, default, lg and display. display is for screens that are about the wait itself. Its digits step down for waits of a day or more so they fit a phone column.
"use client"
import * as React from "react"Inline#
Use size="inline" to keep the surrounding text size. Set spokenLabel={false} when the sentence already names the deadline, and announce="expiry" to speak only when the wait is over.
Send a new code in -:--loading
"use client"
import * as React from "react"Warning#
Set tone="warning" only where money can be lost after the deadline, such as a deposit address that stops accepting funds. Expiry on its own is not an error.
"use client"
import * as React from "react"Display#
Combine size="display" with the ring for a lockout or a recovery hold.
"use client"
import * as React from "react"Several timers#
When a screen shows several long timers, set announce="off" on each. The digits can still be read on demand.
"use client"
import * as React from "react"Pending and expired#
endsAt={null} shows -:-- while the deadline loads. A deadline in the past shows 0:00 muted with an empty ring.
import { Countdown } from "@/components/ui/countdown"
export function CountdownStates() {Accessibility#
The digits sit in an element with role="timer". They are hidden from screen readers and replaced with words, such as "Rate lock: 24 seconds left", which are read when the user moves to the timer.
- Pass a
labelthat names what runs out. It prefixes the spoken time and every announcement. - A separate polite region speaks at 5 minutes, 1 minute, every 10 seconds in the last minute, and at expiry. Long deadlines stay quiet until the last 5 minutes.
- Use
announce="expiry"for a single wait that unlocks something, andannounce="off"when several timers share a screen or the screen announces the outcome itself. - Keep the countdown mounted through expiry and let it show
0:00. Removing it at zero also removes the "time is up" announcement. - Color is not the only signal: the expired state also shows
0:00, andtone="warning"should come with text that explains the risk.
API Reference#
Countdown#
Accepts all span props except children.
| Prop | Type | Default | Description |
|---|---|---|---|
endsAt | Date | number | null | undefined | - | Required. When the time is up. null shows a placeholder. |
duration | number | - | The whole window in seconds. Drives the ring. |
variant | "digits" | "ring" | "digits" | Show the time only, or add a ring that empties as time runs out. |
size | "inline" | "sm" | "default" | "lg" | "display" | "default" | inline keeps the surrounding text size. display is the hero timer of a screen. |
tone | "default" | "warning" | "default" | warning only where money can be lost after the deadline. |
label | string | "Countdown" | What runs out, for screen readers. |
spokenLabel | boolean | true | false drops the label from the timer text. Announcements keep it. |
expiredMessage | string | "Time is up" | Spoken when the time is up. |
announce | "milestones" | "expiry" | "off" | "milestones" | What the polite region speaks. |
onExpire | () => void | - | Called once when the time is up. |
className | string | - | Additional classes to apply to the root element. |
useCountdown#
Ticks once per second toward a deadline. Reads the clock only in effects, so the first render matches on server and client.
const { remaining, seconds, expired } = useCountdown(endsAt, onExpire)| Argument | Type | Description |
|---|---|---|
endsAt | Date | number | null | undefined | The deadline. |
onExpire | () => void | Optional. Fires once per deadline at zero. |
| Return | Type | Description |
|---|---|---|
remaining | number | null | Milliseconds left, or null before the first tick. |
seconds | number | null | Whole seconds left, rounded up, or null before the first tick. |
expired | boolean | true once the time is up. |
formatClock#
Formats whole seconds the way the component shows them: 42 gives "0:42", 3725 gives "1:02:05", two days give "2d 00:00:00".
formatClock(seconds: number): string