Runtime date counters — variations + portfolio-wide pattern
Date: 2026-05-21 · Status: sketched · Build effort: ~30 min for v1 (single widget script), ~1 hr for full portfolio retrofit
The principle (per Dan, 2026-05-21)
“For everything, that countdown timer, or conversely, a count-up timer, days passed since the deadline, like -15 days, etc. … it’s a foundational pattern across everything — one of the coolest, simple things.”
Every dated reference on a static page wants to express time-distance from now to that date, computed at read time, not at publish time. Build-time bake-ins drift; runtime recalculation is always accurate.
The four canonical states
PAST NOW FUTURE
─────────────────────|─────────────────────
│
"overdue 15d" │ "today" "in 54d"
"paid 30d ago" │ "renews in 90d"
"expired 90d" │ "expires in 30d"
"filed 2y ago" │ "due 14:30"
│
[count-up] [zero] [count-down]
Six concrete variations the portfolio surfaces want to express:
| # | State | Distance | UI form | Example |
|---|---|---|---|---|
| 1 | Countdown — future deadline | -N days | “54d left” | claim deadlines, contract renewals, gate-service intervals |
| 2 | Overdue — past deadline, unresolved | +N days | “15d overdue” / “-15d” | unpaid invoices, missed service, expired insurance |
| 3 | Today — happening now | 0 | “today” / “due today” | calendar items, scheduled cron tomorrow |
| 4 | Recency — past event, just record-keeping | +N days | “30d ago” / “30 days ago” | last service, last touched, last contact |
| 5 | Anniversary — past event with periodic meaning | +N days, normalised | “3.2y ago” / “1 year, 47 days” | original purchase date, account opened |
| 6 | Time-of-day countdown — sub-day precision | hours, minutes | “in 3h 22m” / “5h overdue” | same-day deadlines, meeting starts |
Markup pattern (canonical)
Same shape works for all six variations. Authors declare the anchor date + interpretive mode, widget renders the appropriate label:
<!-- Variation 1: countdown -->
<span data-when="2026-07-14" data-mode="countdown">…</span>
<!-- Variation 2: overdue-aware countdown (auto-flips after target) -->
<span data-when="2026-04-15" data-mode="deadline">…</span>
<!-- Variation 4: recency / "ago" -->
<span data-when="2026-05-17" data-mode="ago">…</span>
<!-- Variation 5: anniversary (with auto-unit selection) -->
<span data-when="2016-02-15" data-mode="age">…</span>
<!-- Variation 6: sub-day precision -->
<span data-when="2026-05-21T18:00:00-04:00" data-mode="precise">…</span>
The static HTML body is the fallback (what shows if JS is disabled or hasn’t loaded yet). The widget replaces with computed value on page load + on visibility events (so opening yesterday’s tab today recalculates).
The widget — single source of truth
~/bin/portfolio_date_counter.js (or hosted at gf.cx/widgets/date-counter.js) — one drop-in script that handles all six modes. Embed via the same data-cfasync="false" pattern as ask-opus:
<script src="https://gf.cx/widgets/date-counter.js" defer data-cfasync="false"></script>
Pseudocode of the core dispatch:
function format(anchor, mode, now = new Date()) {
const target = new Date(anchor);
const ms = target - now;
const days = Math.round(ms / 86400000);
switch (mode) {
case "countdown":
return days > 0 ? `${days}d left` :
days === 0 ? "today" :
`${-days}d overdue`;
case "deadline": // alias for countdown, semantically clearer
return format(anchor, "countdown", now);
case "ago":
return days < 0 ? `in ${-days}d` :
days === 0 ? "today" :
days === 1 ? "yesterday" :
days < 30 ? `${days}d ago` :
days < 365 ? `${Math.round(days/30)}mo ago` :
`${(days/365).toFixed(1)}y ago`;
case "age": // sentimental / record-keeping precision
const years = days / 365.25;
return years < 1 ? `${days}d` :
years < 10 ? `${years.toFixed(1)} years` :
`${Math.floor(years)} years`;
case "precise": // sub-day for same-day events
const hours = Math.round(ms / 3600000);
if (Math.abs(hours) < 24) {
return hours > 0 ? `in ${hours}h` :
hours === 0 ? "now" :
`${-hours}h overdue`;
}
return format(anchor, "countdown", now);
}
}
Visual treatment by state
Same data-mode + state-derived CSS class for color-coding without needing extra attributes:
[data-when][data-state="overdue"] { color: var(--accent-alert); font-weight: 700; }
[data-when][data-state="today"] { color: var(--accent-warn); font-weight: 700; }
[data-when][data-state="soon"] { color: var(--accent-warn); } /* <7 days */
[data-when][data-state="future"] { color: var(--ink); }
[data-when][data-state="past"] { color: var(--ink-soft); }
The widget sets data-state after computing the distance: