npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@amirhossein-shk/tournament-bracket-js

v1.0.7

Published

Lightweight vanilla JS tournament bracket library

Downloads

53

Readme

Tournament Bracket

A lightweight JavaScript library for rendering and managing single-elimination tournament brackets in the browser.

It supports both classic browser usage with a <script> tag and modern ESM usage with import. The library renders bracket rounds, matches, players, scores, avatars, and SVG connector lines, while also exposing a small API for updating matches, finishing matches, reading state, and handling events.


Features

  • Render single-elimination tournament brackets
  • Use with a normal browser <script> tag
  • Use with modern ESM import
  • Support player names, scores, avatars, and fallback avatars
  • Automatically normalize missing player data
  • Validate bracket structure before rendering
  • Update match data after initialization
  • Set individual player scores
  • Finish matches and propagate winners
  • Listen to match click, update, and finish events
  • Ship separate JavaScript and CSS builds
  • Include both minified and non-minified output files

Demo

Try the live demo:

https://amirhossein-shk.github.io/tournament-bracket/demo/


File Structure

Recommended project structure:

project/
  src/
    tournament-bracket.js
    tournament-bracket.scss
    entry-browser.js

  dist/
    tournament-bracket.js
    tournament-bracket.min.js
    tournament-bracket.esm.js
    tournament-bracket.esm.min.js
    tournament-bracket.css
    tournament-bracket.min.css

  demo/
    index.html
    tournament-bracket.min.js
    tournament-bracket.min.css

  README.md
  package.json
  vite.config.js

Folder Purpose

| Folder | Purpose | |---|---| | src/ | Source code of the library | | dist/ | Final build output for users | | demo/ | Live demo and usage reference |


Build Files

The dist folder contains the final files that users should consume.

dist/
  tournament-bracket.js
  tournament-bracket.min.js
  tournament-bracket.esm.js
  tournament-bracket.esm.min.js
  tournament-bracket.css
  tournament-bracket.min.css

JavaScript Files

| File | Usage | |---|---| | tournament-bracket.js | Browser build, non-minified | | tournament-bracket.min.js | Browser build, minified | | tournament-bracket.esm.js | ESM build, non-minified | | tournament-bracket.esm.min.js | ESM build, minified |

CSS Files

| File | Usage | |---|---| | tournament-bracket.css | Stylesheet, non-minified | | tournament-bracket.min.css | Stylesheet, minified |

The CSS is intentionally shipped separately from JavaScript. This makes styling easier to override, easier to debug, and easier to use with bundlers or CDNs.


Browser Usage

Use this option when you want to include the library directly in an HTML page.

<link rel="stylesheet" href="./dist/tournament-bracket.min.css" />
<script src="./dist/tournament-bracket.min.js"></script>

After loading the browser build, the global function is available as:

tournamentBracket(...)

Complete Browser Example

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Tournament Bracket</title>
  <link rel="stylesheet" href="./dist/tournament-bracket.min.css" />
</head>
<body>
  <div id="tournament-bracket"></div>

  <script src="./dist/tournament-bracket.min.js"></script>
  <script>
    const tb = tournamentBracket({
      targetId: "tournament-bracket",
      rounds: [
        {
          roundId: "r1",
          name: "Quarter Finals",
          matches: [
            {
              matchId: "m1",
              isFinished: true,
              players: [
                { name: "Player 1", score: 2, avatarUrl: "" },
                { name: "Player 2", score: 1, avatarUrl: "" }
              ]
            },
            {
              matchId: "m2",
              players: [
                { name: "Player 3", score: "-" },
                { name: "Player 4", score: "-" }
              ]
            }
          ]
        },
        {
          roundId: "r2",
          name: "Semi Finals",
          matches: [
            {
              matchId: "m3",
              players: [{}, {}]
            }
          ]
        }
      ]
    });

    tb.init();
  </script>
</body>
</html>

ESM Usage

Use this option when working with a module-based setup.

import tournamentBracket from "./dist/tournament-bracket.esm.js";
import "./dist/tournament-bracket.css";

Complete ESM Example

import tournamentBracket from "./dist/tournament-bracket.esm.js";
import "./dist/tournament-bracket.css";

const tb = tournamentBracket({
  targetId: "tournament-bracket",
  rounds: [
    {
      roundId: "r1",
      name: "Quarter Finals",
      matches: [
        {
          matchId: "m1",
          isFinished: true,
          players: [
            { name: "Player 1", score: 2, avatarUrl: "" },
            { name: "Player 2", score: 1, avatarUrl: "" }
          ]
        },
        {
          matchId: "m2",
          players: [
            { name: "Player 3", score: "-" },
            { name: "Player 4", score: "-" }
          ]
        }
      ]
    },
    {
      roundId: "r2",
      name: "Semi Finals",
      matches: [
        {
          matchId: "m3",
          players: [{}, {}]
        }
      ]
    }
  ]
});

