A MagicMirror² module that displays a mirrored knockout tournament bracket for any sport. Providers fetch data from external APIs and normalize it into a common format; the engine renders it with a CSS Grid layout. You can also use a static bracket that you edit so you can display a tournament that has no online data.
Seeded from MMM-FIFAWorldCup and generalized for multi-sport support.
Wimbledon Mens Singles
FIFA World Cup

cd ~/MagicMirror/modules
git clone https://github.com/msimon360/MMM-BracketEngine
cd MMM-BracketEngine
npm installAdd to your config/config.js:
{
module: "MMM-BracketEngine",
position: "bottom_bar",
config: {
provider: "fifa",
providerConfig: {
seasonId: "285023",
language: "en"
},
updateInterval: 3 * 60 * 1000,
colored: true,
showDates: true,
showPenalties: false,
showFlags: true,
showLastUpdated: true
}
}Restart MagicMirror after installing: pm2 restart mm
| Option | Default | Description |
|---|---|---|
provider |
"fifa" |
Data provider name (fifa, static, wimbledon) |
providerConfig |
{} |
Provider-specific settings (see below) |
updateInterval |
180000 |
Milliseconds between data refreshes |
animationSpeed |
1000 |
DOM update fade speed in ms |
header |
null |
Override module header; null uses provider meta.title |
colored |
true |
Highlight match winners in green |
showDates |
true |
Show match dates under each card |
showPenalties |
false |
Show penalty shootout scores |
showFlags |
true |
Show team flag emojis |
showLastUpdated |
true |
Show last-updated timestamp |
provider: "fifa",
providerConfig: {
seasonId: "285023", // FIFA season ID
language: "en",
title: "FIFA World Cup 2026" // optional header override in payload
}Data source: https://api.fifa.com/api/v3/seasonbracket/season/{seasonId}
provider: "wimbledon",
providerConfig: {
year: "2025", // championship year
drawCode: "MS", // MS, LS, MD, LD, or XD
fromRoundCode: "4", // optional: 1, 2, 3, 4, Q, S, F — default depends on draw
title: "Wimbledon 2025 Gentlemen's Singles" // optional
}Data source: https://www.wimbledon.com/en_GB/scores/feeds/{year}/draws/{drawCode}.json
Defaults: Gentlemen's/Ladies' singles start at the Third Round (3, 16 players); doubles draws start at the Second Round (2). Use fromRoundCode: "4" for a compact singles view (Round of 16 through Final).
provider: "static",
providerConfig: {
filePath: "modules/MMM-BracketEngine/data/placeholder-bracket.json"
}Or pass inline data:
providerConfig: {
bracketData: { meta: { ... }, rounds: [ ... ] }
}The bracket is mirrored left/right with the trophy in the centre:
[R32][R16][QF][SF] 🏆 [SF][QF][R16][R32]
Smaller brackets (e.g. 16-team) omit earlier rounds and adjust column count automatically.
All providers must return this shape:
{
meta: {
title: "Tournament Name", // required
sport: "soccer", // required
icon: "⚽", // optional
updatedAt: "2026-07-06T..." // optional
},
rounds: [
{
id: "R16", // R32, R16, QF, SF, 3RD, F
name: "Round of 16",
matches: [
{
id: 1,
status: "final", // scheduled | live | final
teamA: { name: "Germany", abbr: "GER" },
teamB: { name: "France", abbr: "FRA" },
// optional:
date: "Jul 6",
scoreA: 2, scoreB: 1,
penA: 4, penB: 3,
winner: "GER"
}
]
}
]
}Validation runs in node_helper.js before data reaches the frontend. Invalid payloads trigger BE_BRACKET_ERROR.
After validation, layoutRoundsForMirroredBracket() reorders side-round matches for the mirrored renderer. Bracket results flow outside-in (R32 → R16 → QF → …). Layout walks the other way for sorting only: when QF lists its teams, the engine reorders the R16 match list so winners line up with the correct QF slot — it does not imply QF feeds R16.
Uses each match's teams and winners to infer feeder links when the next stage already lists its participants. No extra provider fields required. Optional sources: [parentIdA, parentIdB] can override inference when an API exposes explicit parent ids.
- Create
providers/MyProvider.jsextendingBaseProvider - Implement
async fetchBracket()returning the schema above - Register in
node_helper.js:
const MyProvider = require("./providers/MyProvider");
const PROVIDERS = { fifa: FifaProvider, static: StaticProvider, mysport: MyProvider };Provider-specific match ordering (e.g. FIFA R32 reorder) belongs in the provider, not the engine.
MMM-BracketEngine.js ←→ node_helper.js ←→ providers/
(render) (router) (fifa, static, …)
Socket notifications: BE_GET_BRACKET, BE_BRACKET_RESULT, BE_BRACKET_ERROR
MMM-BracketEngine and MMM-FIFAWorldCup can coexist on the same mirror (different socket prefixes).
npm testValidates the placeholder bracket against the schema.
- "Loading bracket…" forever — check
pm2 logs mmfor[MMM-BracketEngine]errors. Confirm network access if using the FIFA provider. - Stale data after error — the module keeps the last good bracket and shows a dimmed error message.
- Layout looks wrong — full restart required (
pm2 restart mm), not just a browser refresh.
MIT