Laps table
StableThe Stopwatch’s table on its own: lap, lap time and total, newest first, with the fastest and slowest named.
Feed it laps from lapsFrom(marks) and, while a run is going, a current lap whose times are read from the clock. The lap in progress sits on top and redraws every frame without re-rendering React. Fastest and slowest get a solid or dashed tag with a word and an icon.
Updated View source
Finished laps
import { LapsTable, lapsFrom } from '@rchursin/product/timer'
/** * lapsFrom turns the stopwatch's marks (total time at each Lap press, in ms) into laps, * and names the fastest and slowest once there are two or more. */const laps = lapsFrom([48_210, 95_870, 151_330, 192_480, 241_060])
/** Newest first. Fastest is a solid tag, slowest a dashed one, each with a word and an icon. */export default function LapsTableExample() { return <LapsTable laps={laps} className="w-full max-w-md" />}Usage
import { LapsTable, lapsFrom } from '@rchursin/product/timer'<LapsTable laps={lapsFrom([48_210, 95_870, 151_330])} />Examples
A lap in progress
current adds the running lap on top (“4 · now”). This stopwatch is paused, so it holds still.
import { LapsTable, lapsFrom } from '@rchursin/product/timer'
const marks = [48_210, 95_870, 151_330]/** Where the stopwatch was paused, 3:03.94 in */const pausedAt = 183_940
/** * `current` puts the lap in progress on top. Its times come from functions of the clock, * so a running stopwatch can redraw them every frame; this one is paused. */export default function LapsInProgress() { return ( <LapsTable laps={lapsFrom(marks)} current={{ n: marks.length + 1, read: () => pausedAt - marks[marks.length - 1]!, total: () => pausedAt, live: false }} className="w-full max-w-md" /> )}With your own controls
useStopwatch plus the table. Start, then Lap (or L while focus is inside). It starts empty, with an emptyHint that names those controls.
import { useRef } from 'react'import { useAnnounce } from '@rchursin/product/lib/announce'import { useHotkeys } from '@rchursin/product/lib/hotkeys'import { LapsTable, useStopwatch } from '@rchursin/product/timer'import { Button } from '@rchursin/product/ui'
/** * Your own controls around the table: useStopwatch keeps the run (saved per viewer under its key), * the table draws the laps, and `current` reads the clock while it runs. The table itself is quiet, * so lapping says what happened. L laps while focus is inside, and emptyHint says so. */export default function LapsLive() { const sw = useStopwatch('docs:laps-table') const announce = useAnnounce() const scope = useRef<HTMLDivElement>(null) const running = sw.status === 'running' const lastMark = sw.state.marks.at(-1) ?? 0
function lap() { if (!running) returnAPI
LapsTable props
| Prop | Type | Default | Description |
|---|---|---|---|
lapsrequired | Lap[] | – | Finished laps, oldest first ({ n, ms, total, rank? }). lapsFrom(marks) makes them from the total at each Lap press. |
current | CurrentLap | null | – | The lap in progress: { n, read(now), total(now), live }. read and total return milliseconds; live redraws them every frame. |
emptyHint | ReactNode | 'Laps you record show up here.' | The line under “No laps yet”. Say how to record a lap with your controls; Stopwatch passes “Press Lap (or L) while it runs to split the time.” |
className | string | – | Extra classes on the table’s frame (or the empty state). |
lapsFrom(marks)
| Prop | Type | Default | Description |
|---|---|---|---|
marksrequired | number[] | – | Total elapsed ms at each Lap press, oldest first (StopwatchState.marks). |
returns | Lap[] | – | Laps with their own time and total. With two or more, the quickest is ranked fastest and the longest slowest (no ranks when every lap is the same length). |
Accessibility
- A real <table> named by a visually hidden caption, “Laps, newest first”, with column headers and the lap number as each row’s header.
- The running lap changes every frame on screen but is aria-hidden; screen readers hear “In progress” and “Running” instead of a number that never stops.
- Fastest and slowest are a word and an icon in a solid or dashed tag, and the ranked lap time is bold; nothing depends on colour.
- The table doesn’t announce anything. Whoever records laps should say so (Stopwatch announces “Lap 2, 31.2 seconds”; the live example announces “Lap 2 recorded”).
- With no laps and no current lap it shows a dashed empty state, “No laps yet” over emptyHint. The default hint names no key; if your controls have a shortcut (the live example binds L with useHotkeys), say so in emptyHint.
Guidelines
Do
- Pass live: false when paused so nothing redraws needlessly.
- Keep the newest-first order; it is what the caption promises.
Don’t
- Don’t compute fastest and slowest yourself; lapsFrom does it.
- Don’t put a live number in an aria-live region.
In use
Related
- StopwatchA stopwatch with laps: a square that fills once a minute, hundredths drawn per frame, and Undo after a reset.
- Time displayThe clock face: Geist Mono digits in fixed cells, lit like the buttons, still or live, with a spoken form for screen readers.
- Timer engineClocks that keep honest time: stopwatch and countdown hooks built on timestamps, one shared frame loop, and duration parsing.
- TimerThe Focus screen: a focus session, a stopwatch and a countdown one switch apart, with the time log beside them.
Source
The component’s own files, exactly as they run on this site.
/** * Stopwatch with laps. The square fills once a minute, like a seconds hand; hundredths are drawn per frame. * * <Stopwatch /> keeps its own state (saved under "timer:stopwatch") * <Stopwatch stopwatch={useStopwatch()} /> or drive it from outside * * Keys (while focus is inside it): Space start / pause · L lap · R reset (with Undo). * Laps: newest first; the fastest and slowest are named in words with an icon, never by colour alone. */import { useEffect, useId, useRef, type ReactNode } from "react"import { cx } from "../../components/cx"import { Icon } from "../icons/Icon"import { useAnnounce } from "../lib/announce"import { formatDuration } from "../lib/dates"import { useHotkeys } from "../lib/hotkeys"import { Button } from "../ui/button"import { useToast } from "../ui/toast"import { ClockText, KeyHints, outsideOverlays, spaceToggles } from "./bits"import { elapsedAt, lapText, type Lap, type RunStatus } from "./engine"import { useStopwatch, type StopwatchApi } from "./hooks"