webdevpuneet.com

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

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

CategoryLayouts

Copy & paste

Source Code

<div class="mbt-wrap">
  <h3 class="mbt-title">Quarterfinals → Semifinals → Final</h3>
  <div class="mbt-bracket" id="mbtBracket">
    <svg class="mbt-lines" id="mbtLines"></svg>
    <div class="mbt-round" id="mbtRound0"></div>
    <div class="mbt-round" id="mbtRound1"></div>
    <div class="mbt-round" id="mbtRound2"></div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

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.

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 a single-elimination bracket where SVG connectors are drawn from real getBoundingClientRect positions instead of guessed pseudo-element heights.
Esports and gaming platforms
Update a bracket as matches finish, with semifinal and final slots computed from quarterfinal winners rather than typed in.
Contests and trivia nights
Run hackathon or community competitions, pairing with a live match scoreboard for the match in progress.
Voting and elimination polls
Adapt the structure for head-to-head voting, where floor(i / 2) feed-index maths generalises to any power-of-two bracket size.
Standings and measured layout
Pair with a leaderboard table for final standings, and study connectors that redraw safely on window resize.
Related: Swiper Cards Deck
See the Swiper Cards Deck for a related layouts pattern worth pairing with this one.

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.