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

bracket-forge

v1.0.0

Published

Tournament bracket generator and React components for single elimination, double elimination, triple elimination, consolation, rebuy, and last chance formats. Supports 4 to 1024 players with SVG rendering and pan/zoom.

Readme

bracket-forge

Tournament bracket generator and React components. Supports 6 formats from 4 to 1024 players with SVG rendering, pan/zoom, and fully connected bracket lines. No external bracket dependencies.

Formats

| Format | Description | |--------|-------------| | Single Elimination | Lose once, eliminated | | Double Elimination | Winners + Losers brackets, Grand Finals with reset | | Triple Elimination | Winners + Losers + Last Life brackets | | Consolation | Main bracket + consolation bracket for Round 1 losers | | Rebuy | R1/R2 losers can buy back in through a secondary bracket | | Last Chance | Losers from every round get one more chance |

Install

npm install bracket-forge

Peer dependencies: react >= 17.0.0, react-dom >= 17.0.0

Quick Start

import { TournamentBracket } from 'bracket-forge';
import 'bracket-forge/styles';

function App() {
  return (
    <div style={{ width: '100%', height: '600px' }}>
      <TournamentBracket format="double" playerCount={16} />
    </div>
  );
}

Usage

Drop-in Component

The simplest way to use bracket-forge. Renders a complete bracket with pan/zoom controls.

import { TournamentBracket } from 'bracket-forge';
import 'bracket-forge/styles';

// Minimal
<TournamentBracket format="single" playerCount={8} />

// With built-in format/player selectors
<TournamentBracket format="double" playerCount={32} showControls />

// Custom wrapper styling
<TournamentBracket
  format="triple"
  playerCount={64}
  className="my-bracket"
  style={{ height: '80vh', background: '#111' }}
/>

Using Generators Directly

Generate bracket data and render it yourself or with the <Bracket /> component.

import { generateDoubleElimination, Bracket } from 'bracket-forge';
import 'bracket-forge/styles';

function MyBracket() {
  const data = generateDoubleElimination(16);

  // Use the built-in renderer
  return <Bracket bracketData={data} />;
}

Pre-generated Data

Pass pre-generated bracket data to skip internal generation.

import { TournamentBracket, generateSingleElimination } from 'bracket-forge';

const data = generateSingleElimination(32);

<TournamentBracket bracketData={data} />

Generators Without React

Use the generator functions standalone for server-side logic, APIs, or custom rendering.

import {
  generateSingleElimination,
  generateDoubleElimination,
  generateTripleElimination,
  generateConsolation,
  generateRebuy,
  generateLastChance,
} from 'bracket-forge';

const bracket = generateDoubleElimination(64);

console.log(bracket.allMatches.length);   // 127
console.log(bracket.sections.length);     // 3 (winners, losers, finals)
console.log(bracket.allConnectors.length); // 191

API Reference

Components

<TournamentBracket />

High-level drop-in component.

| Prop | Type | Default | Description | |------|------|---------|-------------| | format | string | 'single' | 'single' | 'double' | 'triple' | 'consolation' | 'rebuy' | 'lastChance' | | playerCount | number | 8 | Power of 2: 4, 8, 16, 32, 64, 128, 256, 512, 1024 | | showControls | boolean | false | Show format and player count selector buttons | | className | string | '' | CSS class for wrapper div | | style | object | {} | Inline styles for wrapper div | | bracketData | object | null | Pre-generated data (skips internal generation) |

<Bracket />

SVG bracket renderer with pan/zoom. Expects output from a generator function.

| Prop | Type | Description | |------|------|-------------| | bracketData | object | Generator output ({ sections, crossConnectors, allMatches }) |

<Match />

Single match box (SVG group). Used internally by BracketSection.

| Prop | Type | Description | |------|------|-------------| | match | object | Match object with position, players, id | | color | object | { bg, border, text } color scheme |

<Connector />

SVG path connecting two matches. Used internally by BracketSection.

| Prop | Type | Description | |------|------|-------------| | connector | object | { id, path, color, dashed? } |

<BracketSection />

Renders a labeled group of matches and connectors (e.g., "Winners Bracket").

| Prop | Type | Description | |------|------|-------------| | section | object | { id, label, matches[], connectors[], color } |

<FormatSelector />

Button group for selecting tournament format.

| Prop | Type | Description | |------|------|-------------| | selected | string | Current format ID | | onChange | function | (formatId: string) => void |

<PlayerCountSelector />

Button group for selecting player count.

| Prop | Type | Description | |------|------|-------------| | selected | number | Current player count | | onChange | function | (count: number) => void |

Hooks

usePanZoom(initialViewBox)

Pan and zoom hook for SVG elements. Used internally by <Bracket />, also available for custom rendering.

import { usePanZoom } from 'bracket-forge';

const { viewBox, svgRef, handlers, handleWheel, zoomIn, zoomOut, resetView } =
  usePanZoom({ x: 0, y: 0, width: 1000, height: 600 });

Returns:

| Property | Type | Description | |----------|------|-------------| | viewBox | { x, y, width, height } | Current SVG viewBox | | scale | number | Current zoom scale | | svgRef | ref | Attach to SVG element | | handlers | object | Spread onto SVG: onMouseDown, onMouseMove, onMouseUp, onMouseLeave | | handleWheel | function | Attach with addEventListener('wheel', fn, { passive: false }) | | zoomIn | function | Zoom in 1.25x | | zoomOut | function | Zoom out 0.8x | | resetView | function | Reset to initial viewBox |

