Skip to content
design
Components

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

A passage plan's next decision: decide by 14:30 UTC, the time left floored to the minute.
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

The same moment in words, short and as a clock, to the minute and to the second.
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

Three watch changes: 3 hr away with no treatment, 20 min away in caution, 4 min away in 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

A watch change that passed 12 minutes ago, counted up and kept in warning.
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

· 19:40 UTC
The time left with the moment written after it in UTC, as the passage plan lists it.
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

A countdown chip that the pointer or the keyboard reaches: its tooltip writes the moment in full, day and reference.
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>
  );
}
The dashboard's next decision as a link to the passage plan: the tooltip says what the decision is and when it falls.
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

· 14:30 UTC
A tidal gate 20 minutes from closing: the chip and its tooltip both take the caution treatment, the tooltip on its edge.
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

PropTypeDefaultDescription
torequirednumber | string | Date–The moment counted to: milliseconds since the epoch, a Date, or an ISO 8601 string.
formatCountdownFormat | 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.
cautionWithinMsnumber | undefined–Milliseconds left at or under which it takes the caution treatment (■), and keeps it once the moment passes.
warningWithinMsnumber | undefined–Milliseconds left at or under which it takes the warning treatment (●), and keeps it once the moment passes.
showTargetboolean | undefinedfalseWrites the moment itself after the time left ("in 1 hr · 13:00 UTC"), with timeReference.
timeZonestring | undefined–The time zone the moment is written in, an IANA name such as UTC or Europe/Lisbon; the browser's unless set.
timeReferencestring | 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.
announceAtreadonly number[] | undefined–Milliseconds left at which a screen reader hears the time left once each ("10 min left"); none unless set.
nownumber | undefined–A fixed current time, for examples and printouts; the countdown keeps its own clock unless set.
classNamestring | undefined–Classes added to the countdown's own.
testIdstring | undefined–Becomes data-testid on the root.
refRef<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">.

  • 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.