react-bracket-ui
Version:
A modern, feature-rich React component library for displaying single-elimination tournament brackets with drag-drop, zoom/pan, and error validation
671 lines (555 loc) • 16 kB
Markdown
# react-bracket-ui
<div align="center">
<h3>🏆 Professional React Tournament Bracket Component Library</h3>
<p>A modern, feature-rich React component for displaying single-elimination tournament brackets</p>
</div>
## ✨ 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
```bash
npm install react-bracket-ui
# or
yarn add react-bracket-ui
# or
pnpm add react-bracket-ui
```
## 🚀 Quick Start
```tsx
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
```typescript
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
```typescript
interface Participant {
id: string | number;
name: string;
score?: number;
seed?: number;
}
```
## 🎨 Advanced Examples
### Example 1: Basic Bracket
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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:
```tsx
import { validateBracket } from 'react-bracket-ui';
const errors = validateBracket(matches);
// Returns BracketError[] with validation issues
```
### hasMatchError
Check if a specific match has errors:
```tsx
import { hasMatchError } from 'react-bracket-ui';
const hasError = hasMatchError(matchId, errors);
```
### getMatchErrorMessage
Get error message for a specific match:
```tsx
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
```tsx
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
```tsx
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:
```typescript
import type {
Match,
Participant,
BracketProps,
BracketError,
BracketChangeEvent,
DragDropResult
} from 'react-bracket-ui';
```
## 🔧 Troubleshooting
### Drag and Drop not working
Make sure to:
1. Set `enableDragDrop={true}`
2. Provide `onBracketChange` callback
3. Update your state with new matches from the callback
### Zoom/Pan not working
Ensure:
1. Set `enableZoomPan={true}`
2. Provide adequate height in `style` prop
3. Check if parent container has overflow issues
### Errors not showing
Verify:
1. Matches have valid IDs
2. Using `validateBracket()` or providing `errors` prop
3. Check console for validation messages
## 🌟 Performance Tips
1. **Memoize matches array** - Prevent unnecessary re-renders
2. **Use stable IDs** - Don't use array indices as match IDs
3. **Optimize callbacks** - Wrap callbacks with `useCallback`
4. **Lazy load** - Load large brackets progressively
## 📄 License
ISC License - see LICENSE file for details
## 👥 Author
**thuatdt137**
## 🔗 Links
- [GitHub Repository](https://github.com/thuatdt137/react-bracket-ui)
- [npm Package](https://www.npmjs.com/package/react-bracket-ui)
- [Report Issues](https://github.com/thuatdt137/react-bracket-ui/issues)
## 🙏 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
<div align="center">
Made with ❤️ for the tournament community
</div>
A React component library for displaying tournament brackets with TypeScript support.
## Installation
```bash
npm install react-bracket-ui
```
## Usage
```tsx
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
```tsx
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