Match Clock
betaThe match clock on playing time, across halves, half-time, extra time, and full time: one line for a scoreboard, or the timekeeper's console with its buttons.
Preview
Not started
before
running
time off
time up
half-time
full time
Half-time
Second half
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/match-clock
Use it for
- Scoreboards, match cards, and headers: the inline layout reads "Second half 9:05", with Time off or Time up in amber.
- The timekeeper's screen: the console layout shows the period, big digits, the match's periods as a track, what happens next, and two buttons.
- A card's bottom row when the digits already show elsewhere: the controls layout shows only the next line and the buttons.
- Keep the clock's value in your app and sync it between devices. It holds no running timer, so every device works out the same time from the same value.
- Pass onChange to run the clock. It hands back the next value and the action, so you can log it, sync it, and offer Undo.
- Set the digits to the referee's watch: press them in the console, type the time, and press Enter. Sin bins on SuspensionRow move with it.
- Pick a preset format, like "15s", "sevens", or "u16", or pass your competition's own MatchFormat.
- Record matchPlayed(clock) when a card is shown, and pass it as SuspensionRow's at. matchClockDisplay(clock, format) gives the time for the match log.
Avoid it for
- Stopping the clock at time up. Play goes on until the ball is next dead, and that time still counts for the sin bin.
- Counting the time in your own interval. The clock ticks by itself, so nothing else re-renders each second.
- extraTime without extra in the format. Sevens, sevens finals, and 10s have it by law. Add your competition's for 15s and the age grades.
- A countdown for training drills. This is a match clock on playing time.
Props
- value
{ phase, period, played, periodStart, running, since }. null before the match is set up, and it reads "Not started". initialMatchClock() gives a clock before kick-off.
MatchClockValue | null
- format
A preset id from matchFormats, or { label, players, half, bin, halfTime?, extra? } in minutes.
MatchFormatId | MatchFormat · default "15s"
- layout
inline is one line. console is the timekeeper's view. controls is the next line and the buttons only.
"inline" | "console" | "controls" · default "inline"
- onChange
Makes it the timekeeper's clock. The console and controls show the buttons, and the console's digits can be set.
(next: MatchClockValue, action: MatchClockAction) => void
- extraTime
A drawn knockout. Ending the second half leads to a break, then extra time. The format needs extra.
boolean · default false
- kickOff
Scheduled kick-off, shown before the match, like "14:20".
string
- className
Extra classes. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| value | MatchClockValue | null | - | { phase, period, played, periodStart, running, since }. null before the match is set up, and it reads "Not started". initialMatchClock() gives a clock before kick-off. |
| format | MatchFormatId | MatchFormat | "15s" | A preset id from matchFormats, or { label, players, half, bin, halfTime?, extra? } in minutes. |
| layout | "inline" | "console" | "controls" | "inline" | inline is one line. console is the timekeeper's view. controls is the next line and the buttons only. |
| onChange | (next: MatchClockValue, action: MatchClockAction) => void | - | Makes it the timekeeper's clock. The console and controls show the buttons, and the console's digits can be set. |
| extraTime | boolean | false | A drawn knockout. Ending the second half leads to a break, then extra time. The format needs extra. |
| kickOff | string | - | Scheduled kick-off, shown before the match, like "14:20". |
| className | string | - | Extra classes. Later classes win. |
Accessibility
- The digits are role="timer", which screen readers do not read out each second.
- With onChange, a polite live region says what each button did, like "Clock stopped at 12:04", and says "Time up" once as the period runs out.
- The buttons are native buttons with words. The main one is solid, so it is not told apart by color alone.
- The settable digits are a button named with the time and what a press does. The field it opens takes Esc to cancel, before any sheet or dialog around it closes.
- Time off and Time up are words, in the warning tone's soft text, which the theme holds to 4.5:1.
- The period track is hidden from screen readers, since the period's name says the same. Each segment keeps a border in forced-colors mode.
Examples
On a scoreboard
// clock comes from your app's synced state.
<MatchClock value={clock} format="sevens" kickOff="14:20" />The timekeeper's console
"use client";
import { useState } from "react";
import { initialMatchClock, MatchClock } from "@/components/ui/match-clock";
export function Timekeeper() {
const [clock, setClock] = useState(() => initialMatchClock());
return <MatchClock layout="console" format="15s" value={clock} onChange={(next) => setClock(next)} kickOff="14:20" />;
}With extra time after a level second half
<MatchClock layout="console" format="10s" extraTime value={clock} onChange={setClock} />Driving it without its buttons
import { matchClockDisplay, stepMatchClock } from "@/components/ui/match-clock";
const stopped = stepMatchClock(clock, "15s", "stop");
const reads = matchClockDisplay(stopped, "15s"); // "52:14"