Due date
StableRelative due dates for task rows (“Today”, “Fri”, “Oct 30”) with overdue shown by shape, and a picker with quick choices.
dueInfo turns an ISO date and a task status into words: Today, Tomorrow, Yesterday, a weekday within the next six days, else a short date (with the year when it isn’t this year), plus overdue and soon flags. DueChip draws that in a row; DueDatePicker is a button whose popover offers Today, Tomorrow, Next week, the month grid and No due date.
Updated View source
In a task list
import { DueChip, StatusIcon, type StatusId } from '@rchursin/product/tasks'import { SampleBadge } from '@rchursin/product/shell/layout'
/** A fixed "now" (Wednesday, Oct 21, 2026, 10:00 AM), so this sample reads the same on any day */const now = new Date(2026, 9, 21, 10, 0)
const tasks: { id: string; title: string; due: string; status: StatusId }[] = [ { id: 'RC-41', title: 'Fix the flaky retry test', due: '2026-10-19', status: 'in_progress' }, { id: 'RC-44', title: 'Write the release notes', due: '2026-10-21', status: 'todo' }, { id: 'RC-45', title: 'Review the pricing copy', due: '2026-10-22', status: 'in_review' }, { id: 'RC-47', title: 'Ship the eval dashboard', due: '2026-10-23', status: 'todo' }, { id: 'RC-39', title: 'Archive old agent logs', due: '2026-10-20', status: 'done' }, { id: 'RC-52', title: 'Plan the Q1 roadmap', due: '2027-01-08', status: 'backlog' },]
/** * Relative and short in dense rows. Overdue swaps the calendar for an alert sign and a firm * outline (and says "overdue" to screen readers); a closed task's date goes quiet, never overdue. */export default function DueDateChips() { return ( <div className="flex w-full max-w-md flex-col gap-2"> <div className="flex items-center justify-between px-1"> <p className="font-mono text-[11px] tracking-[0.12em] text-fg-3 uppercase">Wed, Oct 21</p> <SampleBadge /> </div> <ul className="lux-card flex flex-col rounded-xl bg-raised"> {tasks.map((t) => ( <li key={t.id} className="flex h-10 min-w-0 items-center gap-2.5 border-b border-line px-3 text-[13px] last:border-0"> <StatusIcon status={t.status} /> <span className="hidden font-mono text-[11px] text-fg-3 sm:inline">{t.id}</span> <span className="min-w-0 flex-1 truncate">{t.title}</span> <DueChip due={t.due} status={t.status} now={now} /> </li> ))} </ul> </div> )}Usage
import { DueChip, DueDatePicker, dueInfo } from '@rchursin/product/tasks'<DueChip due={task.due} status={task.status} /><DueDatePicker value={due} onChange={setDue} />Examples
Due date picker
Chip, property row and icon, measured from a fixed Wednesday, Oct 14 (the now prop): Oct 21 is a week out and Oct 2 shows as overdue.
import { useState } from 'react'import { DueDatePicker } from '@rchursin/product/tasks'
/** * Today, Tomorrow and Next week at the top, any other day on the month grid, and "No due date". * Three trigger styles: a chip for toolbars, a full-width row for a property panel, an icon for tight rows. * A fixed `now` (Wednesday 14 October 2026) keeps the labels the same whatever day you read this. */const now = new Date(2026, 9, 14)
export default function DueDatePickerVariants() { const [due, setDue] = useState<string | null>('2026-10-21') const [unset, setUnset] = useState<string | null>(null) const [late, setLate] = useState<string | null>('2026-10-02')
return ( <div className="flex w-full max-w-72 flex-col gap-5"> <div className="flex flex-col gap-1.5"> <p className="font-mono text-[11px] tracking-[0.12em] text-fg-3 uppercase">Chip</p> <div className="flex flex-wrap items-center gap-2"> <DueDatePicker value={due} onChange={setDue} now={now} /> <DueDatePicker value={unset} onChange={setUnset} now={now} /> </div> </div> <div className="flex flex-col gap-1.5"> <p className="font-mono text-[11px] tracking-[0.12em] text-fg-3 uppercase">Property row, overdue</p> <DueDatePicker variant="property" value={late} onChange={setLate} status="todo" now={now} /> </div> <div className="flex flex-col gap-1.5"> <p className="font-mono text-[11px] tracking-[0.12em] text-fg-3 uppercase">Icon only</p> <DueDatePicker variant="icon" value={due} onChange={setDue} now={now} /> </div> </div> )}API
DueChip props
| Prop | Type | Default | Description |
|---|---|---|---|
duerequired | string | null | – | ISO date. Nothing renders when it is null. |
statusrequired | StatusId | – | Closed tasks (done, canceled) never read as overdue and go quiet. |
now | Date | new Date() | The moment “today” is measured from. Pass a fixed one for stable specimens. |
quiet | boolean | – | No outline and meta colour unless overdue, for busy rows. |
className | string | – | Classes on the chip. |
DueDatePicker props
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | string | null | – | ISO date or null. |
onChangerequired | (value: string | null) => void | – | Called with the chosen date, or null for No due date. |
variant | 'chip' | 'icon' | 'property' | 'chip' | Metal chip, a 24px icon button, or a full-width row for a property panel. |
status | StatusId | 'todo' | Only decides whether a past date reads as overdue (alert icon, bold label). |
now | Date | new Date() | What “today” means for the label and the Today, Tomorrow and Next week options. Pass a fixed one for specimens and prerendered pages. |
className | string | – | Classes on the trigger button. |
dueInfo(iso, status, now)
| Prop | Type | Default | Description |
|---|---|---|---|
isorequired | string | null | – | The due date. Returns null when it is null. |
statusrequired | StatusId | – | Whether the task is closed. |
now | Date | new Date() | What “today” means. |
returns | DueInfo | null | – | { label, long, overdue, soon, closed }: “Fri” and “Friday, October 23, 2026”; overdue is past and still open; soon is today or tomorrow and still open. |
DueDatePanel props (the popover’s content)
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | string | null | – | ISO date or null. |
onChangerequired | (iso: string | null) => void | – | A choice was made. |
onDone | () => void | – | Called after any choice (close the popover here). |
now | Date | new Date() | What Today, Tomorrow and Next week (next Monday) are measured from. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| EnterSpace | On the trigger: opens the popover with focus on Today. |
| Up arrowDown arrow | Moves between Today, Tomorrow, Next week and No due date, wrapping around. |
| TabShiftTab | Through the quick choices, then the month panel’s buttons and grid (where the calendar keys apply), then No due date, wrapping around at either end: the dialog is modal, so focus stays in it until a choice, Esc or a click outside. |
| EnterSpace | Chooses and closes; focus returns to the trigger. |
| Esc | Closes without a change. |
- DueChip says “Due Fri” to screen readers, adds “, overdue” when it is, and puts the long date in its tooltip.
- Overdue is shown by shape (an alert sign instead of the calendar, a firm outline), never by colour.
- The trigger is named in full, “Due date: Wednesday, October 21, 2026” (plus “, overdue”), or “Due date: none”, and opens a dialog named “Due date”.
- The quick choices are toggle buttons named with the full date (“Tomorrow, Thursday, October 22, 2026”), pressed when they match.
Guidelines
Do
- Use the short label in dense rows and the long one in details.
- Pass the task’s status, so finished work never shouts “overdue”.
Don’t
- Don’t colour overdue dates red; the shape already says it.
- Don’t compute “today” in several places; pass one now down when you need it fixed.
In use
Related
- Status & priorityTask glyphs told apart by shape: a square that fills as work moves on, priority bars, assignee, labels and estimate.
- Property pickersThe Tasks tool’s pickers for status, priority, assignee, labels and estimate, as a chip, a row or an icon.
- Task listThe task list as a data grid: one Tab stop, arrows to move, pickers in the cells, and groups that collapse.
- Date pickerA field that shows the chosen date and opens a month grid in a popover. Values are ISO date strings.
Source
The component’s own files, exactly as they run on this site.
/** * Small task visuals. Status and priority are told apart by SHAPE (square outline, fill * amount, tick or cross; one to three bars, the "!" square), never by colour. Each icon that * carries meaning has a name ("Status: In review") that is also its tooltip. */import { cx } from "../../components/cx"import { personById, priorityById, statusById, type PriorityId, type StatusId,} from "../data/sample"import { Icon } from "../icons/Icon"import { Avatar } from "../ui/misc"import { dueInfo, estimateLong, personName } from "./model"
const iconSize = { xs: "size-3", sm: "size-3.5", md: "size-4", lg: "size-5" } as consttype IconSize = keyof typeof iconSize