Generator Functions

All generators accept numPlayers (power of 2, 4-1024) and return a BracketData object.

| Function | Sections Returned | |----------|-------------------| | generateSingleElimination(n) | winners | | generateDoubleElimination(n) | winners, losers, finals | | generateTripleElimination(n) | winners, losers, lastLife, finals | | generateConsolation(n) | winners, consolation | | generateRebuy(n) | winners, rebuy | | generateLastChance(n) | winners, lastChance, finals |

Return shape:

{
  sections: [
    {
      id: 'winners',           // Section identifier
      label: 'Winners Bracket', // Display label
      matches: [...],          // Match objects in this section
      connectors: [...],       // Connector lines within this section
      color: { bg, border, text, line }
    },
    // ...more sections
  ],
  crossConnectors: [...],     // Dashed lines between sections (loser drops)
  allMatches: [...],          // Flat array of every match
  allConnectors: [...]        // Flat array of every connector
}

Utility Functions

import {
  generateSeeding,
  generatePlayers,
  createMatch,
  layoutBracket,
  layoutLosersBracket,
  generateConnector,
  generateConnectors,
  generateCrossConnectors,
  getNumRounds,
} from 'bracket-forge';

| Function | Description | |----------|-------------| | generateSeeding(n) | Standard bracket seeding order. generateSeeding(8)[1,8,4,5,2,7,3,6] | | generatePlayers(n) | Array of ['Player 1', ..., 'Player N'] | | createMatch(opts) | Create a match: { id, round, section, matchIndex, player1?, player2? } | | layoutBracket(matches, x?, y?) | Position matches in single-elim layout. Returns total height. | | layoutLosersBracket(matches, x?, y?) | Position matches in losers bracket layout. Returns total height. | | generateConnector(from, to, slot, color) | Create one SVG path connector between two matches | | generateConnectors(matches, all, color) | Create all connectors for a section's matches | | generateCrossConnectors(all, color, dashed?) | Create cross-section connectors (loser drops) | | getNumRounds(n) | Number of rounds needed. getNumRounds(16)4 |

Constants

import {
  MATCH_WIDTH,    // 200  - Match box width (px)
  MATCH_HEIGHT,   // 60   - Match box height (px)
  HORIZONTAL_GAP, // 80   - Space between rounds
  VERTICAL_GAP,   // 20   - Space between matches
  ROUND_WIDTH,    // 280  - MATCH_WIDTH + HORIZONTAL_GAP
  SECTION_GAP,    // 60   - Space between bracket sections
  PLAYER_COUNTS,  // [4, 8, 16, 32, 64, 128, 256, 512, 1024]
  FORMATS,        // [{ id: 'single', label: 'Single Elimination' }, ...]
  COLORS,         // Color schemes per section type
} from 'bracket-forge';

COLORS keys: winners, losers, consolation, finals, lastChance, rebuy, lastLife

Each color object: { bg, border, text, line }

Data Model

Match Object

{
  id: 'W-R1-M1',             // Format: {Section}-R{round}-M{index}
  round: 1,                  // 1-indexed round number
  section: 'winners',        // Section this match belongs to
  matchIndex: 0,             // Index within the round
  players: ['Seed 1', 'Seed 8'], // Player labels (null = TBD)
  nextMatchId: 'W-R2-M1',   // Winner feeds to this match (null = final)
  nextSlot: 0,               // Slot in next match (0 = top, 1 = bottom)
  nextLoserMatchId: 'L-R1-M1', // Loser feeds here (null = eliminated)
  nextLoserSlot: 0,          // Slot in loser destination
  position: { x: 0, y: 0 }  // Computed SVG coordinates
}

Connector Object

{
  id: 'conn-W-R1-M1-W-R2-M1',
  path: 'M 200 30 H 240 V 60 H 280', // SVG path (right-angle lines)
  color: '#2ecc71',
  dashed: false              // true for cross-section connectors
}

Match Counts by Format

| Players | Single | Double | Triple | Consolation | Rebuy | Last Chance | |---------|--------|--------|--------|-------------|-------|-------------| | 4 | 3 | 7 | 9 | 4 | 6 | 6 | | 8 | 7 | 15 | 18 | 10 | 13 | 14 | | 16 | 15 | 31 | 36 | 22 | 27 | 30 | | 32 | 31 | 63 | 72 | 46 | 55 | 62 | | 64 | 63 | 127 | 144 | 94 | 111 | 126 | | 128 | 127 | 255 | 288 | 190 | 223 | 254 | | 256 | 255 | 511 | 576 | 382 | 447 | 510 | | 512 | 511 | 1023 | 1152 | 766 | 895 | 1022 | | 1024 | 1023 | 2047 | 2304 | 1534 | 1791 | 2046 |

Interaction

  • Scroll to zoom in/out
  • Click and drag to pan
  • +/- buttons for zoom controls
  • Reset button to restore default view

Development

# Demo app
npm run dev

# Build library
npm run build:lib

# Build demo
npm run build

# Lint
npm run lint

License

MIT