Command Palette
Search for a command to run...
Loading component...
pnpm dlx shadcn add @skiper-ui/skiper107Knockout bracket
A Google-style tournament knockout bracket that pages one round at a time. The leftmost round stacks compactly while later rounds center between their feeder matches, with cards, elbow connectors and headers animating to every new layout.
Dependencies
Interaction Type
| Click the right chevron to page forward a round | |
| Watch the bracket re-flow and collapse its height | |
| Click the left chevron to reveal earlier rounds | |
| Winners carry a marker, shootouts show FT (P) |
Install via Pro CLI [Requires a Pro license key]
pnpm dlx shadcn add @skiper-ui/skiper107How to use
KnockoutBracket renders a tournament tree the way Google's World Cup widget does: instead of one giant scrollable bracket, it shows a window of three rounds and pages one round at a time. The leftmost visible round stacks its matches compactly; every later round centers each match between its two feeder matches, and elbow connectors tie the pairs together. When you page, every card, connector, header and the container height animates to the new layout.
Pass it a rounds array where each round has exactly half as many matches as the previous one (16 → 8 → 4 → 2 → 1). The bundled ROUNDS and TEAMS mock data mirror a full World Cup knockout stage and are handy as a starting shape.
Basic usage
import { KnockoutBracket, ROUNDS } from "@/components/v1/skiper107"; const Demo = () => ( <div className="flex w-full justify-center py-12"> <KnockoutBracket rounds={ROUNDS} /> </div> );
Starting round
By default the bracket opens on round index 1 (Round of 16 in the demo data), keeping the earliest round tucked behind the left chevron. Set initialRound to change that:
// Open on the Round of 32 instead <KnockoutBracket rounds={ROUNDS} initialRound={0} /> // Jump straight to the semi-finals + final view <KnockoutBracket rounds={ROUNDS} initialRound={2} />
The value is clamped, so anything past the last valid window falls back to the semi-finals + final view.
Bring your own data
Each match names two sides. A side with team: null renders as a TBD slot with a shield icon. Flags load from flagcdn.com using the team's ISO 3166-1 alpha-2 code (England is gb-eng).
import { KnockoutBracket, type Round } from "@/components/v1/skiper107"; const rounds: Round[] = [ { name: "Semi-finals", matches: [ { id: "sf-1", date: "Sat, 12 Jul", status: "finished", home: { team: { name: "France", code: "fr" }, score: 2 }, away: { team: { name: "Spain", code: "es" }, score: 1 }, winner: "home", }, { id: "sf-2", date: "Sun, 13 Jul", time: "9:00 pm", status: "upcoming", home: { team: { name: "Brazil", code: "br" }, score: null }, away: { team: null, score: null }, }, ], }, { name: "Final", matches: [ { id: "f-1", date: "Sat, 19 Jul", time: "8:00 pm", status: "upcoming", home: { team: null, score: null }, away: { team: null, score: null }, }, ], }, ]; const Cup = () => <KnockoutBracket rounds={rounds} initialRound={0} />;
Penalty shootouts
Give both sides a penalties value and the card renders Google-style shootout scores — 1 (3) — and swaps the full-time badge to FT (P). winner still decides who gets the marker and who dims.
{ id: "r32-2", date: "Tue, 30 Jun", status: "finished", home: { team: TEAMS.netherlands, score: 1, penalties: 2 }, away: { team: TEAMS.morocco, score: 1, penalties: 3 }, winner: "away", }
Props | Description |
|---|---|
rounds | Ordered rounds of the tournament. Every round must have half as many matches as the one before it, ending with a single final |
initialRound | Index of the round shown as the leftmost column on mount. Defaults to 1 so the earliest round stays behind the left chevron. Clamped to the valid range |
className | Classes applied to the bracket's outer container |
Data model
Props | Description |
|---|---|
Round | A column of the bracket, e.g. "Quarter-finals" |
Match | One tie. status is "finished" | "upcoming"; winner is "home" | "away" and controls the marker and dimming |
MatchSide | One side of a match. team: null renders a TBD shield; penalties renders shootout scores and the FT (P) badge |
Team | code is the ISO 3166-1 alpha-2 country code used to load the flag from flagcdn.com |
Demo
import { Skiper107 } from "@/components/v1/skiper107"; const DemoSkiper107 = () => ( <div className="h-screen w-full"> <Skiper107 /> </div> );
Notes
- Layout is computed, not scrolled: the base round stacks at a fixed rhythm and every later match sits at the exact vertical midpoint of its two feeders, so pairs always line up with their connectors
- Paging animates everything with a single shared transition (0.5s cubic-bezier) — cards, elbow connectors, entry stubs, round headers, chevron opacity and the container height move together
- Rounds behind the window collapse onto their parent match as they fade out, which keeps the exit animation tidy when paging forward
- Card geometry is driven by the
CARD_W,CARD_H,GAP_XandGAP_Yconstants at the top of the file — keepCARD_Hin sync if you change the card's internal spacing - The window shows up to three rounds (
VISIBLE_COLS); the last page always shows the final two rounds, and chevrons hide at either end - Flags are plain
<img>tags served by flagcdn.com — swapTeamFlagif you need self-hosted assets - The "All times are in India Standard Time" footer is static demo copy — adjust or remove it for your locale
- Relative date labels in the mock data ("Yesterday", "Today") are plain strings, not computed dates
Source code
Keep in mind
Most components here are recreations of the best out there. I don't claim to be the original creator. This is my attempt to reverse-engineer, replicate, and often add a few extra features. I've tried to credit everyone, but if I missed something, let me know.
Contact
License & Usage:
- Free to use and modify in both personal and commercial projects.
- Attribution to Skiper UI is required when using the free version.
- No attribution required with Skiper UI Pro.