Tournament Match Bracket — Free Single-Elimination Bracket HTML CSS JS

Tournament Match Bracket · Layouts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Measured, not guessed, connectors
SVG paths are drawn from real getBoundingClientRect() positions, so they always line up.
Data-derived rounds
Semifinal and final matches are computed from quarterfinal winners, never hand-authored separately.
Resize-safe layout
Connectors redraw on window resize as flex spacing shifts match positions.
Generalizes to any bracket size
The floor(i / 2) feed-index math works for any power-of-two round count.
Clear winner state
A distinct background, weight, and checkmark mark the advancing competitor per match.
Champion accent
The final round's winning slot gets a distinct gold treatment via CSS nth-child.
Overflow-safe names
Long competitor names truncate cleanly instead of breaking match card layout.
Framework-friendly structure
The measure-then-draw pattern ports directly to any component framework's ref-based DOM access.

About this UI Snippet

Tournament Match Bracket — Connector Lines Computed From Real Layout, Not Guessed

Screenshot of the Tournament Match Bracket snippet rendered live

Most CSS-only tournament brackets fake their connector lines with fixed-height pseudo-elements that only line up if every match box happens to be exactly the same height and every round's spacing math works out by coincidence. This snippet takes a more reliable approach: it renders three rounds of matches with flexbox, then measures their *actual* rendered positions with getBoundingClientRect() and draws SVG paths between them — so the connectors are always correct, in plain HTML, CSS, and vanilla JavaScript.

Winners derived from data, not hardcoded per round

The bracket starts from one array, QF, holding four quarterfinal matches with a winner index each. buildRound() takes the previous round's winners two at a time and builds the next round's matches from them — so SF (semifinals) and FINAL are *computed*, not separately authored, guaranteeing the bracket is always internally consistent: whoever the data says won a quarterfinal is exactly who appears in the semifinal slot that quarterfinal feeds.

Layout with flexbox, connectors with measured SVG

Each round is a flex column with justify-content: space-around, which gets matches visually close to their correct positions cheaply. But rather than trust that spacing to be pixel-exact — which breaks the moment match card heights vary, text wraps differently, or the browser rounds subpixel values differently — drawConnectors() reads every match element's real getBoundingClientRect() after render and draws an SVG path from the vertical center of each feeding match's right edge to the vertical center of the match it feeds into's left edge, with a horizontal-vertical-horizontal "elbow" shape (M x y H midX V targetY H targetX).

Why this connector logic is provably correct

Match i in round r always feeds match Math.floor(i / 2) in round r + 1 — that's the fixed mathematical structure of single elimination, and it's exactly the index math drawConnectors() uses to find each source match's target element. Because the coordinates come from the live DOM rather than assumed CSS math, the lines connect correctly even if you change match card height, font size, or round spacing — there's no magic number to keep in sync.

Redraws when the layout can change

