
Security News
Axios Maintainer Confirms Social Engineering Attack Behind npm Compromise
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.
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.
See Gracket in action with interactive examples and real-time features!
npm install gracket
# or
yarn add gracket
# or
pnpm add gracket
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 }]]
]
});
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`);
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:
Handle tournaments with any number of teams - not just powers of 2!
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.
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 โ
โโโโโโโโโโโโโโโโ
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...
];
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;
}
// 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 games
Automatically generate tournament brackets based on match results. Perfect for live tournaments!
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.');
}
// 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
});
// 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;
}
});
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!
// 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' })
};
}
}
Comprehensive tournament reporting, team tracking, and statistics.
// 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 = latest
// 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)
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)
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 3
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)
// ==================================================
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();
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 data
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.
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>
);
}
<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>
<!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>
new Gracket(container: HTMLElement | string, options?: GracketOptions)
| 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 |
| 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 |
| 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 |
update(data: TournamentData): voidUpdate the bracket with new tournament data and re-render.
bracket.update(newTournamentData);
destroy(): voidRemove the bracket and clean up event listeners.
bracket.destroy();
getSettings(): GracketSettingsGet current bracket settings.
const settings = bracket.getSettings();
console.log(settings.byeLabel); // 'BYE'
getData(): TournamentDataGet current tournament data (read-only copy).
const data = bracket.getData();
console.log(data.length); // Number of rounds
updateScore(roundIndex: number, gameIndex: number, teamIndex: number, score: number): voidUpdate 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 | nullGet 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): booleanCheck if all matches in a round are complete.
if (bracket.isRoundComplete(0)) {
console.log('Round 0 is complete!');
}
advanceRound(fromRound?: number, options?: AdvanceOptions): TournamentDataAdvance 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): voidAutomatically 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
});
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 | nullGet a team's complete tournament history.
const history = bracket.getTeamHistory('warriors');
console.log(`${history.team.name}: ${history.wins}W-${history.losses}L`);
getStatistics(): TournamentStatisticsGet tournament statistics.
const stats = bracket.getStatistics();
console.log(`Completion: ${stats.completionPercentage}%`);
console.log(`Average score: ${stats.averageScore}`);
generateReport(options?: ReportOptions): TournamentReport | stringGenerate 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' });
generateTournamentWithByes(teams: Team[], strategy?: ByeSeedingStrategy): TournamentDataGenerate 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 byescalculateByesNeeded(teamCount: number): numberCalculate how many byes are needed for a tournament.
import { calculateByesNeeded } from 'gracket';
const byesNeeded = calculateByesNeeded(6); // Returns 2
Gracket includes beautiful default styles, but everything is customizable.
/* 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);
}
.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;
}
.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;
}
Gracket is built with TypeScript and includes comprehensive type definitions.
// 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...
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
};
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
});
// 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
);
}
};
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();
}
}
// 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 = Match
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 }]
]
];
# 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
# 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
Requirements:
MIT ยฉ Erik Zettersten
This is a modernized version of the original jquery.gracket.js plugin. Special thanks to:
Good news! All new features in v2.1 are 100% backward compatible. Your existing code will continue to work without any changes.
const bracket = new Gracket('#bracket', {
src: tournamentData,
cornerRadius: 15
});
// 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:
Contributions are welcome! Please read our Contributing Guide for details.
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)Made with โค๏ธ by Erik Zettersten
Ready to build amazing tournament brackets? Get started now! ๐
FAQs
A modern, framework-agnostic single elimination tournament bracket library
We found that gracket demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.ย It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.