Countdown

Days left until a date that matters, with progress, working days and milestones.

Pattern

≈ 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 timeZone for 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 precision unset unless the user asks for hours or seconds. It tightens on its own as the date gets close.
  • Set since only when you know when the wait began, for example the booking date from a tool result.
  • Turn on countBusinessDays for 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 precision set, 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 in timeZone; 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 .ics file: 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:

app/api/chat/route.ts
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

PropTypeNotes
eventsCountdownEvent[]1–5. The first gets the big countdown
precision"days" | "hours" | "minutes" | "seconds"Smallest unit shown. Default: automatic
countBusinessDaysbooleanAlso show working days left, Mon–Fri. Default false
milestones{ label, daysBefore }[]?Up to 8 things to do before the first event
CountdownEventTypeNotes
idstringReturned in the result
labelstringReads after "until": "Lisbon trip"
atstringYYYY-MM-DD, a local date-time, or a date-time with an offset. Data-bound when it comes from a booking or calendar
timeZonestring?IANA zone the date applies in. Default: the viewer's
sincestring?When the wait began. Adds the progress bar
notestring?One line, up to 120 characters. First event only: read to screen readers and added to the calendar file, not printed
urlstring?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.

Source

On this page