The initial draw runs inside requestAnimationFrame so it measures positions only after the browser has committed layout, and a resize listener redraws the connectors whenever the viewport changes — since flex spacing (and therefore every match's pixel position) shifts with the container width.

Overflow-safe names, clear winner state

Long player names truncate with an ellipsis inside each slot, and the winning competitor in each match gets a distinct background, bold weight, and a checkmark — with the champion's final-round win additionally accented gold via a :nth-child selector on the last round.

Customizing it

Extend ROUNDS/ROUND_IDS to support 16 or 32 players (the connector math needs no changes — it already generalizes to any number of rounds), swap the winner-selection logic for real live results, or animate each connector path drawing in with stroke-dasharray. Pair it with a leaderboard table for a seeding list alongside the bracket.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than debugging misaligned bracket lines by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why drawConnectors() measures real getBoundingClientRect() positions instead of relying on CSS spacing to line matches up, and how the Math.floor(i / 2) feed-index math generalizes to brackets with more rounds. The same assistant can help optimize it — for example asking whether redrawing on every resize event should be throttled/debounced for very rapid window resizing, or whether a ResizeObserver on the bracket container would be more precise than a window resize listener. It's also useful for extending the bracket: ask it to animate each connector path drawing in with stroke-dasharray as results come in, add support for a third-place consolation match, or handle byes for a non-power-of-two number of competitors. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a single-elimination "tournament match bracket" for 8 competitors (quarterfinals, semifinals, final — 3 rounds) in plain HTML, CSS, and JavaScript with no library.

Requirements:
- A base data array of 4 quarterfinal match objects, each holding two competitor names and a winner index (0 or 1); a function that takes any round's array of matches and builds the next round's matches by pairing up consecutive matches' winners, so the semifinal and final rounds are computed from the quarterfinal data rather than being separately hand-authored (this must guarantee the bracket can never show an inconsistent state where a displayed semifinalist did not actually win their quarterfinal).
- Three round columns laid out with CSS flexbox, each containing that round's match cards, with the currently winning competitor in each match visually distinguished from the loser (different background, font weight, and a checkmark or similar indicator).
- Connector lines between rounds that are computed from the real rendered positions of the match elements (using getBoundingClientRect on the actual DOM, not fixed pixel offsets or percentage-based CSS pseudo-elements assumed to line up), drawn as an SVG overlay positioned absolutely over the bracket container.
- The connector logic must correctly implement the feed relationship where match index i in one round connects to match index Math.floor(i / 2) in the next round, drawing an elbow-shaped path (horizontal, then vertical, then horizontal) from the vertical center of the source match's right edge to the vertical center of the target match's left edge.
- The initial connector drawing must happen only after the browser has committed layout (for example via requestAnimationFrame), and connectors must be redrawn whenever the window is resized, since the flexbox round columns reposition their match cards as available width changes.
- Long competitor names must truncate with an ellipsis rather than breaking a match card's fixed-width layout.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAn 8-player bracket renders across three rounds with winners already highlighted.
  2. 2
    Trace a connectorFollow a line from a quarterfinal match's winner into its semifinal slot — it lines up exactly.
  3. 3
    Resize the browser windowConnector lines redraw to stay attached to their matches at the new layout.
  4. 4
    Edit the QF arrayChange player names or winner indexes — SF and FINAL recompute automatically.
  5. 5
    Extend to 16 playersAdd a round and a corresponding round element/ID; the connector loop generalizes automatically.
  6. 6
    Wire in live resultsUpdate a match's winner index as real results come in, then re-render and redraw.

Real-world uses

Common Use Cases

Sports tournament sites
Show single-elimination brackets for leagues, playoffs, or local tournaments.
Esports and gaming platforms
Live-update a bracket as best-of-N matches conclude.
Corporate and community contests
Trivia nights, hackathon judging, or office March Madness-style brackets.
Voting and elimination polls
Adapt the same structure for head-to-head elimination voting rounds.
Event dashboards
Pair with a live match scoreboard for the currently active match.
Learning measured-layout techniques
A reference for getBoundingClientRect()-driven drawing — compare with leaderboard table for a flat ranked alternative.

Got questions?

Frequently Asked Questions

Instead of relying on CSS spacing math to happen to line up, drawConnectors() calls getBoundingClientRect() on every match element after the browser has committed layout, and draws each SVG path between the real measured center-points of a feeding match and the match it advances into. Because the coordinates come from the live rendered DOM rather than an assumed formula, the lines are correct regardless of match card height, font size, or how the browser rounds subpixel flex positions.

Match index i in any round always feeds match Math.floor(i / 2) in the next round — that is the fixed structure of single-elimination brackets (matches 0 and 1 feed slot 0, matches 2 and 3 feed slot 1, and so on). The drawConnectors() loop uses exactly that formula to look up each target match element, so the feed relationship is derived from index math rather than any separately maintained mapping.

buildRound() takes a round\'s matches two at a time and creates the next round\'s match from each pair\'s winner, so SF and FINAL are always internally consistent with QF — there is no way for the semifinal bracket to show a player who did not actually win their quarterfinal, because the data literally cannot represent that state.

Each round is a flexbox column using justify-content: space-around at a fixed width, so match card positions shift horizontally and vertically whenever the viewport (and therefore the flex container\'s available space) changes. Because the SVG lines are drawn from measured pixel coordinates rather than percentages, they need to be recalculated after any layout change, which is why a resize listener re-runs drawConnectors().

Add more matches to the first round\'s data array, call buildRound() one more time to generate the additional round, add a matching round container element and ID to the ROUNDS/ROUND_IDS arrays, and add a CSS column for it. The connector-drawing loop already iterates over ROUNDS.length - 1 pairs of adjacent rounds and uses the generalized floor(i / 2) feed index, so no changes to drawConnectors() itself are needed.