tb.init();

If you use ESM directly in the browser, load your code with:

<script type="module" src="./your-file.js"></script>

Do not open ESM files directly with file://. Use a local development server instead.


Required HTML

The page must contain a target container element:

<div id="tournament-bracket"></div>

The targetId option must match this element ID:

const tb = tournamentBracket({
  targetId: "tournament-bracket",
  rounds: []
});

Configuration

Create a bracket instance by calling:

const tb = tournamentBracket(config);

Available Options

| Option | Type | Default | Description | |---|---|---:|---| | targetId | string | "tournament-bracket" | ID of the container element | | rounds | Array | required | Tournament rounds data | | distance | number | 42 | Internal spacing used for layout and connectors | | width | number | 196 | Width of each match card | | matchHeight | number | 94 | Height of each match card | | roundGap | number | 96 | Horizontal gap between rounds | | avatarFallbackUrl | string | "./images/avatar.png" | Fallback avatar when a player avatar is missing | | connectorColor | string | "white" | Color of SVG connector lines | | onMatchClick | function | noop | Called when a match is clicked | | onMatchUpdate | function | noop | Called when a match is updated | | onMatchFinish | function | noop | Called when a match is finished |


Full Configuration Example

const sampleConfig = {
  targetId: "tournament-bracket",
  distance: 80,
  width: 196,
  matchHeight: 72,
  roundGap: 80,
  connectorColor: "#64748b",
  rounds: [
    {
      roundId: "r1",
      name: "Quarter Finals",
      matches: [
        {
          matchId: "m1",
          isFinished: true,
          players: [
            {
              name: "Player 1",
              score: 2,
              avatarUrl: ""
            },
            {
              name: "Player 2",
              score: 1,
              avatarUrl: ""
            }
          ]
        },
        {
          matchId: "m2",
          players: [
            {
              name: "Player 3",
              score: "-"
            },
            {
              name: "Player 4",
              score: "-"
            }
          ]
        }
      ]
    },
    {
      roundId: "r2",
      name: "Semi Finals",
      matches: [
        {
          matchId: "m3",
          players: [{}, {}]
        }
      ]
    }
  ]
};

Data Structure

Rounds

The rounds array defines the complete tournament structure.

rounds: [
  {
    roundId: "r1",
    name: "Quarter Finals",
    matches: [
      {
        matchId: "m1",
        players: [
          { name: "Player 1", score: 2 },
          { name: "Player 2", score: 1 }
        ]
      }
    ]
  }
]

Round Object

| Field | Type | Required | Description | |---|---|---|---| | roundId | string | no | Optional custom round ID | | name | string | no | Optional round name | | matches | Array | yes | List of matches in this round |

Match Object

| Field | Type | Required | Description | |---|---|---|---| | matchId | string | no | Optional custom match ID | | isFinished | boolean | no | Marks the match as completed | | players | Array | yes | Exactly two players |

Player Object

| Field | Type | Required | Description | |---|---|---|---| | name | string | no | Player display name | | score | number \| string | no | Player score, or "-" for pending score | | avatarUrl | string | no | Player avatar URL |


Default Player Values

If a player is missing or partially defined, the library fills missing fields with default values.

{
  name: "-",
  avatarUrl: "",
  score: "-"
}

This is valid:

players: [{}, {}]

This is also valid:

players: [
  { name: "Player 1" },
  {}
]

Empty player objects are useful for future rounds where participants are not known yet.


Validation Rules

The library validates the tournament structure before rendering.

The rules are:

  • rounds must be a non-empty array
  • every round must be an object
  • every round must contain a matches array
  • every match must be an object
  • every match must contain exactly 2 players
  • the first round match count must be a power of two
  • each next round must contain exactly half the matches of the previous round
  • the final round must contain exactly 1 match

Valid Bracket Shape

Round 1: 8 matches
Round 2: 4 matches
Round 3: 2 matches
Round 4: 1 match

Invalid Bracket Shape

Round 1: 3 matches
Round 2: 2 matches
Round 3: 1 match

This is invalid because the first round has 3 matches, and 3 is not a power of two.

Tournament math is strict. Sadly, it does not accept emotional arguments.


Instance API

tournamentBracket(config) returns an object with these methods:

{
  init,
  updateMatch,
  setMatchScore,
  finishMatch,
  destroy,
  getState,
  onMatchClick,
  onMatchUpdate,
  onMatchFinish
}

