Countdown
Days left until a date that matters, with progress, working days and milestones.
≈ 1,149 tokens per use (tool 735 · guidance 310 · typical payload 104)How token costs are estimated
Example: Countdown
How many days until my Lisbon trip?
until Lisbon trip
When to use
- The user asks how long until a date: a trip, a launch, a wedding, a deadline, a due date, a holiday.
- The user wants a few upcoming dates side by side, counted down.
When not to use
- The user wants to time minutes or hours they start and stop, like cooking or a workout. Use the Timer.
- The user needs a reminder to fire at a time. Use a reminder or calendar tool.
- The date is unknown or only implied. Ask, or look it up first; never guess a date.
- The question is only about the past ("how long ago was…"). Answer in text.
How the agent should use it
- Use a plain date for whole-day events (trips, birthdays, deadlines) and a date-time with
timeZonefor events at a time, like a launch at 9 AM New York time. - Put the event the user asked about first. It gets the big number; the rest are listed below it, soonest first.
- Leave
precisionunset unless the user asks for hours or seconds. It tightens on its own as the date gets close. - Set
sinceonly when you know when the wait began, for example the booking date from a tool result. - Turn on
countBusinessDaysfor work deadlines. It counts Monday to Friday and doesn't know about holidays.
How it works
- Days are calendar days in the event's timezone (the viewer's by default). On October 9, a trip on November 20 is "42 days" away, whatever the time of day. A daylight-saving change in between never shows up as a stray hour.
- Precision tightens as it gets close: days while more than 2 days away, then "1 day and 6 hours", then hours and minutes inside 6 hours, then minutes and seconds in the last hour. With
precisionset, the hero shows every unit down to it ("41 days and 9 hours"). - Date-only targets count to local midnight. A date-time without an offset (
2026-12-01T09:00) is local time intimeZone; one with an offset is that exact moment. - On the day the hero says "Today" (no exclamation mark), and screen readers hear "Lisbon trip is today" once if the page was open when it arrived. Afterwards it says "Lisbon trip was 3 days ago" in muted text; never a negative number.
- Time comes from the wall clock after mount. The server renders a skeleton for the number, so there's no hydration mismatch, and the component wakes exactly when the shown value changes (every second in the last hour, otherwise every minute).
- Screen readers get one sentence, "42 days and 6 hours until Lisbon trip, on Friday, November 20, 2026", capped at minutes so it doesn't change every second. The ticking number is hidden from them.
- Under the number, one quiet line holds the date, the working days left (with
countBusinessDays: Monday to Friday, holidays not excluded) and the link's host name. - Progress (with
since) is a bar with "68% of the wait is over" under it. The start date is read to screen readers. - Milestones sit on dates counted back from the first event: "Book airport transfer · Nov 13 · in 35 days". Past ones get a check mark and "3 days ago", in text rather than color.
Actions
The footer has a closed set of actions for the first event:
- Add to calendar downloads an
.icsfile: an all-day event for a date-only target, an exact moment (in UTC) for a timed one, with the note and link. Text is escaped and long lines folded per RFC 5545. - Share opens the system share sheet with "42 days until Lisbon trip" where the browser has one, and otherwise copies that text and says "Copied".
Each reports to onResult for analytics; nothing locks.
Installation
npx shadcn@latest add https://your-site.com/r/countdown.json
Usage
The footer actions don't change what the agent does, so the tool can return immediately:
import { tool } from "ai"
import { countdownTool } from "@/components/genui/countdown.schema"
const tools = {
[countdownTool.name]: tool({
description: countdownTool.description,
inputSchema: countdownTool.inputSchema,
execute: async () => ({ shown: true }),
}),
}Render it through the Renderer under show_countdown, or directly. onResult reports which action the user took:
<Countdown
{...part.input}
status={part.state === "input-streaming" ? "streaming" : "ready"}
onResult={(r) => track("countdown_action", r)} // { action: "add-to-calendar" | "share", eventId }
/>The countdown never locks, so there's no finished state to restore from history. The helpers live in countdown-utils.ts (resolveTarget, timeLeft, countdownState, businessDaysBetween, buildIcs, …), pure functions you can reuse on the server.
With LangGraph
Push it from a graph node with ui.push({ name: "show_countdown", props }, { message }), and render the thread's UI messages with the Renderer.
Schema
| Prop | Type | Notes |
|---|---|---|
events | CountdownEvent[] | 1–5. The first gets the big countdown |
precision | "days" | "hours" | "minutes" | "seconds" | Smallest unit shown. Default: automatic |
countBusinessDays | boolean | Also show working days left, Mon–Fri. Default false |
milestones | { label, daysBefore }[]? | Up to 8 things to do before the first event |
CountdownEvent | Type | Notes |
|---|---|---|
id | string | Returned in the result |
label | string | Reads after "until": "Lisbon trip" |
at | string | YYYY-MM-DD, a local date-time, or a date-time with an offset. Data-bound when it comes from a booking or calendar |
timeZone | string? | IANA zone the date applies in. Default: the viewer's |
since | string? | When the wait began. Adds the progress bar |
note | string? | One line, up to 120 characters. First event only: read to screen readers and added to the calendar file, not printed |
url | string? | http(s) link, e.g. the booking. Shown for the first event only. Data-bound |
Host props: status (streaming or ready), onResult, now (a fixed epoch-ms time, for tests only) and className.