Bracket Match
betaOne knockout match on its own, 124px tall and as wide as its column, with a peek that adds what the tile has no room for: the round, the result in words, and facts like the venue or form.
Preview
hover or focus a match: its peek adds what the tile has no room for
before kick-off, and live: a badge replaces FT
other badges: AET and Forfeit
width={280}, and never under 240
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/bracket-match
Use it for
- A single knockout fixture in a feed, a list, or a side panel.
- Pass the same BracketMatchData a KnockoutBracket takes. It shows the date and time before kick-off, and FT once it's finished.
- Pass round, like "Cup semi-final", so its peek says which round it is.
- Pass onClick and selected to make it a match you choose, like a card that opens the match panel.
- Use badge for "Live 9'", "AET", or "Forfeit". It replaces FT. A level score with kicks reads FT (P).
Avoid it for
- A whole draw. Use KnockoutBracket, which lays the tiles out round by round.
- A fixture outside a knockout, or with a flat team shape. Use MatchCard, which has no peek.
- Putting the peek's facts only in the peek. The tile already shows them, so the peek just repeats them.
Props
- match
The match: { id, date?, time?, status?, home, away, winner?, badge? }. Each side is { team, score, penalties?, label? }.
BracketMatchData
- round
The round's name. It titles the peek, like "Cup semi-final". Without it, the teams do.
string
- facts
What the tile cannot show, for the peek, as [label, value] pairs, like ["Venue", "Riverside Park"] or each side's form. The peek never repeats the tile's teams, score, or date.
PeekFact[]
- selected
Draws it as the chosen match. Only a match with onClick can be chosen.
boolean · default false
- onClick
Makes the match a button that says whether it is pressed.
() => void
- width
A fixed width in px, never under 240. By default it fills its column.
number
- effect
How the peek opens and closes. rise fades in and moves out from the match. grow fades in and grows from the match, and closes the same way. Left out, it follows an effect-<name> class on <body>, where popups open, and rises with none.
"rise" | "grow" · default "rise"
- className
Extra classes for the root. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| match | BracketMatchData | - | The match: { id, date?, time?, status?, home, away, winner?, badge? }. Each side is { team, score, penalties?, label? }. |
| round | string | - | The round's name. It titles the peek, like "Cup semi-final". Without it, the teams do. |
| facts | PeekFact[] | - | What the tile cannot show, for the peek, as [label, value] pairs, like ["Venue", "Riverside Park"] or each side's form. The peek never repeats the tile's teams, score, or date. |
| selected | boolean | false | Draws it as the chosen match. Only a match with onClick can be chosen. |
| onClick | () => void | - | Makes the match a button that says whether it is pressed. |
| width | number | - | A fixed width in px, never under 240. By default it fills its column. |
| effect | "rise" | "grow" | "rise" | How the peek opens and closes. rise fades in and moves out from the match. grow fades in and grows from the match, and closes the same way. Left out, it follows an effect-<name> class on <body>, where popups open, and rises with none. |
| className | string | - | Extra classes for the root. Later classes win. |
Accessibility
- The tile names both sides, the scores, and the winner in text. The winner's caret and weight only show it.
- With onClick, the tile is a button with aria-pressed, and the peek follows its hover and focus. Enter stays the button's own.
- Without onClick, the tile is the peek's trigger: a button that says it opens a dialog.
- The peek is a dialog named by its title: the round, or the two teams when there is no round.
- A chosen match has a 2px outline that shows in forced colors.
Examples
Finished, with the round and the venue in its peek
<BracketMatch
round="Cup semi-final"
facts={[
["Venue", "Riverside Park"],
["Rovers' form", "W W D W"],
]}
match={{
id: "sf1",
date: "Sat 17 Oct",
time: "15:20",
status: "finished",
home: { team: { name: "Rovers" }, score: 24 },
away: { team: { name: "Lions" }, score: 17 },
}}
/>Chosen, with onClick
<BracketMatch match={match} round="Final" selected={selected === match.id} onClick={() => setSelected(match.id)} />Live, at a fixed width
<BracketMatch
width={280}
match={{
id: "f",
time: "15:00 · Pitch 1",
badge: "Live 9'",
home: { team: { name: "Hawks" }, score: 3 },
away: { team: { name: "Wolves" }, score: 0 },
}}
/>Peek grows from the match
<BracketMatch
effect="grow"
round="Cup final"
match={{
id: "f1",
date: "Sat 24 Oct",
time: "15:00",
home: { team: { name: "Rovers" }, score: null },
away: { team: { name: "Hawks" }, score: null },
}}
/>