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.
Maintainers
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-forgePeer 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); // 191API 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 lintLicense
MIT
