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!
- 🎨 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
 
- 🎲 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
 
- Installation
 - Quick Start
 - Core Features
 - Framework Integration
 - Complete API Reference
 - Advanced Examples
 - Styling & Customization
 - TypeScript Support
 - Migration Guide
 
npm install gracket
# or
yarn add gracket
# or
pnpm add gracketimport { 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:
- Hover over any team to highlight all their appearances
 - Smooth animations and transitions
 - Responsive canvas-drawn connector lines
 - ESPN-inspired modern styling
 
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 gamesAutomatically 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 3const 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 dataconst 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 the bracket with new tournament data and re-render.
bracket.update(newTournamentData);Remove the bracket and clean up event listeners.
bracket.destroy();Get current bracket settings.
const settings = bracket.getSettings();
console.log(settings.byeLabel);  // 'BYE'Get current tournament data (read-only copy).
const data = bracket.getData();
console.log(data.length);  // Number of roundsUpdate a team's score in a specific match.
// Round 0, Game 0, Team 0: 100 points
bracket.updateScore(0, 0, 0, 100);Get the winner of a specific match (null if incomplete).
const winner = bracket.getMatchWinner(0, 0);
if (winner) {
  console.log(`Winner: ${winner.name}`);
}Check if all matches in a round are complete.
if (bracket.isRoundComplete(0)) {
  console.log('Round 0 is complete!');
}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)
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
});Get teams advancing from a round (default: latest completed round).
const advancing = bracket.getAdvancingTeams(0);
console.log(advancing.map(t => t.name));Get detailed results for a round.
const results = bracket.getRoundResults(0);
results.forEach(r => {
  console.log(`${r.winner.name} defeated ${r.loser?.name || 'BYE'}`);
});Get a team's complete tournament history.
const history = bracket.getTeamHistory('warriors');
console.log(`${history.team.name}: ${history.wins}W-${history.losses}L`);Get tournament statistics.
const stats = bracket.getStatistics();
console.log(`Completion: ${stats.completionPercentage}%`);
console.log(`Average score: ${stats.averageScore}`);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' });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
Calculate how many byes are needed for a tournament.
import { calculateByesNeeded } from 'gracket';
const byesNeeded = calculateByesNeeded(6);  // Returns 2Gracket 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 = Matchconst 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- ✅ Chrome (latest)
 - ✅ Firefox (latest)
 - ✅ Safari (latest)
 - ✅ Edge (latest)
 - ✅ Modern mobile browsers
 
Requirements:
- ES2015+ support
 - Canvas API
 
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:
- ✅ All existing APIs unchanged
 - ✅ All existing tests passing
 - ✅ New features are opt-in
 - ✅ Zero breaking changes
 
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
 
- Handle any number of teams
 - Automatic bye generation
 - Visual bye placeholders
 - Customizable bye display
 
- Interactive score entry
 - Automatic round advancement
 - Multiple tie-breaking strategies
 - Event callbacks
 - Full tournament auto-generation
 
- 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! 🏆