Skip to content
txbrendan/ui

Finals Board

beta

Finals day on one board: two semi-finals feed the cup final and the bronze final, each pitch's matches sit on a shared timeline, and a hold moves the later matches on the final's pitch.

Preview

before any result: choose a team to record its win, and ⌘Z takes it back for 8 seconds

Cup bracket
Semi-final 1Pitch 1 · 13:00
RoversLions
Semi-final 2Pitch 2 · 13:00
HawksWolves
Cup finalPitch 1 · 14:30
Semi-final 1 winner
Semi-final 2 winner
Bronze finalPitch 2 · 14:00
Semi-final 1 loser
Semi-final 2 loser

Champion to be decided

Day control
Pitch 1
  • Semi-final 113:00 to 13:40
  • Cup final14:30 to 15:10
Pitch 2
  • Semi-final 213:00 to 13:40
  • Bronze final14:00 to 14:40

Moves each match on Pitch 1 with no result 15 min later.

both semi-finals in, and Pitch 1 held: the cup final moves 15 minutes

Cup bracket
Semi-final 1Pitch 1 · 13:00Rovers won.
RoversLions
Semi-final 2Pitch 2 · 13:00Wolves won.
HawksWolves
Cup finalPitch 1 · 14:45moved +15 min
RoversWolves
Bronze finalPitch 2 · 14:00
LionsHawks

Champion to be decided

Day control
Pitch 1
  • Semi-final 113:00 to 13:40
  • Cup final14:45 to 15:25 +15
Pitch 2
  • Semi-final 213:00 to 13:40
  • Bronze final14:00 to 14:40

Moves each match on Pitch 1 with no result 15 min later.

played out, with the champion in place on load

Cup bracket
Semi-final 1Pitch 1 · 13:00Rovers won.
RoversLions
Semi-final 2Pitch 2 · 13:00Wolves won.
HawksWolves
Cup finalPitch 1 · 14:30Rovers won.
RoversWolves
Bronze finalPitch 2 · 14:00Hawks won.
LionsHawks

Champion: Rovers

Day control
Pitch 1
  • Semi-final 113:00 to 13:40
  • Cup final14:30 to 15:10
Pitch 2
  • Semi-final 213:00 to 13:40
  • Bronze final14:00 to 14:40

Moves each match on Pitch 1 with no result 15 min later.

no bronze final, no kits so each team shows its initials, 70-minute slots, and a 10-minute hold

Cup bracket
Semi-final 1Main pitch · 10:00
Lions UnitedBears
Semi-final 2Main pitch · 11:30
Tigers CitySaints
Plate finalMain pitch · 15:00
Semi-final 1 winner
Semi-final 2 winner

Champion to be decided

Day control
Main pitch
  • Semi-final 110:00 to 11:10
  • Semi-final 211:30 to 12:40
  • Plate final15:00 to 16:10

Moves each match on Main pitch with no result 10 min later.

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/finals-board

Use it for

  • Running a cup's finals day: record each winner as it happens, and hold the final's pitch when it runs late.
  • Pass the four sides as the semi-finals pair them, and where and when each match is played. The slot and the hold's length come from the competition.
  • Leave out schedule.bronze when there is no bronze final.
  • Give each team its kit colors. A team without them shows its initials.
  • Pass value and onChange to keep results in your data, or defaultValue to let the board keep its own. The same goes for held, onHold, and defaultHeld.

Avoid it for

  • A draw bigger than four. Use KnockoutBracket, which takes any number of rounds.
  • Showing a past finals day. Nothing here should be pressed then: use KnockoutBracket or MatchCard.
  • A league day. Nothing feeds anything, so there is no bracket to fill in.

Props

  • semis

    The four sides, as the semi-finals pair them. Each is { id, name, colors?, pattern? }.

    [[FinalsTeam, FinalsTeam], [FinalsTeam, FinalsTeam]]

  • schedule

    Each match's { pitch, kickoff, title? }, with kickoff like "13:00". Without bronze, there is no bronze final.

    { sf1, sf2, final, bronze? }

  • minutes

    How long each match holds its pitch: the competition's slot.

    number · default 40

  • value

    Winners by match, as team ids, controlled. Like { sf1: "rovers", final: null }.

    FinalsResults

  • defaultValue

    Winners at the start, when the board keeps its own.

    FinalsResults

  • onChange

    Hears every result, and the old results back on ⌘Z.

    (results: FinalsResults) => void

  • held

    Whether the final's pitch is held now, controlled.

    boolean

  • defaultHeld

    Whether it starts held, when the board keeps its own.

    boolean · default false

  • onHold

    Hears the hold switch.

    (held: boolean) => void

  • holdMinutes

    How far a hold moves the matches on the final's pitch that have no result when it starts. A moved match keeps its new kickoff once recorded.

    number · default 15

  • className

    Extra classes. Later classes win.

    string

Accessibility

  • Cup bracket and Day control are groups named by their titles.
  • Each match's two teams are a radio group, named by the match and described by its pitch and kickoff. Tab reaches the group, the arrow keys move between the teams, and the chosen team is the winner.
  • A recorded match also names its winner in text, like "Rovers won.", which the group's description reads with its pitch and kickoff.
  • A final waits as a plain group that names who it waits for, like "Semi-final 1 winner", until both semi-finals are in.
  • One live line says each result and each hold once, like "Semi-final 1: Rovers won. Cup final and Bronze final cleared." ⌘Z says the undo in the page's own region.
  • ⌘Z works with focus on a team, since the teams are not text fields.
  • A held match's new kickoff has "+15 min" beside it, in words as well as amber, so it shows in forced colors.
  • The timeline is a named region with a list per pitch. It takes a Tab stop only when it scrolls sideways.
  • The champion types itself in only when the final is recorded on the page, never on load. Screen readers hear the name whole. Under reduced motion or with motion off, it shows at once.

Examples

Four teams on two pitches, with a bronze final

<FinalsBoard
  semis={[[rovers, lions], [hawks, wolves]]}
  schedule={{
    sf1: { pitch: "Pitch 1", kickoff: "13:00" },
    sf2: { pitch: "Pitch 2", kickoff: "13:00" },
    final: { pitch: "Pitch 1", kickoff: "14:30" },
    bronze: { pitch: "Pitch 2", kickoff: "14:00" },
  }}
/>

Results in your data

const [results, setResults] = useState<FinalsResults>({});
const [held, setHeld] = useState(false);

<FinalsBoard semis={semis} schedule={schedule} value={results} onChange={setResults} held={held} onHold={setHeld} />