Methods

init()

Initializes and renders the bracket.

tb.init();

Call this after creating the instance.


updateMatch(matchId, patch)

Updates an existing match by ID.

tb.updateMatch("m2", {
  players: [
    { name: "Player 3", score: 1 },
    { name: "Player 4", score: 2 }
  ]
});

Use this when you want to update match data after initialization.


setMatchScore(matchId, score1, score2)

Updates both player scores in a match.

tb.setMatchScore("m2", 3, 1);

Arguments:

| Argument | Description | |---|---| | matchId | ID of the match | | score1 | First player score | | score2 | Second player score |


finishMatch(matchId, score1, score2)

Marks a match as finished with final scores.

tb.finishMatch("m2", 3, 1);

When a match is finished, the library can resolve the winner and move the winner forward according to the bracket structure.


destroy()

Clears the rendered bracket from the target container.

tb.destroy();

Use this when you want to remove the bracket or re-render from scratch.


getState()

Returns the current internal bracket state.

const state = tb.getState();
console.log(state);

This is useful for debugging, inspection, or syncing with external application state.


Event Callbacks

Callbacks can be passed in the initial configuration.

const tb = tournamentBracket({
  ...sampleConfig,
  onMatchClick: function (match) {
    console.log("clicked", match);
  },
  onMatchUpdate: function (match) {
    console.log("updated", match);
  },
  onMatchFinish: function (match) {
    console.log("finished", match);
  }
});

They can also be registered or replaced later using the instance API.


onMatchClick(fn)

Registers a callback for match click events.

tb.onMatchClick(function (match) {
  console.log("clicked", match);
});

onMatchUpdate(fn)

Registers a callback for match update events.

tb.onMatchUpdate(function (match) {
  console.log("updated", match);
});

onMatchFinish(fn)

Registers a callback for match finish events.

tb.onMatchFinish(function (match) {
  console.log("finished", match);
});

Complete Event Example

const tb = tournamentBracket({
  ...sampleConfig,
  onMatchClick: function (match) {
    console.log("config.onMatchClick", {
      matchId: match.matchId,
      players: match.players
    });
  },
  onMatchUpdate: function (match) {
    console.log("config.onMatchUpdate", {
      matchId: match.matchId,
      players: match.players,
      isFinished: match.isFinished
    });
  },
  onMatchFinish: function (match) {
    console.log("config.onMatchFinish", {
      matchId: match.matchId,
      players: match.players
    });
  }
});

tb.init();

Typical Usage Flow

A common usage flow looks like this:

const tb = tournamentBracket(sampleConfig);

tb.init();

tb.setMatchScore("m2", 2, 0);

tb.finishMatch("m2", 2, 0);

console.log(tb.getState());

Demo

The demo/ folder can contain a live example of the library.

Recommended demo structure:

demo/
  index.html
  tournament-bracket.min.js
  tournament-bracket.min.css

The demo should show both:

  • the rendered tournament bracket
  • the code used to create it

This way, users can see how the library works and copy the usage code from the same place.


Styling

The library requires CSS for proper visual presentation.

Browser Usage

<link rel="stylesheet" href="./dist/tournament-bracket.min.css" />
<script src="./dist/tournament-bracket.min.js"></script>

ESM Usage

import "./dist/tournament-bracket.css";
import tournamentBracket from "./dist/tournament-bracket.esm.js";

If the JavaScript works but the bracket looks broken or unstyled, the CSS file is probably missing.

Without CSS, the bracket technically works, but visually it may look like it lost a fight with a spreadsheet.


Troubleshooting

The bracket does not render

Check that the target container exists:

<div id="tournament-bracket"></div>

Also check that the targetId matches:

targetId: "tournament-bracket"

The browser says tournamentBracket is not defined

Make sure the browser build is loaded before your script:

<script src="./dist/tournament-bracket.min.js"></script>
<script>
  const tb = tournamentBracket(config);
</script>

ESM import does not work

Make sure you are using the ESM build:

import tournamentBracket from "./dist/tournament-bracket.esm.js";

If you are testing directly in the browser, use a local server and a module script:

<script type="module" src="./tournament-bracket.min.js"></script>

Do not rely on file:// for ESM testing.


Styles are missing

Include the CSS file:

<link rel="stylesheet" href="./dist/tournament-bracket.min.css" />

or import it in your module setup:

import "./dist/tournament-bracket.css";

Invalid rounds error

Check these structure rules:

  • first round match count must be a power of two
  • each next round must have half the matches of the previous round
  • final round must have exactly one match
  • every match must have exactly two players

Links


author: [email protected]


License

MIT