Countdown
The time left until a moment, floored, with the caution and warning treatments as the moment nears.
On this page
Tidal gate at the Needlesdecide by 14:30 UTC
Code
import { Countdown } from "@bosunkit/components/countdown";
import type { ReactElement } from "react";
export const description: string =
"A decision point's countdown at a fixed time: decide by 14:30 UTC, 2 hr 10 min left, caution from 30 minutes.";
const NOW: number = Date.UTC(2026, 9, 17, 12, 20);
export default function Showcase(): ReactElement {
return (
<p style={{ margin: 0, display: "flex", gap: "var(--space-sm)", alignItems: "baseline", flexWrap: "wrap" }}>
<strong>Tidal gate at the Needles</strong>
<span>decide by 14:30 UTC</span>
<Countdown
to={Date.UTC(2026, 9, 17, 14, 30)}
now={NOW}
appearance="chip"
cautionWithinMs={30 * 60_000}
warningWithinMs={5 * 60_000}
/>
</p>
);
}
A countdown is the time left until a moment: a decision point's decide-by time, a tidal gate, a watch change. It
floors, so it never promises more time than there is: 2 hours 10 minutes and 59 seconds reads "in 2 hr 10 min", and
the last minute reads "in under 1 min". Once the moment passes it says "time is up", or with after="elapsed" how
long ago it was. format writes it in words, short, or as a clock, and precision="second" counts seconds.
It keeps its own clock and wakes only when its text changes: once a minute, or once a second in the last minute or
at second precision, and not at all while the page is hidden. A screen reader reads it as a timer and is not
interrupted by each tick; announceAt names the moments worth a word ("10 min left"). As the moment nears,
cautionWithinMs and warningWithinMs give it the caution (■) and warning (●) treatments, shape and color together; it
never flashes, since a countdown is not an alarm. showTarget writes the moment itself with its timeReference. A
fixed now stops the clock, for examples and printouts. The countdown takes a ref and a tabIndex, so a Tip can
describe it: the moment in full, or what the moment is for. The tooltip adds detail; the time left and, with
showTarget, the moment are on the page without it.
With appearance="chip" the countdown is drawn as a due chip, so a time left and a
date coming due look like the same kind of thing in a list. The chip takes the countdown's treatment, neutral until
cautionWithinMs, and fades only when that treatment changes, not on each tick.
Examples
Formats
Code
import { Countdown } from "@bosunkit/components/countdown";
import type { ReactElement } from "react";
export const description: string = "The same moment in words, short and as a clock, to the minute and to the second.";
const NOW: number = Date.UTC(2026, 9, 17, 12, 0, 0);
const TO: number = Date.UTC(2026, 9, 17, 13, 5, 30);
export default function Formats(): ReactElement {
return (
<ul style={{ margin: 0, paddingInlineStart: "1.2em", display: "grid", gap: "var(--space-xs)" }}>
<li>
<Countdown to={TO} now={NOW} />
</li>
<li>
<Countdown to={TO} now={NOW} format="short" />
</li>
<li>
<Countdown to={TO} now={NOW} format="clock" precision="second" />
</li>
</ul>
);
}
Caution and warning
Code
import { Countdown } from "@bosunkit/components/countdown";
import type { ReactElement } from "react";
export const description: string =
"Three watch changes: 3 hr away with no treatment, 20 min away in caution, 4 min away in warning.";
const NOW: number = Date.UTC(2026, 9, 17, 3, 0);
const MINUTE: number = 60_000;
export default function Treatments(): ReactElement {
return (
<div style={{ display: "flex", gap: "var(--space-sm)", flexWrap: "wrap" }}>
{[180, 20, 4].map(
(minutes: number): ReactElement => (
<Countdown
key={minutes}
to={NOW + minutes * MINUTE}
now={NOW}
appearance="chip"
cautionWithinMs={30 * MINUTE}
warningWithinMs={5 * MINUTE}
/>
),
)}
</div>
);
}
Time since the moment
Code
import { Countdown } from "@bosunkit/components/countdown";
import type { ReactElement } from "react";
export const description: string = "A watch change that passed 12 minutes ago, counted up and kept in warning.";
const NOW: number = Date.UTC(2026, 9, 17, 4, 12);
export default function Elapsed(): ReactElement {
return (
<Countdown
to={Date.UTC(2026, 9, 17, 4, 0)}
now={NOW}
after="elapsed"
appearance="chip"
warningWithinMs={5 * 60_000}
/>
);
}
With the moment and its reference
Code
import { Countdown } from "@bosunkit/components/countdown";
import type { ReactElement } from "react";
export const description: string = "The time left with the moment written after it in UTC, as the passage plan lists it.";
const NOW: number = Date.UTC(2026, 9, 17, 18, 15);
export default function WithTarget(): ReactElement {
return <Countdown to={Date.UTC(2026, 9, 17, 19, 40)} now={NOW} showTarget timeZone="UTC" timeReference="UTC" />;
}
In a tooltip
Code
import { Countdown } from "@bosunkit/components/countdown";
import { Tip } from "@bosunkit/components/tooltip";
import type { ReactElement } from "react";
export const description: string =
"A countdown chip that the pointer or the keyboard reaches: its tooltip writes the moment in full, day and reference.";
const NOW: number = Date.UTC(2026, 9, 17, 12, 20);
export default function WithTooltip(): ReactElement {
return (
<Tip description="Decide by 14:30 UTC on Saturday 17 October">
<Countdown
to={Date.UTC(2026, 9, 17, 14, 30)}
now={NOW}
appearance="chip"
cautionWithinMs={30 * 60_000}
tabIndex={0}
/>
</Tip>
);
}
In a link, with a tooltip
Code
import { Countdown } from "@bosunkit/components/countdown";
import { Tip } from "@bosunkit/components/tooltip";
import type { ReactElement } from "react";
export const description: string =
"The dashboard's next decision as a link to the passage plan: the tooltip says what the decision is and when it falls.";
const NOW: number = Date.UTC(2026, 9, 17, 13, 50);
export default function InALink(): ReactElement {
return (
<Tip title="Arrival before dark" description="Reach Falmouth by 18:20 UTC, sunset; the passage plan has the timings.">
<a href="#passage-plan" style={{ display: "inline-flex", gap: "var(--space-sm)", alignItems: "center" }}>
Arrival before dark
<Countdown to={Date.UTC(2026, 9, 17, 18, 20)} now={NOW} appearance="chip" cautionWithinMs={60 * 60_000} />
</a>
</Tip>
);
}
A caution tooltip
Code
import { Countdown } from "@bosunkit/components/countdown";
import { Tip } from "@bosunkit/components/tooltip";
import type { ReactElement } from "react";
export const description: string =
"A tidal gate 20 minutes from closing: the chip and its tooltip both take the caution treatment, the tooltip on its edge.";
const NOW: number = Date.UTC(2026, 9, 17, 14, 10);
export default function CautionTooltip(): ReactElement {
return (
<Tip
priority="caution"
title="Tidal gate at the Needles"
description="The stream turns foul at 14:30 UTC. After that the passage waits for the next fair tide at 20:45 UTC."
>
<Countdown
to={Date.UTC(2026, 9, 17, 14, 30)}
now={NOW}
appearance="chip"
cautionWithinMs={30 * 60_000}
warningWithinMs={5 * 60_000}
showTarget
timeZone="UTC"
timeReference="UTC"
tabIndex={0}
/>
</Tip>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| torequired | number | string | Date | – | The moment counted to: milliseconds since the epoch, a Date, or an ISO 8601 string. |
| format | CountdownFormat | undefined | "words" | How the time left is written; words unless set. |
| precision | "minute" | "second" | undefined | "minute" | minute floors to whole minutes ("in under 1 min" in the last one); second counts seconds. minute unless set. |
| after | "due" | "elapsed" | undefined | "due" | Once the moment has passed: due says "time is up", elapsed says how long ago ("12 min ago"). due unless set. |
| appearance | "text" | "chip" | undefined | "text" | text sits in a sentence; chip is a due chip, as a list row or a card shows it, fading only when its treatment
changes. text unless set. |
| cautionWithinMs | number | undefined | – | Milliseconds left at or under which it takes the caution treatment (■), and keeps it once the moment passes. |
| warningWithinMs | number | undefined | – | Milliseconds left at or under which it takes the warning treatment (●), and keeps it once the moment passes. |
| showTarget | boolean | undefined | false | Writes the moment itself after the time left ("in 1 hr · 13:00 UTC"), with timeReference. |
| timeZone | string | undefined | – | The time zone the moment is written in, an IANA name such as UTC or Europe/Lisbon; the browser's unless set. |
| timeReference | string | undefined | – | The moment's reference, printed after it, translated by the caller ("UTC", "ship's time"). |
| onReach | (() => void) | undefined | – | Called once when the moment arrives while the countdown is shown. |
| announceAt | readonly number[] | undefined | – | Milliseconds left at which a screen reader hears the time left once each ("10 min left"); none unless set. |
| now | number | undefined | – | A fixed current time, for examples and printouts; the countdown keeps its own clock unless set. |
| className | string | undefined | – | Classes added to the countdown's own. |
| testId | string | undefined | – | Becomes data-testid on the root. |
| ref | Ref<HTMLSpanElement> | undefined | – | The root element, so a Tip can describe the countdown; give it tabIndex={0} to reach it by keyboard. |
Also takes every attribute of Omit<HTMLAttributes<HTMLSpanElement>, "children" | "className" | "style">.
Related
- Due chip: the chip a countdown is drawn as, and a record's date that has come or is coming, written by the caller.
- Tooltip: a tip that describes a countdown with the moment in full.
- Duration input: a length of time the crew type, written the same way.