Reminders
StableReminders grouped from Overdue to Done, checked off, snoozed or deleted with Undo, plus a compact widget for a dashboard.
ReminderList groups reminders into Overdue, Today, Tomorrow, This week, Later and Done. Each row is a checkbox with its time and repeat; a repeating reminder moves to its next time when checked. It is fully controlled: pass useReminders(), which saves to this browser and is shared by everything on the page. RemindersWidget puts quick capture, the next four reminders and an “Open Notes” link (href) in one dashboard card.
Reminders widget
import { useRouter } from '@rchursin/product/lib/router'import { RemindersWidget } from '@rchursin/product/notes'
/** * Type a reminder in plain words ("check the cost report every Monday at 9") and press Enter. * The next four open reminders sit under it, overdue first. It reads the same saved reminders * as the Notes app, so a change in one shows up in the other. * href sends “Open Notes” to your own page; here, the tool’s page in this Library. */export default function RemindersWidgetExample() { const { href } = useRouter() return ( <div className="lux-card w-full max-w-sm rounded-2xl bg-raised p-5"> <RemindersWidget href={href('/library/tools/notes-app')} /> </div> )}Usage
import { ReminderList, ReminderRow, RemindersWidget, SnoozeMenu, useReminders } from '@rchursin/product/notes'const r = useReminders()
<ReminderList reminders={r.items} now={r.now} onToggle={r.toggle} onSnooze={r.snooze} onDelete={r.remove} />Examples
The grouped list
ReminderList on local state and a fixed clock (Tuesday, 9:30 AM). Check one off, snooze it from the alarm button, or delete it: focus follows the row to its new group.
import { useState } from 'react'import type { Reminder } from '@rchursin/product/data/sample'import { nextOccurrence, parseRepeat, ReminderList } from '@rchursin/product/notes'import { SampleBadge } from '@rchursin/product/shell/layout'import { Button, useToast } from '@rchursin/product/ui'
/** A fixed clock (Tuesday, 9:30 AM), so the groups read the same on every visit */const NOW = new Date(2026, 9, 13, 9, 30)const at = (dayOffset: number, h: number) => new Date(2026, 9, 13 + dayOffset, h).getTime()
const start: Reminder[] = [ { id: 'ex-1', text: 'Send the cost report to Ines', at: at(-1, 16), repeat: null, done: false }, { id: 'ex-2', text: 'Rerun the scan after merging skills', at: at(0, 14), repeat: null, done: false }, { id: 'ex-3', text: 'Check pr-review cost after the prompt change', at: at(1, 9), repeat: null, done: false }, { id: 'ex-4', text: 'Weekly skill review', at: at(3, 10), repeat: 'Every Friday', done: false }, { id: 'ex-5', text: 'Plan the Q1 agent budget', at: at(9, 11), repeat: null, done: false }, { id: 'ex-6', text: 'Turn on OTel export for Cursor', at: at(-2, 11), repeat: null, done: true },]
/**API
ReminderList props
| Prop | Type | Default | Description |
|---|---|---|---|
remindersrequired | Reminder[] | – | { id, text, at (epoch ms), repeat (“Every Friday” or null), done }. |
nowrequired | Date | – | Decides the groups and what is overdue. |
onTogglerequired | (id: string) => void | – | The checkbox changed. useReminders().toggle moves a repeating one to its next time. |
onSnoozerequired | (id: string, until: Date) => void | – | A snooze option was chosen. |
onDeleterequired | (id: string) => void | – | The delete button. Focus has already moved to the next row. |
emptyAction | ReactNode | – | Shown in the empty state, e.g. a button that focuses quick capture. |
idPrefix | string | '' | Prefix for the rows’ DOM ids. Give each list its own when two show the same reminders on one page. |
className | string | – | Classes on the region. |
ReminderRow props
| Prop | Type | Default | Description |
|---|---|---|---|
reminderrequired | Reminder | – | One reminder. Render rows inside a <ul role="list">. |
nowrequired | Date | – | For “Overdue” and the time shown. |
onTogglerequired | (id: string) => void | – | The checkbox changed. |
onSnoozerequired | (id: string, until: Date) => void | – | A snooze option was chosen. |
onDelete | (id: string) => void | – | Adds a delete button. |
compact | boolean | false | Denser, with the full date, for a widget. |
idPrefix | string | '' | Prefix for the row’s DOM ids when the same reminder shows twice on a page. |
SnoozeMenu props
| Prop | Type | Default | Description |
|---|---|---|---|
reminderrequired | Reminder | – | Names the menu: “Snooze “Rerun the scan””. |
nowrequired | Date | – | Options are worked out from it: later today (three hours on, until 9 PM), tomorrow 9 AM, next Monday 9 AM. |
onSnoozerequired | (id: string, until: Date) => void | – | An option was chosen. |
afterSnooze | () => void | – | Runs after onSnooze, e.g. to move focus to the row in its new group. |
size | 'xs' | 'sm' | 'sm' | Size of the alarm icon button. |
RemindersWidget props
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | null | '#notes' | Where the “Open Notes” link goes: your Notes page. The default is the product kit’s own anchor; null hides the link. |
useReminders() returns
| Prop | Type | Default | Description |
|---|---|---|---|
items | Reminder[] | – | Every reminder: { id, text, at (epoch ms), repeat (“Every Friday” or null), done }. |
now | Date | – | The hook’s clock. It wakes when the next reminder comes due, every 30 seconds at most, and when the tab comes back. |
dueNow | Reminder[] | – | Reminders that came due while this page was open and weren’t dismissed. Feed it to DueBanner. |
add | (input: NewReminder) => Reminder | – | Add { text, at, repeat }, the shape QuickCapture hands over. Toast with Undo. |
toggle | (id: string) => void | – | Check off or back on. A repeating reminder moves to its next time instead, with a toast that says when (and Undo). |
snooze | (id: string, until: Date) => void | – | Move it to a new time. Toast with Undo. |
remove | (id: string) => void | – | Delete it. Toast with Undo. |
dismiss | (id: string) => void | – | Hide it from dueNow for this visit; the reminder itself stays overdue. |
reset | () => void | – | Back to the four sample reminders. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Space | On a reminder’s checkbox: check it off, or back on. Focus follows the row into its new group. |
| EnterSpaceDown arrow | On the alarm button: open the snooze menu on its first option (↑ opens on the last). |
| Up arrowDown arrowHomeEnd | In the snooze menu: move between options. |
| Enter | In the snooze menu: snooze until that time. In quick capture: add the reminder. |
| Esc | Close the snooze menu; focus returns to the alarm button. |
- The list is a region named “Reminders”. Each group is a group named by its heading, which includes the count (“Overdue 1”). Done can be hidden with a toggle that has aria-expanded.
- Each row is a native checkbox named by the reminder’s text and described by its state, time and repeat. The text is its label, so clicking the words ticks the box.
- Overdue is said in words with an icon and a heavier weight, never by colour.
- When a row moves (checked, snoozed, moved to its next time) focus follows it; after a delete it moves to the next row, or the list itself when it was the last.
- With useReminders, adding, snoozing, deleting and moving a repeating reminder show a toast with Undo; checking off a one-time reminder is announced (“Rerun the scan: done”).
- In the widget, a checked reminder leaves the list, so focus moves to the row that takes its place (or the one above, or the capture field when the list empties). Its checkbox ids carry a prefix from useId, so they never clash with a list on the same page.
Guidelines
Do
- Use useReminders() for every view on a page: it is one store, so the list, the widget and the due bar never disagree.
- Pair the list with QuickCapture, so people can type “every Friday at 10” instead of filling in a form.
- Show reminders that come due with DueBanner, inside the page; it works where notifications can’t ask for permission.
Don’t
- Don’t render two ReminderLists of the same reminders on one page without giving each its own idPrefix; their checkbox ids would repeat.
- Don’t mark overdue with colour alone; keep the word and the icon.
In use
Used on this site
Related
- NotesNotes and reminders in one window: a searchable list beside a block editor, and reminders typed in plain words.
- Quick captureOne field that reads a date, time or repeat out of plain English and shows what it understood before you save.
- BannersIn-page messages for things that happen on their own: a timer that finished, a reminder that came due.
- MenuA menu button with actions, checkbox and radio items, group labels, shortcut hints and type-ahead, in a popover beside its trigger.
- CheckboxA labelled checkbox drawn as the brand square with a cut-in tick, with a hint and a mixed state.
- Tasks widgetYour open tasks for a dashboard, soonest due first: tick one to finish it, or change its status from its icon.
Source
The component’s own files, exactly as they run on this site.
/** * Reminders, grouped Overdue / Today / Tomorrow / This week / Later / Done. * * Each row is a native checkbox named by the reminder's own text ("Rerun the scan, checkbox, * not checked"), described by its time, repeat rule and state. Overdue is said in words with an * icon, never by colour. Checking off a repeating reminder moves it to its next time and says when. * When a row moves to another group, focus follows it. */import { useId, useRef, useState, type ReactNode } from "react"import { cx } from "../../components/cx"import { Icon } from "../icons/Icon"import { formatTime, formatTimeShort } from "../lib/dates"import type { Reminder } from "../data/sample"import { useHydrated } from "../lib/hydration"import { Button, IconButton } from "../ui/button"import { Menu } from "../ui/menu"import { EmptyState } from "../ui/misc"import { reminderGroupNames, reminderGroupOf,