World Cup 2026 — Pick Your Bracket
Football > World Cup 2026 > Pick Your Bracket
Football · World Cup 2026 · Pick Your Bracket
Back yourself: pick the whole bracket
Call a winner in all 31 knockout ties, from the round of 32 to the final. The model’s pick is marked in every tie, your detours are counted as upsets, and your bracket saves automatically in this browser.
Click a team to send them through — they appear in the next round, and any downstream picks they invalidate are cleared. The model's pick in each tie (by simulated title probability) carries a ◆ marker; backing the other side counts as an upset. The small percentage next to each team is its champion probability. Field + seeding follow the projected group finishes on FIFA's official 2026 bracket.
Show code
statsEsc = window.statsEsc
// Group projections + champion odds from the LIVE in-browser sim
// (wc-live-sim.js): real + in-progress results baked in, re-run every minute
// while games are live. p_champ stays on the 0–100 scale (as the parquet was).
_wcpGroups = window.wcLiveSim.groupsStream()
_wcpSim = window.wcLiveSim.simStream()
// Flag lookup + markup come from the shared wcMaps helper (wc-maps.js);
// this page keeps its own .wcp-flag sizing CSS.
wcpFlagImg = (team) => window.wcMaps.flagImg(team, "wcp-flag")
// Same R32 bracket as the wallchart / title-race pages — FIFA's real
// pairings via the shared wcMaps.r32Seeds (wc-maps.js).
wcpSeeds = window.wcMaps.r32SeedsShow code
wcpField = {
if (!_wcpGroups) return null
const byGroup = new Map()
for (const r of _wcpGroups) { if (!byGroup.has(r.group)) byGroup.set(r.group, []); byGroup.get(r.group).push(r) }
const seedToTeam = new Map(), groupThird = new Map()
for (const [g, rows] of byGroup.entries()) {
const w = [...rows].sort((a, b) => b.win_group - a.win_group)[0]?.team || "?"
const ru = [...rows].sort((a, b) => b.runner_up - a.runner_up)[0]?.team || "?"
seedToTeam.set("1" + g, w); seedToTeam.set("2" + g, ru)
const third = [...rows].filter(x => x.team !== w && x.team !== ru).sort((a, b) => (b.third ?? 0) - (a.third ?? 0))[0]
if (third) groupThird.set(g, { g, team: third.team, score: third.third ?? 0 })
}
// 8 best thirds into the composite "3X/Y/Z" slots — shared
// wcMaps.assignThirdSlots (distinct teams, backtracking + greedy fallback).
const qualifiers = [...groupThird.values()].sort((a, b) => b.score - a.score).slice(0, 8)
const assigned = window.wcMaps.assignThirdSlots(qualifiers)
for (const s of wcpSeeds.flat()) {
if (s.startsWith("3") && s.includes("/")) seedToTeam.set(s, assigned.get(s)?.team || "?")
}
return wcpSeeds.map(([s1, s2]) => ({ a: seedToTeam.get(s1) || s1, b: seedToTeam.get(s2) || s2 }))
}
wcpChamp = _wcpSim ? new Map(_wcpSim.map(t => [t.team, t.p_champ])) : new Map()Show code
// ── Interactive bracket ───────────────────────────────────────────
{
// wcpField is null ONLY when the groups parquet failed to load (OJS never
// runs this cell while the loader is still pending) — failure copy, not
// a forever-"Loading…".
if (wcpField == null) return html`<p class="wcw-loading">Data failed to load — try refreshing (see console for details).</p>`
const ROUNDS = ["Round of 32", "Round of 16", "Quarter-finals", "Semi-finals", "Final"]
const NR = ROUNDS.length
const KEY = "ig-wc-bracket"
const cp = t => wcpChamp.get(t) ?? 0
// model's pick of a tie (higher title probability)
const modelPick = (a, b) => (a === "?" || a == null) ? b : (b === "?" || b == null) ? a : (cp(b) > cp(a) ? b : a)
// picks[r][i] = the user's chosen winner of round r, matchup i (or null)
let picks = ROUNDS.map((_, r) => new Array(wcpField.length >> r).fill(null))
try { const s = JSON.parse(localStorage.getItem(KEY) || "null"); if (Array.isArray(s) && s.length === NR) picks = s.map((row, r) => { const want = wcpField.length >> r; const a = new Array(want).fill(null); for (let i = 0; i < want; i++) a[i] = row[i] ?? null; return a }) } catch (e) {}
const save = () => { try { localStorage.setItem(KEY, JSON.stringify(picks)) } catch (e) {} }
// teams entering round r, matchup i
const tie = (r, i) => r === 0 ? wcpField[i] : { a: picks[r - 1][2 * i], b: picks[r - 1][2 * i + 1] }
// After a pick, drop any downstream pick that's no longer one of its tie's teams.
function reconcile() {
for (let r = 1; r < NR; r++) for (let i = 0; i < picks[r].length; i++) {
const t = tie(r, i)
if (picks[r][i] != null && picks[r][i] !== t.a && picks[r][i] !== t.b) picks[r][i] = null
}
}
function setPick(r, i, team) { picks[r][i] = team; reconcile(); save(); render() }
// model bracket (for the comparison badge + "fill with model")
function modelBracket() {
const mp = ROUNDS.map((_, r) => new Array(wcpField.length >> r).fill(null))
for (let r = 0; r < NR; r++) for (let i = 0; i < mp[r].length; i++) {
const t = r === 0 ? wcpField[i] : { a: mp[r - 1][2 * i], b: mp[r - 1][2 * i + 1] }
mp[r][i] = modelPick(t.a, t.b)
}
return mp
}
const wrap = document.createElement("div"); wrap.className = "wcp-wrap"
const bar = document.createElement("div"); bar.className = "wcp-bar"
const board = document.createElement("div"); board.className = "wcp-board"
wrap.append(bar, board)
function teamRow(r, i, team, isPick, model) {
const el = document.createElement("button")
el.className = "wcp-team" + (team == null || team === "?" ? " tbd" : "") + (isPick ? " picked" : (isPick === false ? " dropped" : ""))
if (model && team === model && team != null && team !== "?") el.classList.add("model")
el.disabled = team == null || team === "?"
// NB: p_champ in wc2026_simulation.parquet is already 0–100 (see title-race
// page) — don't multiply by 100 again.
el.innerHTML = wcpFlagImg(team) + `<span class="wcp-tn">${statsEsc(team == null ? "—" : team)}</span>` + (team && team !== "?" && cp(team) ? `<span class="wcp-cp">${cp(team).toFixed(0)}%</span>` : "")
if (!el.disabled) el.onclick = () => setPick(r, i, team)
return el
}
function render() {
const mp = modelBracket()
const myChamp = picks[NR - 1][0], modelChamp = mp[NR - 1][0]
// count picks made + upsets (your pick differs from model on the same tie's teams)
let made = 0, upsets = 0
for (let r = 0; r < NR; r++) for (let i = 0; i < picks[r].length; i++) {
if (picks[r][i] != null) { made++; const t = tie(r, i); if (modelPick(t.a, t.b) !== picks[r][i]) upsets++ }
}
bar.replaceChildren()
const status = document.createElement("div"); status.className = "wcp-status"
status.innerHTML = myChamp
? `Your champion: <b>${wcpFlagImg(myChamp)} ${statsEsc(myChamp)}</b>` + (myChamp === modelChamp ? ` — agrees with the model` : ` — model picks <b>${statsEsc(modelChamp)}</b>`)
: `Pick winners to crown your champion · model favours <b>${statsEsc(modelChamp || "?")}</b>`
const meta = document.createElement("div"); meta.className = "wcp-meta"
meta.innerHTML = `${made}/31 picked · <span class="legend-tag legend-bad" style="font-size:0.7rem">${upsets} upset${upsets === 1 ? "" : "s"}</span>`
const actions = document.createElement("div"); actions.className = "wcp-actions"
const fill = document.createElement("button"); fill.className = "wcp-btn"; fill.textContent = "Fill with model"; fill.onclick = () => { picks = mp.map(row => row.slice()); save(); render() }
const reset = document.createElement("button"); reset.className = "wcp-btn"; reset.textContent = "Clear"; reset.onclick = () => { picks = ROUNDS.map((_, r) => new Array(wcpField.length >> r).fill(null)); save(); render() }
actions.append(fill, reset)
bar.append(status, meta, actions)
board.replaceChildren()
for (let r = 0; r < NR; r++) {
const col = document.createElement("div"); col.className = "wcp-col"
const h = document.createElement("div"); h.className = "wcp-col-head"; h.textContent = ROUNDS[r]; col.appendChild(h)
const ties = document.createElement("div"); ties.className = "wcp-ties"
for (let i = 0; i < picks[r].length; i++) {
const t = tie(r, i), p = picks[r][i]
const box = document.createElement("div"); box.className = "wcp-tie"
box.append(
teamRow(r, i, t.a, p == null ? null : (p === t.a), mp[r][i]),
teamRow(r, i, t.b, p == null ? null : (p === t.b), mp[r][i])
)
ties.appendChild(box)
}
col.appendChild(ties)
board.appendChild(col)
}
// trophy column
const champCol = document.createElement("div"); champCol.className = "wcp-col wcp-champ-col"
const h2 = document.createElement("div"); h2.className = "wcp-col-head"; h2.textContent = "Champion"; champCol.appendChild(h2)
const champBox = document.createElement("div"); champBox.className = "wcp-champ"
champBox.innerHTML = myChamp ? wcpFlagImg(myChamp) + `<div class="wcp-champ-name">${statsEsc(myChamp)}</div>` : `<div class="wcp-champ-tbd">?</div>`
champCol.appendChild(champBox)
board.appendChild(champCol)
}
render()
wrap.appendChild(window.editorial.tableSource({
source: "pannadata",
sourceUrl: "https://github.com/peteowen1/pannadata",
license: "CC BY 4.0",
asAt: "Live during play",
hint: "Champion odds from the live in-browser sim · re-runs while games are live"
}))
return wrap
}