react-bracket-ui
v1.2.0
Published
A modern, feature-rich React component library for displaying single-elimination tournament brackets with drag-drop, zoom/pan, and error validation
Maintainers
Readme
react-bracket-ui
✨ Features
- ✅ Pure Display Component - Only displays bracket data passed from FE
- ✅ React 19 Ready - Built with TypeScript and latest React features
- ✅ Zoom & Pan - Interactive zoom/pan for better bracket navigation
- ✅ Drag & Drop - Rearrange teams between matches with intuitive drag-and-drop
- ✅ Error Validation - Visual error indicators (red borders) for invalid matches
- ✅ Callbacks - Emit events when users modify bracket positions
- ✅ Customizable - Extensive styling and color customization options
- ✅ Optimized - Performance-optimized with React.memo and useMemo
- ✅ TypeScript - Full type safety with comprehensive TypeScript definitions
- ✅ Zero Config - Works out-of-the-box with sensible defaults
📦 Installation
npm install react-bracket-ui
# or
yarn add react-bracket-ui
# or
pnpm add react-bracket-ui🚀 Quick Start
import React from 'react';
import { Bracket } from 'react-bracket-ui';
import type { Match } from 'react-bracket-ui';
const matches: Match[] = [
{
id: 1,
round: 1,
participant1: { id: 'p1', name: 'Team A', score: 2 },
participant2: { id: 'p2', name: 'Team B', score: 1 },
winner: 'p1',
nextMatchId: 3
},
{
id: 2,
round: 1,
participant1: { id: 'p3', name: 'Team C', score: 3 },
participant2: { id: 'p4', name: 'Team D', score: 0 },
winner: 'p3',
nextMatchId: 3
},
{
id: 3,
round: 2,
participant1: { id: 'p1', name: 'Team A' },
participant2: { id: 'p3', name: 'Team C' }
}
];
function App() {
return (
<Bracket
matches={matches}
showRoundNames={true}
/>
);
}📖 API Reference
BracketProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| matches | Match[] | required | Array of match objects |
| className | string | undefined | CSS class name |
| style | React.CSSProperties | undefined | Inline styles |
| enableDragDrop | boolean | false | Enable drag & drop functionality |
| onBracketChange | (event: BracketChangeEvent) => void | undefined | Callback when bracket changes |
| enableZoomPan | boolean | false | Enable zoom/pan controls |
| minZoom | number | 0.5 | Minimum zoom level |
| maxZoom | number | 3 | Maximum zoom level |
| initialZoom | number | 1 | Initial zoom level |
| errors | BracketError[] | undefined | Custom validation errors |
| onErrorClick | (error: BracketError) => void | undefined | Callback when error is clicked |
| showRoundNames | boolean | true | Show round names (Final, Semi-Final, etc.) |
| roundNames | Record<number, string> | undefined | Custom round names |
| matchWidth | number | 220 | Width of match box (px) |
| matchHeight | number | 100 | Height of match box (px) |
| gap | number | 20 | Gap between rounds (px) |
| colors | ColorConfig | default theme | Custom color scheme |
Match Interface
interface Match {
id: string | number;
participant1?: Participant | null;
participant2?: Participant | null;
winner?: string | number | null;
round: number;
matchNumber?: number;
nextMatchId?: string | number | null;
hasError?: boolean;
errorMessage?: string;
}Participant Interface
interface Participant {
id: string | number;
name: string;
score?: number;
seed?: number;
}🎨 Advanced Examples
Example 1: Basic Bracket
import { Bracket } from 'react-bracket-ui';
function BasicBracket() {
const matches = [
{
id: 1,
round: 1,
participant1: { id: 'p1', name: 'Player 1', score: 3 },
participant2: { id: 'p2', name: 'Player 2', score: 1 },
winner: 'p1'
}
];
return <Bracket matches={matches} />;
}Example 2: With Drag & Drop
import { Bracket, BracketChangeEvent } from 'react-bracket-ui';
import { useState } from 'react';
function DraggableBracket() {
const [matches, setMatches] = useState([
{
id: 1,
round: 1,
participant1: { id: 'p1', name: 'Team A' },
participant2: { id: 'p2', name: 'Team B' }
},
{
id: 2,
round: 1,
participant1: { id: 'p3', name: 'Team C' },
participant2: { id: 'p4', name: 'Team D' }
}
]);
const handleBracketChange = (event: BracketChangeEvent) => {
console.log('Bracket changed:', event);
setMatches(event.matches);
// Send to backend
// await api.updateBracket(event.matches);
};
return (
<Bracket
matches={matches}
enableDragDrop={true}
onBracketChange={handleBracketChange}
/>
);
}Example 3: With Zoom & Pan
import { Bracket } from 'react-bracket-ui';
function ZoomableBracket() {
return (
<Bracket
matches={matches}
enableZoomPan={true}
minZoom={0.3}
maxZoom={5}
initialZoom={1}
style={{ height: '800px' }}
/>
);
}Example 4: Custom Colors & Styling
import { Bracket } from 'react-bracket-ui';
function CustomBracket() {
return (
<Bracket
matches={matches}
colors={{
primary: '#ff6b6b',
secondary: '#4ecdc4',
background: '#1a1a2e',
error: '#ff0000',
warning: '#ffa500',
winner: '#90ee90'
}}
matchWidth={250}
matchHeight={120}
gap={30}
style={{
backgroundColor: '#0f0f23',
border: '2px solid #4ecdc4'
}}
/>
);
}Example 5: With Error Validation
import { Bracket, BracketError, validateBracket } from 'react-bracket-ui';
import { useMemo } from 'react';
function ValidatedBracket() {
const matches = [
{
id: 1,
round: 1,
participant1: { id: 'p1', name: 'Team A' },
participant2: { id: 'p2', name: 'Team B' },
winner: 'p999' // Invalid winner ID!
}
];
// Automatic validation
const errors = useMemo(() => validateBracket(matches), [matches]);
// Or custom validation
const customErrors: BracketError[] = [
{
matchId: 1,
message: 'This match has scheduling conflict',
type: 'warning'
}
];
return (
<Bracket
matches={matches}
errors={[...errors, ...customErrors]}
onErrorClick={(error) => {
alert(`Error in match ${error.matchId}: ${error.message}`);
}}
/>
);
}Example 6: Custom Round Names
import { Bracket } from 'react-bracket-ui';
function CustomRoundNames() {
return (
<Bracket
matches={matches}
showRoundNames={true}
roundNames={{
1: 'Round of 16',
2: 'Quarter Finals',
3: 'Semi Finals',
4: 'Grand Final'
}}
/>
);
}Example 7: Complete Tournament Example
import { Bracket, BracketChangeEvent } from 'react-bracket-ui';
import { useState, useCallback } from 'react';
function TournamentBracket() {
const [matches, setMatches] = useState([
// Round 1
{
id: 1,
round: 1,
matchNumber: 1,
participant1: { id: 't1', name: 'Team Alpha', seed: 1, score: 3 },
participant2: { id: 't8', name: 'Team Hotel', seed: 8, score: 0 },
winner: 't1',
nextMatchId: 5
},
{
id: 2,
round: 1,
matchNumber: 2,
participant1: { id: 't4', name: 'Team Delta', seed: 4, score: 2 },
participant2: { id: 't5', name: 'Team Echo', seed: 5, score: 1 },
winner: 't4',
nextMatchId: 5
},
{
id: 3,
round: 1,
matchNumber: 3,
participant1: { id: 't2', name: 'Team Bravo', seed: 2, score: 3 },
participant2: { id: 't7', name: 'Team Golf', seed: 7, score: 2 },
winner: 't2',
nextMatchId: 6
},
{
id: 4,
round: 1,
matchNumber: 4,
participant1: { id: 't3', name: 'Team Charlie', seed: 3, score: 1 },
participant2: { id: 't6', name: 'Team Foxtrot', seed: 6, score: 3 },
winner: 't6',
nextMatchId: 6
},
// Semi-Finals
{
id: 5,
round: 2,
matchNumber: 5,
participant1: { id: 't1', name: 'Team Alpha' },
participant2: { id: 't4', name: 'Team Delta' },
nextMatchId: 7
},
{
id: 6,
round: 2,
matchNumber: 6,
participant1: { id: 't2', name: 'Team Bravo' },
participant2: { id: 't6', name: 'Team Foxtrot' },
nextMatchId: 7
},
// Final
{
id: 7,
round: 3,
matchNumber: 7,
participant1: null,
participant2: null
}
]);
const handleBracketChange = useCallback(async (event: BracketChangeEvent) => {
console.log('Bracket updated:', event);
setMatches(event.matches);
// Sync with backend
try {
// await updateTournamentBracket(event.matches);
console.log('Bracket saved successfully');
} catch (error) {
console.error('Failed to save bracket:', error);
}
}, []);
return (
<div style={{ padding: '20px' }}>
<h1>World Championship 2025</h1>
<Bracket
matches={matches}
enableDragDrop={true}
enableZoomPan={true}
onBracketChange={handleBracketChange}
showRoundNames={true}
roundNames={{
1: 'Quarter Finals',
2: 'Semi Finals',
3: 'Grand Final'
}}
colors={{
primary: '#1976d2',
winner: '#c8e6c9'
}}
matchWidth={240}
gap={25}
style={{ height: '700px' }}
/>
</div>
);
}🛠️ Utility Functions
validateBracket
Validates bracket structure and returns errors:
import { validateBracket } from 'react-bracket-ui';
const errors = validateBracket(matches);
// Returns BracketError[] with validation issueshasMatchError
Check if a specific match has errors:
import { hasMatchError } from 'react-bracket-ui';
const hasError = hasMatchError(matchId, errors);getMatchErrorMessage
Get error message for a specific match:
import { getMatchErrorMessage } from 'react-bracket-ui';
const message = getMatchErrorMessage(matchId, errors);🎯 Use Cases
- Tournament Management Systems - Display and manage tournament brackets
- Sports Applications - Visualize competition brackets
- Gaming Platforms - Show esports tournament structures
- Event Management - Organize single-elimination events
- Admin Dashboards - Allow admins to adjust bracket arrangements
🤝 Integration Examples
With React Query
import { useQuery, useMutation } from '@tanstack/react-query';
import { Bracket, BracketChangeEvent } from 'react-bracket-ui';
function TournamentView({ tournamentId }: { tournamentId: string }) {
const { data: matches } = useQuery({
queryKey: ['tournament', tournamentId],
queryFn: () => fetchTournamentMatches(tournamentId)
});
const mutation = useMutation({
mutationFn: (matches: Match[]) => updateTournament(tournamentId, matches)
});
const handleChange = (event: BracketChangeEvent) => {
mutation.mutate(event.matches);
};
return (
<Bracket
matches={matches || []}
enableDragDrop={true}
onBracketChange={handleChange}
/>
);
}With Redux
import { useDispatch, useSelector } from 'react-redux';
import { Bracket } from 'react-bracket-ui';
import { updateBracketAction } from './store/tournamentSlice';
function ReduxBracket() {
const dispatch = useDispatch();
const matches = useSelector(state => state.tournament.matches);
return (
<Bracket
matches={matches}
enableDragDrop={true}
onBracketChange={(event) => {
dispatch(updateBracketAction(event.matches));
}}
/>
);
}📋 TypeScript Support
Full TypeScript support with comprehensive type definitions:
import type {
Match,
Participant,
BracketProps,
BracketError,
BracketChangeEvent,
DragDropResult
} from 'react-bracket-ui';🔧 Troubleshooting
Drag and Drop not working
Make sure to:
- Set
enableDragDrop={true} - Provide
onBracketChangecallback - Update your state with new matches from the callback
Zoom/Pan not working
Ensure:
- Set
enableZoomPan={true} - Provide adequate height in
styleprop - Check if parent container has overflow issues
Errors not showing
Verify:
- Matches have valid IDs
- Using
validateBracket()or providingerrorsprop - Check console for validation messages
🌟 Performance Tips
- Memoize matches array - Prevent unnecessary re-renders
- Use stable IDs - Don't use array indices as match IDs
- Optimize callbacks - Wrap callbacks with
useCallback - Lazy load - Load large brackets progressively
📄 License
ISC License - see LICENSE file for details
👥 Author
thuatdt137
🔗 Links
🙏 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
A React component library for displaying tournament brackets with TypeScript support.
Installation
npm install react-bracket-uiUsage
import React from 'react';
import { Bracket, Match } from 'react-bracket-ui';
const matches: Match[] = [
{
id: 1,
participant1: { id: 1, name: "Player A", score: 3 },
participant2: { id: 2, name: "Player B", score: 1 },
winner: 1,
round: 1,
nextMatchId: 3
},
{
id: 2,
participant1: { id: 3, name: "Player C", score: 2 },
participant2: { id: 4, name: "Player D", score: 4 },
winner: 4,
round: 1,
nextMatchId: 3
},
{
id: 3,
participant1: { id: 1, name: "Player A" },
participant2: { id: 4, name: "Player D" },
round: 2
}
];
function App() {
return (
<div>
<h1>Tournament</h1>
<Bracket matches={matches} />
</div>
);
}Props
Bracket Component
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| matches | Match[] | required | Array of match objects |
| className | string | undefined | CSS class name |
| style | React.CSSProperties | undefined | Inline styles |
Match Interface
interface Match {
id: string | number;
participant1?: {
id: string | number;
name: string;
score?: number;
};
participant2?: {
id: string | number;
name: string;
score?: number;
};
winner?: string | number;
round: number;
nextMatchId?: string | number;
}Features
- ✅ TypeScript support
- ✅ Responsive design
- ✅ Customizable styling
- ✅ Multiple rounds support
- ✅ Winner highlighting
- ✅ Score display
- ✅ ESM and CommonJS support
License
ISC
Author
thuatdt137
