gracket
v2.1.1
Published
A modern, framework-agnostic single elimination tournament bracket library
Maintainers
Readme
🏆 Gracket
The most powerful, modern tournament bracket library for JavaScript
Built with TypeScript • Zero Dependencies • Framework Agnostic • Fully Featured
Gracket is a comprehensive tournament bracket library that handles everything from simple 8-team brackets to complex tournaments with byes, automatic round generation, score tracking, and detailed reporting. Works seamlessly with React, Vue, Angular, or vanilla JavaScript.
🎮 Live Demo →
See Gracket in action with interactive examples and real-time features!
✨ Features
Core Features
- 🎨 Modern & Beautiful - ESPN-inspired design with smooth animations
- ⚡ Framework Agnostic - Works with React, Vue, Angular, or vanilla JS
- 📦 TypeScript First - Full TypeScript support with comprehensive type definitions
- 🎯 Zero Dependencies - No jQuery required - pure modern JavaScript
- 🚀 Tree Shakable - ES modules with optimal bundle size (~8.5 KB gzipped)
- 📱 Responsive - Works perfectly on all screen sizes
- ♿ Accessible - Built with accessibility in mind
NEW in v2.1: Advanced Tournament Management
- 🎲 Byes Support - Handle any number of teams, not just powers of 2
- 🔄 Auto-Generation - Automatically generate brackets based on match results
- 📊 Comprehensive Reporting - Track teams, generate reports, export data
- 📈 Real-Time Scoring - Interactive score entry with automatic advancement
- 🎯 Event System - React to score updates, round completions, and more
- 📉 Statistics - Calculate completion rates, averages, and tournament metrics
- 💾 Multiple Export Formats - JSON, Plain Text, HTML, and Markdown
📋 Table of Contents
- Installation
- Quick Start
- Core Features
- Framework Integration
- Complete API Reference
- Advanced Examples
- Styling & Customization
- TypeScript Support
- Migration Guide
🚀 Installation
npm install gracket
# or
yarn add gracket
# or
pnpm add gracket🎯 Quick Start
Simplest Example
import { Gracket } from 'gracket';
import 'gracket/style.css';
const bracket = new Gracket('#bracket', {
src: [
[
[{ name: 'Team A', seed: 1, score: 100 }, { name: 'Team B', seed: 2, score: 85 }]
],
[[{ name: 'Team A', seed: 1 }]]
]
});Feature-Rich Example
import { Gracket, generateTournamentWithByes } from 'gracket';
import 'gracket/style.css';
// Generate tournament for 6 teams (with automatic byes)
const teams = [
{ name: 'Warriors', id: 'warriors', seed: 1 },
{ name: 'Lakers', id: 'lakers', seed: 2 },
{ name: 'Celtics', id: 'celtics', seed: 3 },
{ name: 'Heat', id: 'heat', seed: 4 },
{ name: 'Bucks', id: 'bucks', seed: 5 },
{ name: 'Suns', id: 'suns', seed: 6 }
];
const tournamentData = generateTournamentWithByes(teams, 'top-seeds');
// Create interactive bracket
const bracket = new Gracket('#bracket', {
src: tournamentData,
byeLabel: 'BYE',
roundLabels: ['Round 1', 'Semifinals', 'Finals', 'Champion'],
cornerRadius: 15,
canvasLineColor: '#667eea',
// Real-time callbacks
onScoreUpdate: (round, game, team, score) => {
console.log(`Score entered: ${score}`);
},
onRoundComplete: (round) => {
const advancing = bracket.getAdvancingTeams(round);
console.log('Teams advancing:', advancing.map(t => t.name));
}
});
// Interactive scoring
bracket.updateScore(0, 0, 0, 105); // Round 0, Game 0, Team 0: 105
bracket.updateScore(0, 0, 1, 98); // Round 0, Game 0, Team 1: 98
// Auto-advance when round completes
if (bracket.isRoundComplete(0)) {
bracket.advanceRound(0, {
tieBreaker: 'higher-seed',
createRounds: true
});
}
// Generate comprehensive report
const report = bracket.generateReport({
format: 'text',
includeScores: true,
includeStatistics: true
});
console.log(report);
// Track specific team
const warriorsHistory = bracket.getTeamHistory('warriors');
console.log(`${warriorsHistory.team.name}: ${warriorsHistory.wins}W-${warriorsHistory.losses}L`);🎨 Core Features
Basic Tournament Bracket
The foundation - display beautiful tournament brackets with any structure.
import { Gracket } from 'gracket';
import 'gracket/style.css';
const tournamentData = [
// Round 1 - Quarterfinals
[
[
{ name: 'Team A', id: 'team-a', seed: 1, score: 100 },
{ name: 'Team B', id: 'team-b', seed: 8, score: 85 }
],
[
{ name: 'Team C', id: 'team-c', seed: 4, score: 90 },
{ name: 'Team D', id: 'team-d', seed: 5, score: 88 }
],
[
{ name: 'Team E', id: 'team-e', seed: 2, score: 105 },
{ name: 'Team F', id: 'team-f', seed: 7, score: 95 }
],
[
{ name: 'Team G', id: 'team-g', seed: 3, score: 92 },
{ name: 'Team H', id: 'team-h', seed: 6, score: 88 }
]
],
// Round 2 - Semifinals
[
[
{ name: 'Team A', id: 'team-a', seed: 1, score: 95 },
{ name: 'Team C', id: 'team-c', seed: 4, score: 92 }
],
[
{ name: 'Team E', id: 'team-e', seed: 2, score: 98 },
{ name: 'Team G', id: 'team-g', seed: 3, score: 96 }
]
],
// Round 3 - Finals
[
[
{ name: 'Team A', id: 'team-a', seed: 1, score: 102 },
{ name: 'Team E', id: 'team-e', seed: 2, score: 99 }
]
],
// Champion
[
[{ name: 'Team A', id: 'team-a', seed: 1 }]
]
];
const bracket = new Gracket('#bracket', {
src: tournamentData,
cornerRadius: 15,
canvasLineColor: '#667eea',
canvasLineWidth: 2,
roundLabels: ['Quarterfinals', 'Semifinals', 'Finals', 'Champion']
});
// Update bracket with new data
bracket.update(newTournamentData);
// Get current data
const currentData = bracket.getData();
// Clean up
bracket.destroy();Interactive Features:
- Hover over any team to highlight all their appearances
- Smooth animations and transitions
- Responsive canvas-drawn connector lines
- ESPN-inspired modern styling
Byes Support (NEW!)
Handle tournaments with any number of teams - not just powers of 2!
What are Byes?
In real-world tournaments, you often have participant counts that aren't perfect powers of 2 (like 5, 6, 7, 9, 10 teams). Byes are automatic advancements where top-seeded teams skip the first round.
Automatic Bye Generation
import { Gracket, generateTournamentWithByes } from 'gracket';
// Tournament with 6 teams (normally would need 8)
const teams = [
{ name: 'Warriors', id: 'warriors', seed: 1 },
{ name: 'Lakers', id: 'lakers', seed: 2 },
{ name: 'Celtics', id: 'celtics', seed: 3 },
{ name: 'Heat', id: 'heat', seed: 4 },
{ name: 'Bucks', id: 'bucks', seed: 5 },
{ name: 'Suns', id: 'suns', seed: 6 }
];
// Generate tournament structure with byes
// Top 2 seeds (Warriors, Lakers) will get byes
const tournamentData = generateTournamentWithByes(teams, 'top-seeds');
const bracket = new Gracket('#bracket', {
src: tournamentData,
byeLabel: 'BYE', // Label for bye placeholder
byeClass: 'g_bye', // CSS class for styling
showByeGames: true // Show/hide bye visualizations
});Result:
Round 1 Round 2
┌──────────────┐
│ Heat 105│────┐
│ Bucks 98│ │ ┌──────────────┐
└──────────────┘ ├──────┤ Heat 112│
│ │ Warriors 118│
┌──────────────┐ │ └──────────────┘
│ Suns 110│────┘
│ (6 seed) 102│
└──────────────┘
┌──────────────┐
│ Warriors │────┐ (BYE - automatically advances)
│ BYE │ │
└──────────────┘ │
┌──────────────┐ │
│ Lakers │────┘ (BYE - automatically advances)
│ BYE │
└──────────────┘Manual Bye Structure
You can also manually create byes by using single-team games:
const tournamentData = [
[
// Regular matchup
[
{ name: 'Heat', seed: 4, score: 105 },
{ name: 'Bucks', seed: 5, score: 98 }
],
// BYE - single team automatically advances
[{ name: 'Warriors', seed: 1 }],
[{ name: 'Lakers', seed: 2 }]
],
// Next round...
];Bye Customization
const bracket = new Gracket('#bracket', {
src: tournamentData,
byeLabel: 'AUTO WIN', // Custom label
byeClass: 'custom-bye', // Custom CSS class
showByeGames: false // Hide bye placeholders entirely
});/* Custom bye styling */
.custom-bye {
background: linear-gradient(90deg, #f8f9fa 0%, #e9ecef 100%);
border-left: 4px dashed #6c757d !important;
opacity: 0.5;
font-style: italic;
}Bye Seeding Strategies
// Strategy 1: Top seeds get byes (default)
generateTournamentWithByes(teams, 'top-seeds');
// Strategy 2: Random byes
generateTournamentWithByes(teams, 'random');
// Strategy 3: Custom (manual structure)
// Just create your own tournament structure with single-team gamesAuto-Generation (NEW!)
Automatically generate tournament brackets based on match results. Perfect for live tournaments!
Interactive Score Entry
const bracket = new Gracket('#bracket', {
src: initialData,
// Callback fired when score is entered
onScoreUpdate: (roundIndex, gameIndex, teamIndex, score) => {
console.log(`Score updated: Round ${roundIndex + 1}, Game ${gameIndex + 1}, Team ${teamIndex}, Score: ${score}`);
// Auto-advance when round completes
if (bracket.isRoundComplete(roundIndex)) {
bracket.advanceRound(roundIndex, {
tieBreaker: 'higher-seed',
createRounds: true
});
}
},
// Callback fired when round is complete
onRoundComplete: (roundIndex) => {
const advancing = bracket.getAdvancingTeams(roundIndex);
console.log(`Round ${roundIndex + 1} complete!`);
console.log('Advancing teams:', advancing.map(t => t.name).join(', '));
},
// Callback fired when new round is generated
onRoundGenerated: (roundIndex, roundData) => {
console.log(`Round ${roundIndex + 1} generated with ${roundData.length} games`);
}
});
// Update scores (e.g., from user input or live feed)
bracket.updateScore(0, 0, 0, 100); // Round 0, Game 0, Team 0: 100 points
bracket.updateScore(0, 0, 1, 85); // Round 0, Game 0, Team 1: 85 points
// Check if match has winner
const winner = bracket.getMatchWinner(0, 0);
if (winner) {
console.log(`Winner: ${winner.name}`);
}
// Check if entire round is complete
if (bracket.isRoundComplete(0)) {
console.log('Round 0 is complete! Ready to advance.');
}Manual Round Advancement
// Advance one round at a time
bracket.advanceRound(0, {
tieBreaker: 'higher-seed', // How to handle tied scores
tieBreakerFn: undefined, // Custom tie-breaker function
preserveScores: false, // Keep scores when advancing
createRounds: true // Create next round if missing
});Tie-Breaking Strategies
// Strategy 1: Throw error on ties (default)
bracket.advanceRound(0, { tieBreaker: 'error' });
// Strategy 2: Higher seed wins
bracket.advanceRound(0, { tieBreaker: 'higher-seed' });
// Strategy 3: Lower seed wins (upset preference)
bracket.advanceRound(0, { tieBreaker: 'lower-seed' });
// Strategy 4: Custom function
bracket.advanceRound(0, {
tieBreaker: 'callback',
tieBreakerFn: (team1, team2) => {
// Your custom logic
// Example: Use head-to-head record
return getHeadToHeadWinner(team1, team2);
// Example: Random
return Math.random() > 0.5 ? team1 : team2;
// Example: Prefer lower seed (upset)
return team1.seed > team2.seed ? team1 : team2;
}
});Full Auto-Generation
Generate the entire tournament from just the first round's results:
// Define ONLY first round with scores
const firstRoundData = [
[
[
{ name: 'Team A', seed: 1, score: 100 },
{ name: 'Team B', seed: 8, score: 85 }
],
[
{ name: 'Team C', seed: 4, score: 90 },
{ name: 'Team D', seed: 5, score: 88 }
],
[
{ name: 'Team E', seed: 2, score: 105 },
{ name: 'Team F', seed: 7, score: 95 }
],
[
{ name: 'Team G', seed: 3, score: 92 },
{ name: 'Team H', seed: 6, score: 88 }
]
]
];
const bracket = new Gracket('#bracket', { src: firstRoundData });
// Auto-generate ALL subsequent rounds
bracket.autoGenerateTournament({
tieBreaker: 'higher-seed',
onRoundGenerated: (roundIndex, roundData) => {
console.log(`Round ${roundIndex + 1}:`, roundData);
},
stopAtRound: 2 // Optional: stop at specific round
});
// Result: Complete tournament structure from quarters to champion!Real-World Example: Live Tournament
// Tournament management system
class LiveTournament {
bracket: Gracket;
constructor(teams: Team[]) {
const data = generateTournamentWithByes(teams, 'top-seeds');
this.bracket = new Gracket('#bracket', {
src: data,
roundLabels: ['Round of 16', 'Quarterfinals', 'Semifinals', 'Finals', 'Champion'],
onScoreUpdate: (r, g, t, score) => {
// Save to database
this.saveScore(r, g, t, score);
// Broadcast to spectators
this.broadcastUpdate({ round: r, game: g, team: t, score });
},
onRoundComplete: (r) => {
// Notify all participants
const advancing = this.bracket.getAdvancingTeams(r);
this.notifyAdvancingTeams(advancing);
// Generate next round
this.bracket.advanceRound(r, {
tieBreaker: 'higher-seed',
createRounds: true
});
}
});
}
// Admin enters score from match
recordMatchScore(round: number, game: number, team: number, score: number) {
this.bracket.updateScore(round, game, team, score);
}
// Get current tournament state
getStatus() {
return {
data: this.bracket.getData(),
stats: this.bracket.getStatistics(),
report: this.bracket.generateReport({ format: 'json' })
};
}
}Reporting & Statistics (NEW!)
Comprehensive tournament reporting, team tracking, and statistics.
Get Advancing Teams
// Get teams advancing from specific round
const advancingFromRound1 = bracket.getAdvancingTeams(0);
console.log('Teams advancing to Round 2:');
advancingFromRound1.forEach(team => {
console.log(` - ${team.name} (Seed ${team.seed})`);
});
// Get advancing from latest completed round
const latestAdvancing = bracket.getAdvancingTeams(); // No argument = latestGet Round Results
// Get detailed results for a round
const roundResults = bracket.getRoundResults(0);
roundResults.forEach((result, idx) => {
if (result.isBye) {
console.log(`Match ${idx + 1}: ${result.winner.name} (BYE)`);
} else {
console.log(
`Match ${idx + 1}: ${result.winner.name} (${result.winnerScore}) ` +
`defeated ${result.loser?.name} (${result.loserScore})`
);
}
});
// Output:
// Match 1: Team A (100) defeated Team B (85)
// Match 2: Team C (90) defeated Team D (88)
// Match 3: Team E (BYE)Track Team History
Follow a specific team through the entire tournament:
const teamHistory = bracket.getTeamHistory('warriors');
console.log(`=== ${teamHistory.team.name} Tournament History ===`);
console.log(`Final Record: ${teamHistory.wins}W - ${teamHistory.losses}L`);
console.log(`Final Placement: ${teamHistory.finalPlacement || 'In Progress'}`);
console.log('\nMatch-by-Match:');
teamHistory.matches.forEach((match, index) => {
const result = match.won ? '✓ WIN' : '✗ LOSS';
const opponent = match.isBye ? 'BYE' : match.opponent?.name;
const scoreDetail = match.score && match.opponentScore
? ` (${match.score}-${match.opponentScore})`
: '';
console.log(
` ${index + 1}. ${match.roundLabel}: ${result} vs ${opponent}${scoreDetail}`
);
});
// Output:
// === Warriors Tournament History ===
// Final Record: 4W - 0L
// Final Placement: 1
//
// Match-by-Match:
// 1. Round 1: ✓ WIN vs BYE
// 2. Quarterfinals: ✓ WIN vs Thunder (112-98)
// 3. Semifinals: ✓ WIN vs Lakers (118-105)
// 4. Finals: ✓ WIN vs Celtics (120-115)Get Tournament Statistics
const stats = bracket.getStatistics();
console.log('Tournament Statistics:');
console.log(` Participants: ${stats.participantCount}`);
console.log(` Total Rounds: ${stats.totalRounds}`);
console.log(` Byes: ${stats.byeCount}`);
console.log(` Average Score: ${stats.averageScore?.toFixed(1) || 'N/A'}`);
console.log(` Completion: ${stats.completionPercentage}%`);
if (stats.highestScore) {
console.log(
` Highest Score: ${stats.highestScore.team.name} ` +
`scored ${stats.highestScore.score} in round ${stats.highestScore.round + 1}`
);
}
// Output:
// Tournament Statistics:
// Participants: 8
// Total Rounds: 4
// Byes: 2
// Average Score: 98.5
// Completion: 100%
// Highest Score: Team A scored 120 in round 3Generate Reports (Multiple Formats)
Plain Text Report
const textReport = bracket.generateReport({
format: 'text',
includeScores: true,
includeStatistics: true
});
console.log(textReport);
// Output:
// ==================================================
// TOURNAMENT REPORT
// ==================================================
//
// Tournament Statistics:
// - Total Participants: 8
// - Total Rounds: 4
// - Total Matches: 7
// - Completed: 7/7 (100%)
// - Byes: 2
// - Average Score: 98.5
//
// QUARTERFINALS
// ✓ Match 1: Team A (100) defeated Team B (85)
// ✓ Match 2: Team C (90) defeated Team D (88)
// ✓ Match 3: Team E (BYE)
// ✓ Match 4: Team F (BYE)
//
// Advancing: Team A, Team C, Team E, Team F
//
// SEMIFINALS
// ✓ Match 1: Team A (95) defeated Team E (88)
// ✓ Match 2: Team C (92) defeated Team F (90)
//
// Advancing: Team A, Team C
//
// FINALS
// ✓ Match 1: Team A (102) defeated Team C (99)
//
// Advancing: Team A
//
// CHAMPION: Team A (Seed 1)
// ==================================================JSON Report (For APIs)
const jsonReport = bracket.generateReport({ format: 'json' });
// Use in API calls
fetch('/api/tournaments/123/results', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(jsonReport)
});
// Or save to file
const blob = new Blob([JSON.stringify(jsonReport, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'tournament-results.json';
a.click();HTML Report (For Web Display)
const htmlReport = bracket.generateReport({
format: 'html',
includeScores: true,
includeStatistics: true
});
// Display in your app
document.getElementById('tournament-results').innerHTML = htmlReport;
// Result: Beautiful HTML table with all tournament dataMarkdown Report (For Documentation)
const mdReport = bracket.generateReport({
format: 'markdown',
includeScores: true
});
// Save for documentation
const blob = new Blob([mdReport], { type: 'text/markdown' });
// ... download logic
// Result: Markdown tables perfect for GitHub, docs, etc.🖼️ Framework Integration
React
import { GracketReact } from 'gracket/react';
import { generateTournamentWithByes } from 'gracket';
import 'gracket/style.css';
import { useState } from 'react';
function TournamentBracket() {
const [data, setData] = useState(() =>
generateTournamentWithByes(teams, 'top-seeds')
);
const [gracket, setGracket] = useState<Gracket | null>(null);
const handleScoreUpdate = (r: number, g: number, t: number, score: number) => {
console.log(`Score: ${score}`);
// Auto-advance when round completes
if (gracket?.isRoundComplete(r)) {
gracket.advanceRound(r, {
tieBreaker: 'higher-seed',
createRounds: true
});
setData([...gracket.getData()]);
}
};
return (
<div>
<GracketReact
data={data}
byeLabel="BYE"
cornerRadius={15}
canvasLineColor="#667eea"
roundLabels={['Round 1', 'Semifinals', 'Finals', 'Champion']}
onInit={(g) => setGracket(g)}
onScoreUpdate={handleScoreUpdate}
onRoundComplete={(r) => {
const advancing = gracket?.getAdvancingTeams(r);
console.log('Advancing:', advancing);
}}
/>
{gracket && (
<div className="tournament-controls">
<button onClick={() => {
const report = gracket.generateReport({
format: 'text',
includeStatistics: true
});
alert(report);
}}>
Generate Report
</button>
<button onClick={() => {
const stats = gracket.getStatistics();
console.log('Stats:', stats);
}}>
Show Statistics
</button>
</div>
)}
</div>
);
}Vue 3
<script setup lang="ts">
import { ref, computed } from 'vue';
import { GracketVue } from 'gracket/vue';
import { generateTournamentWithByes, type Gracket } from 'gracket';
import 'gracket/style.css';
const teams = ref([/* your teams */]);
const data = ref(generateTournamentWithByes(teams.value, 'top-seeds'));
const gracket = ref<Gracket | null>(null);
const options = ref({
byeLabel: 'BYE',
cornerRadius: 15,
canvasLineColor: '#667eea',
roundLabels: ['Round 1', 'Semifinals', 'Finals', 'Champion']
});
const handleInit = (g: Gracket) => {
gracket.value = g;
};
const handleScoreUpdate = (r: number, g: number, t: number, score: number) => {
if (gracket.value?.isRoundComplete(r)) {
gracket.value.advanceRound(r, {
tieBreaker: 'higher-seed',
createRounds: true
});
data.value = [...gracket.value.getData()];
}
};
const generateReport = () => {
if (!gracket.value) return;
const report = gracket.value.generateReport({
format: 'text',
includeStatistics: true
});
alert(report);
};
const showStats = () => {
if (!gracket.value) return;
const stats = gracket.value.getStatistics();
console.log('Statistics:', stats);
};
const advancingTeams = computed(() => {
if (!gracket.value) return [];
return gracket.value.getAdvancingTeams();
});
</script>
<template>
<div>
<GracketVue
:data="data"
:options="options"
@init="handleInit"
@score-update="handleScoreUpdate"
@round-complete="(r) => console.log('Round complete:', r)"
/>
<div class="controls">
<button @click="generateReport">Generate Report</button>
<button @click="showStats">Show Statistics</button>
</div>
<div v-if="advancingTeams.length" class="advancing">
<h3>Advancing Teams:</h3>
<ul>
<li v-for="team in advancingTeams" :key="team.id">
{{ team.name }} (Seed {{ team.seed }})
</li>
</ul>
</div>
</div>
</template>Vanilla JavaScript
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/gracket/dist/style.css" />
</head>
<body>
<div id="bracket"></div>
<div class="controls">
<button id="generate-report">Generate Report</button>
<button id="show-stats">Show Statistics</button>
<button id="show-advancing">Show Advancing Teams</button>
</div>
<script type="module">
import { Gracket, generateTournamentWithByes } from 'https://unpkg.com/gracket';
const teams = [
{ name: 'Warriors', id: 'warriors', seed: 1 },
{ name: 'Lakers', id: 'lakers', seed: 2 },
{ name: 'Celtics', id: 'celtics', seed: 3 },
{ name: 'Heat', id: 'heat', seed: 4 },
{ name: 'Bucks', id: 'bucks', seed: 5 },
{ name: 'Suns', id: 'suns', seed: 6 }
];
const data = generateTournamentWithByes(teams, 'top-seeds');
const bracket = new Gracket('#bracket', {
src: data,
byeLabel: 'BYE',
roundLabels: ['Round 1', 'Semifinals', 'Finals', 'Champion'],
onScoreUpdate: (r, g, t, score) => {
if (bracket.isRoundComplete(r)) {
bracket.advanceRound(r, { createRounds: true });
}
},
onRoundComplete: (r) => {
console.log('Round complete:', r);
}
});
// Event listeners
document.getElementById('generate-report').addEventListener('click', () => {
const report = bracket.generateReport({
format: 'text',
includeStatistics: true
});
alert(report);
});
document.getElementById('show-stats').addEventListener('click', () => {
const stats = bracket.getStatistics();
console.log('Statistics:', stats);
});
document.getElementById('show-advancing').addEventListener('click', () => {
const advancing = bracket.getAdvancingTeams();
console.log('Advancing teams:', advancing.map(t => t.name));
});
</script>
</body>
</html>📖 Complete API Reference
Constructor
new Gracket(container: HTMLElement | string, options?: GracketOptions)Core Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| src | TournamentData | [] | Tournament bracket data |
| gracketClass | string | 'g_gracket' | CSS class for main container |
| gameClass | string | 'g_game' | CSS class for game containers |
| roundClass | string | 'g_round' | CSS class for round containers |
| teamClass | string | 'g_team' | CSS class for team containers |
| winnerClass | string | 'g_winner' | CSS class for winner container |
| currentClass | string | 'g_current' | CSS class for hover state |
| cornerRadius | number | 15 | Corner radius for bracket lines (px) |
| canvasLineColor | string | '#eee' | Color of bracket lines |
| canvasLineWidth | number | 2 | Width of bracket lines (px) |
| canvasLineGap | number | 15 | Gap between elements and lines (px) |
| canvasLineCap | 'round' \| 'square' \| 'butt' | 'round' | Line cap style |
| roundLabels | string[] | [] | Custom labels for each round |
NEW: Byes Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| byeLabel | string | 'BYE' | Label for bye placeholders |
| byeClass | string | 'g_bye' | CSS class for bye elements |
| showByeGames | boolean | true | Show/hide bye visualizations |
NEW: Event Callbacks
| Callback | Parameters | Description |
|----------|------------|-------------|
| onScoreUpdate | (roundIndex, gameIndex, teamIndex, score) | Fired when score is updated |
| onRoundComplete | (roundIndex) | Fired when round completes |
| onRoundGenerated | (roundIndex, roundData) | Fired when new round is created |
Core Methods
update(data: TournamentData): void
Update the bracket with new tournament data and re-render.
bracket.update(newTournamentData);destroy(): void
Remove the bracket and clean up event listeners.
bracket.destroy();getSettings(): GracketSettings
Get current bracket settings.
const settings = bracket.getSettings();
console.log(settings.byeLabel); // 'BYE'getData(): TournamentData
Get current tournament data (read-only copy).
const data = bracket.getData();
console.log(data.length); // Number of roundsNEW: Score Management Methods
updateScore(roundIndex: number, gameIndex: number, teamIndex: number, score: number): void
Update a team's score in a specific match.
// Round 0, Game 0, Team 0: 100 points
bracket.updateScore(0, 0, 0, 100);getMatchWinner(roundIndex: number, gameIndex: number): Team | null
Get the winner of a specific match (null if incomplete).
const winner = bracket.getMatchWinner(0, 0);
if (winner) {
console.log(`Winner: ${winner.name}`);
}isRoundComplete(roundIndex: number): boolean
Check if all matches in a round are complete.
if (bracket.isRoundComplete(0)) {
console.log('Round 0 is complete!');
}NEW: Round Advancement Methods
advanceRound(fromRound?: number, options?: AdvanceOptions): TournamentData
Advance winners to the next round.
bracket.advanceRound(0, {
tieBreaker: 'higher-seed', // How to handle ties
tieBreakerFn: undefined, // Custom tie-breaker
preserveScores: false, // Keep scores
createRounds: true // Create next round if missing
});AdvanceOptions:
tieBreaker:'error' | 'higher-seed' | 'lower-seed' | 'callback'(default:'error')tieBreakerFn:(team1: Team, team2: Team) => TeampreserveScores:boolean(default:false)createRounds:boolean(default:false)
autoGenerateTournament(options?: AutoGenerateOptions): void
Automatically generate entire tournament from results.
bracket.autoGenerateTournament({
tieBreaker: 'higher-seed',
onRoundGenerated: (idx, data) => {
console.log(`Round ${idx + 1} generated`);
},
stopAtRound: 3 // Optional: stop at specific round
});NEW: Reporting Methods
getAdvancingTeams(roundIndex?: number): Team[]
Get teams advancing from a round (default: latest completed round).
const advancing = bracket.getAdvancingTeams(0);
console.log(advancing.map(t => t.name));getRoundResults(roundIndex: number): MatchResult[]
Get detailed results for a round.
const results = bracket.getRoundResults(0);
results.forEach(r => {
console.log(`${r.winner.name} defeated ${r.loser?.name || 'BYE'}`);
});getTeamHistory(teamId: string): TeamHistory | null
Get a team's complete tournament history.
const history = bracket.getTeamHistory('warriors');
console.log(`${history.team.name}: ${history.wins}W-${history.losses}L`);getStatistics(): TournamentStatistics
Get tournament statistics.
const stats = bracket.getStatistics();
console.log(`Completion: ${stats.completionPercentage}%`);
console.log(`Average score: ${stats.averageScore}`);generateReport(options?: ReportOptions): TournamentReport | string
Generate formatted tournament report.
// JSON format
const jsonReport = bracket.generateReport({ format: 'json' });
// Plain text
const textReport = bracket.generateReport({
format: 'text',
includeScores: true,
includeStatistics: true
});
// HTML
const htmlReport = bracket.generateReport({ format: 'html' });
// Markdown
const mdReport = bracket.generateReport({ format: 'markdown' });Utility Functions
generateTournamentWithByes(teams: Team[], strategy?: ByeSeedingStrategy): TournamentData
Generate tournament structure with byes for non-power-of-2 team counts.
const teams = [/* 6 teams */];
const data = generateTournamentWithByes(teams, 'top-seeds');Strategies:
'top-seeds'- Top-seeded teams get byes (default)'random'- Random teams get byes
calculateByesNeeded(teamCount: number): number
Calculate how many byes are needed for a tournament.
import { calculateByesNeeded } from 'gracket';
const byesNeeded = calculateByesNeeded(6); // Returns 2🎨 Styling & Customization
Gracket includes beautiful default styles, but everything is customizable.
Basic Customization
/* Customize team colors */
.g_team {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
}
.g_team:hover {
transform: translateX(5px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
}
/* Customize winner display */
.g_winner {
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
border: 3px solid #ffd700;
}
.g_winner .g_team {
background: rgba(255, 215, 0, 0.2);
border-left: 6px solid #ffd700;
}
/* Customize round labels */
.g_round_label {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 14px;
padding: 10px 20px;
}
/* Customize bye placeholders */
.g_bye {
background: linear-gradient(90deg, #f8f9fa 0%, #e9ecef 100%);
border-left: 4px dashed #6c757d !important;
opacity: 0.6;
font-style: italic;
}
/* Customize seed badges */
.g_seed {
background: #667eea;
color: white;
font-weight: bold;
border-radius: 4px;
padding: 4px 8px;
}
/* Customize scores */
.g_score {
font-size: 24px;
font-weight: 900;
color: #667eea;
text-shadow: 0 0 10px rgba(102, 126, 234, 0.5);
}Dark Theme Example
.g_gracket {
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
}
.g_team {
background: linear-gradient(90deg, #0f3460 0%, #16213e 100%);
border-left-color: #e94560;
color: #ffffff;
}
.g_team:hover {
background: linear-gradient(90deg, #16213e 0%, #1a1a2e 100%);
border-left-color: #00d4ff;
}
.g_bye {
background: linear-gradient(90deg, rgba(15, 52, 96, 0.3) 0%, rgba(22, 33, 62, 0.3) 100%);
border-left-color: #5a7a94 !important;
}Light Theme Example
.g_gracket {
background: linear-gradient(180deg, #ffffff 0%, #f8f9fa 100%);
}
.g_team {
background: linear-gradient(90deg, #ffffff 0%, #f8f9fa 100%);
border-left-color: #667eea;
color: #333333;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.g_team:hover {
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
transform: translateX(3px);
}
.g_bye {
background: linear-gradient(90deg, #f8f9fa 0%, #e9ecef 100%);
border-left-style: dashed;
border-left-color: #adb5bd !important;
}💻 TypeScript Support
Gracket is built with TypeScript and includes comprehensive type definitions.
Core Types
// Team/Player
interface Team {
name: string; // Team/player name
id?: string; // Unique identifier
seed: number; // Tournament seed
displaySeed?: string | number; // Alternative seed display
score?: number; // Match score
}
// Game structure
type Game = Team[]; // 1 team (bye) or 2 teams (match)
type Round = Game[]; // Array of games
type TournamentData = Round[]; // Complete tournament
// Match result
interface MatchResult {
winner: Team;
loser: Team | null; // null for byes
winnerScore?: number;
loserScore?: number;
isBye: boolean;
}
// Team history
interface TeamHistory {
team: Team;
matches: MatchEntry[];
finalPlacement?: number; // 1st, 2nd, 3rd, etc.
wins: number;
losses: number;
}
// Tournament statistics
interface TournamentStatistics {
participantCount: number;
totalRounds: number;
byeCount: number;
averageScore?: number;
highestScore?: {
team: Team;
score: number;
round: number;
};
completionPercentage: number;
}
// And many more...Type-Safe Usage
import type {
Gracket,
Team,
TournamentData,
GracketOptions,
MatchResult,
TeamHistory,
TournamentStatistics,
ReportOptions
} from 'gracket';
// Type-safe team data
const teams: Team[] = [
{ name: 'Warriors', id: 'warriors', seed: 1 },
{ name: 'Lakers', id: 'lakers', seed: 2 }
];
// Type-safe options
const options: GracketOptions = {
src: tournamentData,
byeLabel: 'BYE',
cornerRadius: 15,
onScoreUpdate: (r, g, t, score) => {
console.log(`Score: ${score}`);
}
};
// Type-safe bracket
const bracket: Gracket = new Gracket('#bracket', options);
// Type-safe results
const results: MatchResult[] = bracket.getRoundResults(0);
const history: TeamHistory | null = bracket.getTeamHistory('warriors');
const stats: TournamentStatistics = bracket.getStatistics();
// Type-safe report options
const reportOptions: ReportOptions = {
format: 'json',
includeScores: true,
includeStatistics: true
};🎓 Advanced Examples
Example 1: March Madness Style Bracket
import { Gracket, generateTournamentWithByes } from 'gracket';
// 64 teams (power of 2, no byes needed)
const teams = Array.from({ length: 64 }, (_, i) => ({
name: `Team ${i + 1}`,
id: `team-${i + 1}`,
seed: i + 1
}));
const data = generateTournamentWithByes(teams, 'top-seeds');
const bracket = new Gracket('#bracket', {
src: data,
roundLabels: [
'Round of 64',
'Round of 32',
'Sweet 16',
'Elite 8',
'Final Four',
'Championship',
'Winner'
],
cornerRadius: 10,
canvasLineColor: '#003366',
canvasLineWidth: 3
});Example 2: Real-Time Score Updates
// Connect to WebSocket for live updates
const socket = new WebSocket('wss://tournament-server.com');
const bracket = new Gracket('#bracket', {
src: tournamentData,
onScoreUpdate: (r, g, t, score) => {
// Broadcast score to all spectators
socket.send(JSON.stringify({
type: 'score_update',
round: r,
game: g,
team: t,
score
}));
}
});
// Receive live updates
socket.onmessage = (event) => {
const update = JSON.parse(event.data);
if (update.type === 'score_update') {
bracket.updateScore(
update.round,
update.game,
update.team,
update.score
);
}
};Example 3: Tournament Dashboard
class TournamentDashboard {
private bracket: Gracket;
constructor(container: string, teams: Team[]) {
const data = generateTournamentWithByes(teams, 'top-seeds');
this.bracket = new Gracket(container, {
src: data,
onRoundComplete: (r) => this.updateDashboard(r)
});
this.renderDashboard();
}
renderDashboard() {
const stats = this.bracket.getStatistics();
document.getElementById('participants').textContent =
stats.participantCount.toString();
document.getElementById('completion').textContent =
`${stats.completionPercentage}%`;
document.getElementById('avg-score').textContent =
stats.averageScore?.toFixed(1) || 'N/A';
}
updateDashboard(round: number) {
const advancing = this.bracket.getAdvancingTeams(round);
// Update advancing teams list
const list = document.getElementById('advancing-teams');
list.innerHTML = advancing
.map(t => `<li>${t.name} (Seed ${t.seed})</li>`)
.join('');
// Update stats
this.renderDashboard();
}
exportResults(format: 'json' | 'text' | 'html' | 'markdown') {
const report = this.bracket.generateReport({
format,
includeScores: true,
includeStatistics: true
});
// Download report
const blob = new Blob([
typeof report === 'string' ? report : JSON.stringify(report, null, 2)
], {
type: format === 'json' ? 'application/json' : 'text/plain'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `tournament-results.${format === 'json' ? 'json' : 'txt'}`;
a.click();
}
}📚 Data Structure Deep Dive
Tournament Data Structure
// Tournament is an array of rounds
type TournamentData = Round[];
// Each round is an array of games
type Round = Game[];
// Each game is an array of teams (1 or 2)
type Game = Team[];
// Single team = BYE, Two teams = MatchExample: 6-Team Tournament with Byes
const tournamentData: TournamentData = [
// Round 1 - 2 matches + 2 byes
[
// Regular match
[
{ name: 'Heat', seed: 4, score: 105 },
{ name: 'Bucks', seed: 5, score: 98 }
],
// Regular match
[
{ name: 'Suns', seed: 3, score: 110 },
{ name: 'Nuggets', seed: 6, score: 102 }
],
// BYE - single team
[{ name: 'Warriors', seed: 1 }],
// BYE - single team
[{ name: 'Lakers', seed: 2 }]
],
// Round 2 - 2 matches (all 4 teams play)
[
[
{ name: 'Heat', seed: 4, score: 112 },
{ name: 'Warriors', seed: 1, score: 118 }
],
[
{ name: 'Suns', seed: 3, score: 108 },
{ name: 'Lakers', seed: 2, score: 115 }
]
],
// Round 3 - Finals
[
[
{ name: 'Warriors', seed: 1, score: 120 },
{ name: 'Lakers', seed: 2, score: 115 }
]
],
// Champion
[
[{ name: 'Warriors', seed: 1 }]
]
];🧪 Testing
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with UI
npm run test:ui
# Run tests with coverage
npm run test:coverage🏗️ Development
# Install dependencies
npm install
# Start dev server with demo
npm run dev
# Build library
npm run build
# Run linter
npm run lint
# Format code
npm run format
# Type checking
npm run type-check🌐 Browser Support
- ✅ Chrome (latest)
- ✅ Firefox (latest)
- ✅ Safari (latest)
- ✅ Edge (latest)
- ✅ Modern mobile browsers
Requirements:
- ES2015+ support
- Canvas API
📄 License
MIT © Erik Zettersten
🙏 Acknowledgments
This is a modernized version of the original jquery.gracket.js plugin. Special thanks to:
📚 Additional Resources
🆚 Migration from v2.0
Good news! All new features in v2.1 are 100% backward compatible. Your existing code will continue to work without any changes.
Before (v2.0)
const bracket = new Gracket('#bracket', {
src: tournamentData,
cornerRadius: 15
});After (v2.1)
// Same code works identically
const bracket = new Gracket('#bracket', {
src: tournamentData,
cornerRadius: 15
});
// NEW: Optional features available
bracket.updateScore(0, 0, 0, 100);
const advancing = bracket.getAdvancingTeams(0);
const report = bracket.generateReport({ format: 'json' });Key Changes:
- ✅ All existing APIs unchanged
- ✅ All existing tests passing
- ✅ New features are opt-in
- ✅ Zero breaking changes
🤝 Contributing
Contributions are welcome! Please read our Contributing Guide for details.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
🚀 What's New in v2.1
Byes Support (Issue #15)
- Handle any number of teams
- Automatic bye generation
- Visual bye placeholders
- Customizable bye display
Auto-Generation (Issue #14a)
- Interactive score entry
- Automatic round advancement
- Multiple tie-breaking strategies
- Event callbacks
- Full tournament auto-generation
Reporting & Statistics (Issue #14b)
- Query advancing teams
- Track team history
- Generate reports (JSON, text, HTML, markdown)
- Calculate tournament statistics
- Export functionality
Made with ❤️ by Erik Zettersten
Ready to build amazing tournament brackets? Get started now! 🏆
