World Cup 2026 — Venues
Football > World Cup 2026 > Venues
Football · World Cup 2026 · Venues
Sixteen stadiums, three countries
From Estadio Azteca — the first ground to stage matches at three World Cups — to BMO Field’s lakefront box in Toronto, all 104 fixtures mapped onto the 16 host stadiums: 72 group games plus every knockout match through to the July 19 final at MetLife Stadium.
Show code
// ── Curated venue metadata ───────────────────────────────────────
// `key` must match wcMaps.V display strings EXACTLY — the join below warns
// on any drift. Coordinates from English Wikipedia stadium articles,
// cross-checked against Wikidata (P625) — agreement to ~4 decimal places
// on all 16. Capacities are FIFA's published World Cup-configuration
// seating figures (2026 FIFA World Cup Wikipedia venues table, spot-checked
// against FIFA's official-capacity announcement coverage: Azteca 80,824,
// MetLife 80,663, BMO ~43,000). They run well below each ground's normal
// max because FIFA's pitch + media + hospitality footprint eats seats.
// `metro` is FIFA's host-city label (the stadium city often differs —
// AT&T Stadium is in Arlington, not Dallas).
wcVenueMeta = [
{ key: "AT&T Stadium, Arlington", stadium: "AT&T Stadium", city: "Arlington", metro: "Dallas", country: "United States", lat: 32.7478, lon: -97.0928, cap: 70649 },
{ key: "Arrowhead Stadium, Kansas City", stadium: "Arrowhead Stadium", city: "Kansas City", metro: "Kansas City", country: "United States", lat: 39.0489, lon: -94.4839, cap: 69045 },
{ key: "BMO Field, Toronto", stadium: "BMO Field", city: "Toronto", metro: "Toronto", country: "Canada", lat: 43.6333, lon: -79.4186, cap: 43036 },
{ key: "BC Place, Vancouver", stadium: "BC Place", city: "Vancouver", metro: "Vancouver", country: "Canada", lat: 49.2767, lon: -123.1119, cap: 52497 },
{ key: "Estadio Akron, Zapopan", stadium: "Estadio Akron", city: "Zapopan", metro: "Guadalajara", country: "Mexico", lat: 20.6817, lon: -103.4628, cap: 45664 },
{ key: "Estadio Azteca, Mexico City", stadium: "Estadio Azteca", city: "Mexico City", metro: "Mexico City", country: "Mexico", lat: 19.3031, lon: -99.1506, cap: 80824 },
{ key: "Estadio BBVA, Guadalupe", stadium: "Estadio BBVA", city: "Guadalupe", metro: "Monterrey", country: "Mexico", lat: 25.6692, lon: -100.2444, cap: 51243 },
{ key: "Gillette Stadium, Foxborough", stadium: "Gillette Stadium", city: "Foxborough", metro: "Boston", country: "United States", lat: 42.0910, lon: -71.2643, cap: 64146 },
{ key: "Hard Rock Stadium, Miami Gardens", stadium: "Hard Rock Stadium", city: "Miami Gardens", metro: "Miami", country: "United States", lat: 25.9581, lon: -80.2389, cap: 64478 },
{ key: "Lumen Field, Seattle", stadium: "Lumen Field", city: "Seattle", metro: "Seattle", country: "United States", lat: 47.5952, lon: -122.3316, cap: 66925 },
{ key: "Lincoln Financial Field, Philadelphia", stadium: "Lincoln Financial Field", city: "Philadelphia", metro: "Philadelphia", country: "United States", lat: 39.9008, lon: -75.1675, cap: 68324 },
{ key: "Levi's Stadium, Santa Clara", stadium: "Levi's Stadium", city: "Santa Clara", metro: "San Francisco Bay Area", country: "United States", lat: 37.4034, lon: -121.9700, cap: 68827 },
{ key: "Mercedes-Benz Stadium, Atlanta", stadium: "Mercedes-Benz Stadium", city: "Atlanta", metro: "Atlanta", country: "United States", lat: 33.7556, lon: -84.4008, cap: 68239 },
{ key: "MetLife Stadium, East Rutherford", stadium: "MetLife Stadium", city: "East Rutherford", metro: "New York New Jersey", country: "United States", lat: 40.8135, lon: -74.0744, cap: 80663 },
{ key: "NRG Stadium, Houston", stadium: "NRG Stadium", city: "Houston", metro: "Houston", country: "United States", lat: 29.6847, lon: -95.4108, cap: 68777 },
{ key: "SoFi Stadium, Inglewood", stadium: "SoFi Stadium", city: "Inglewood", metro: "Los Angeles", country: "United States", lat: 33.9534, lon: -118.3387, cap: 70492 },
]Show code
wcKoSchedule = window.wcMaps.koSchedule
// Live fixture feed — group-game kickoff times for the fixture rows
// (rendered in the reader's timezone). NULL/[] degrade to date-only rows.
_wcvFx = {
return await window.wcMaps.fetchWcFixtures()
}
// "day|home|away" (canonical names, UTC day — same key shape as
// wcMaps.venues) → local kickoff time string.
_wcvTimeByKey = {
const wc = window.wcMaps
const map = new Map()
for (const m of (_wcvFx || [])) {
if (!m.date || String(m.date).length <= 11) continue
const t = new Date(m.date).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
map.set(`${String(m.date).slice(0, 10)}|${m._h}|${m._a}`, t)
}
// Dead-join check (page convention: warn on silent join drift) — timed feed
// rows exist but none key onto the curated venues map means the key shapes
// diverged and every fixture row silently lost its kickoff time.
if (map.size > 0 && !Object.keys(wc.venues).some(k => map.has(k))) {
console.warn("[wc-venues] kickoff-time join matched 0 fixtures — feed vs venues-map key drift?")
}
return map
}Show code
wcVenuePhotos = ({
"AT&T Stadium, Arlington": { file: "Cowboys Stadium 2.jpg", artist: "bobbyh_80", license: "CC BY 2.0" },
"Arrowhead Stadium, Kansas City": { file: "Aerial_view_of_Arrowhead_Stadium_08-31-2013.jpg", artist: "Ichabod", license: "CC BY-SA 3.0" },
"BMO Field, Toronto": { file: "BMO Field in 2016.png", artist: "Pablopicassotoronto", license: "CC BY-SA 4.0" },
"BC Place, Vancouver": { file: "BC Place Opening Day 2011-09-30.jpg", artist: "Yvrphoto", license: "CC BY-SA 3.0" },
"Estadio Akron, Zapopan": { file: "Estadio Omnilife Chivas.jpg", artist: "Juan Olivas", license: "CC BY 2.0" },
"Estadio Azteca, Mexico City": { file: "Vista aérea del Estadio Azteca - 2026 - 02.jpg", artist: "ProtoplasmaKid", license: "CC BY 4.0" },
"Estadio BBVA, Guadalupe": { file: "Mexico Guadalupe Monterrey Estadio BBVA Bancomer fifa world cup 2026 6.JPG", artist: "Arne Müseler", license: "CC BY-SA 3.0 de" },
"Gillette Stadium, Foxborough": { file: "Gillette Stadium02.jpg", artist: "Bernard Gagnon", license: "CC BY-SA 3.0" },
"Hard Rock Stadium, Miami Gardens": { file: "Hard Rock Stadium for Super Bowl LIV (49606707583).jpg", artist: "elisfkc2", license: "CC BY-SA 2.0" },
"Lumen Field, Seattle": { file: "Qwest Field North.jpg", artist: "Smart Destinations / GoSeattleCard.com", license: "CC BY-SA 2.0" },
"Lincoln Financial Field, Philadelphia": { file: "Philly (45).JPG", artist: "Betp (French Wikipedia)", license: "CC BY-SA 3.0" },
"Levi's Stadium, Santa Clara": { file: "Levi's Stadium interior 1.jpg", artist: "Matthew Roth", license: "CC BY-SA 2.0" },
"Mercedes-Benz Stadium, Atlanta": { file: "Mercedes Benz Stadium time lapse capture 2017-08-13.jpg", artist: "Atlanta Falcons", license: "CC BY 3.0" },
"MetLife Stadium, East Rutherford": { file: "New Meadowlands Stadium Mezz Corner.jpg", artist: "babyknight", license: "CC BY 2.0" },
"NRG Stadium, Houston": { file: "Reliantstadium.jpg", artist: "eschipul (Flickr)", license: "CC BY-SA 2.0" },
"SoFi Stadium, Inglewood": { file: "SoFi Stadium (51126606022).jpg", artist: "Thank You (21 Millions+) views (Flickr)", license: "CC BY 2.0" },
})
// img src + Commons page + plain-text credit for a venue key (null = no photo)
venuePhoto = (key, width) => {
const p = wcVenuePhotos[key]
if (!p) return null
return {
src: "https://commons.wikimedia.org/wiki/Special:FilePath/" + encodeURIComponent(p.file) + "?width=" + (width || 800),
page: "https://commons.wikimedia.org/wiki/" + encodeURIComponent("File:" + p.file),
credit: `Photo: ${p.artist} · ${p.license} · Wikimedia Commons`
}
}Show code
venueRows = {
const wc = window.wcMaps
const byVenue = new Map()
for (const [k, stadium] of Object.entries(wc.venues)) {
const [day, home, away] = k.split("|")
if (!day || !home || !away) { console.warn("[wc-venues] malformed venue key:", k); continue }
if (!byVenue.has(stadium)) byVenue.set(stadium, [])
byVenue.get(stadium).push({ day, home, away })
}
const metaKeys = new Set(wcVenueMeta.map(m => m.key))
for (const s of byVenue.keys()) {
if (!metaKeys.has(s)) console.warn("[wc-venues] venue in wcMaps.venues but missing metadata:", s)
}
const koByVenue = new Map()
for (const k of wcKoSchedule) {
if (!metaKeys.has(k.key)) console.warn("[wc-venues] knockout venue missing metadata:", k.key)
if (!koByVenue.has(k.key)) koByVenue.set(k.key, [])
koByVenue.get(k.key).push(k)
}
const rows = wcVenueMeta.map(m => {
const matches = (byVenue.get(m.key) || []).sort((a, b) => a.day.localeCompare(b.day))
if (matches.length === 0) console.warn("[wc-venues] no fixtures mapped for:", m.key)
const ko = (koByVenue.get(m.key) || []).sort((a, b) => a.m - b.m)
return { ...m, matches, ko, total: matches.length + ko.length }
})
return rows.sort((a, b) => b.cap - a.cap)
}
// Shared fixture-row markup (tooltip + cards) — date (+ local kickoff time
// when the live feed has it), then home v away with flags + click-through
// team links from wcMaps.
venueMatchRowsHtml = (matches) => {
const wc = window.wcMaps
return matches.map(m => {
const t = _wcvTimeByKey.get(`${m.day}|${wc.normalizeWcTeam(m.home)}|${wc.normalizeWcTeam(m.away)}`)
return `<div class="wcv-match"><span class="wcv-date">${wc.fmtDay(m.day)}${t ? `<span class="wcv-time">${wc.esc(t)}</span>` : ""}</span>` +
`${wc.teamLinkHtml(m.home)}<span class="wcv-vs">v</span>${wc.teamLinkHtml(m.away)}</div>`
}).join("")
}
// Knockout rows — teams unknown until the bracket fills, so show the round
// + FIFA bracket pairing ("2A v 2B", "W89 v W90"); match number in the title.
venueKoRowsHtml = (ko) => {
const wc = window.wcMaps
if (!ko || ko.length === 0) return ""
const rows = ko.map(k =>
`<div class="wcv-match wcv-ko" title="FIFA match ${k.m}"><span class="wcv-date">${wc.fmtDay(k.day)}</span>` +
`<span class="wcv-ko-round">${wc.esc(k.round)}</span><span class="wcv-ko-num">${wc.esc(k.pair)}</span></div>`
).join("")
return `<div class="wcv-ko-head">Knockout</div>${rows}`
}Where the World Cup lives
Show code
_topojsonClient = {
try { return await require("topojson-client@3") }
catch (e) { console.error("[wc-venues] topojson-client load failed:", e); return null }
}
// World outlines from the world-atlas CDN build (Natural Earth 1:50m).
// cdn.jsdelivr.net is already in the site CSP for connect-src.
_worldTopo = {
try {
const r = await fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json")
if (!r.ok) throw new Error("HTTP " + r.status)
return await r.json()
} catch (e) {
console.error("[wc-venues] world topojson load failed:", e)
return null
}
}Show code
// ── The map ──────────────────────────────────────────────────────
// Conic conformal projection over North America, framed so Mexico City to
// Vancouver/Toronto all fit (≈14°N–58°N / 130°W–60°W). Host countries get
// a faint theme-reactive wash + muted stroke (chart-helpers conventions);
// neighbours are ghosted in for context. Markers are sized by total match
// count (group + knockout); hover shows the venue tooltip, click pins it so
// the team links are clickable.
{
if (_worldTopo == null || _topojsonClient == null) {
return html`<p class="text-muted">Map data failed to load — the venue cards below carry everything (see console for details).</p>`
}
const wc = window.wcMaps
const esc = wc.esc
const svgEl = window.chartHelpers.svgEl
const W = 880, H = 600
const proj = d3.geoConicConformal().rotate([95, 0]).parallels([20, 50])
// MultiPoint frame (corner + edge-midpoints — no polygon-winding trap)
const frame = { type: "MultiPoint", coordinates: [
[-130, 14], [-95, 14], [-60, 14], [-60, 58], [-95, 58], [-130, 58]
] }
proj.fitExtent([[10, 10], [W - 10, H - 10]], frame)
const geoPath = d3.geoPath(proj)
const wrap = document.createElement("div")
wrap.className = "wcv-map-wrap"
const svg = svgEl("svg", { viewBox: `0 0 ${W} ${H}`, class: "wcv-map-svg", role: "img",
"aria-label": "Map of the 16 World Cup 2026 stadiums across the USA, Canada and Mexico" })
wrap.appendChild(svg)
// Country outlines — hosts emphasized, neighbours ghosted
const HOSTS = new Set(["840", "124", "484"]) // USA, Canada, Mexico
const countries = _topojsonClient.feature(_worldTopo, _worldTopo.objects.countries).features
for (const f of countries) {
const host = HOSTS.has(String(f.id))
const d = geoPath(f)
if (!d) continue
svg.appendChild(svgEl("path", {
d,
style: host
? "fill: rgba(var(--site-overlay-rgb), 0.05); stroke: rgba(var(--site-overlay-rgb), 0.32); stroke-width: 1"
: "fill: rgba(var(--site-overlay-rgb), 0.02); stroke: rgba(var(--site-overlay-rgb), 0.10); stroke-width: 0.7"
}))
}
// Country labels — faint, letterspaced, almanac-style
const labels = [
["CANADA", [-116, 54.5]], ["UNITED STATES", [-107.5, 43]], ["MEXICO", [-105.5, 23.8]]
]
for (const [text, lonlat] of labels) {
const p = proj(lonlat)
if (!p) continue
svg.appendChild(svgEl("text", {
x: p[0], y: p[1], "text-anchor": "middle", class: "wcv-country-label"
}, text))
}
// Tooltip (page-local glass tip — dark in both modes, like .field-tooltip)
const tip = document.createElement("div")
tip.className = "wcv-tip"
wrap.appendChild(tip)
tip.addEventListener("click", e => e.stopPropagation())
let pinnedKey = null
const markers = new Map()
function tipHtml(v) {
const ph = venuePhoto(v.key, 400)
const photo = ph
? `<img class="wcv-tip-photo" src="${ph.src}" alt="" loading="lazy" title="${esc(ph.credit)}" onerror="this.style.display='none'">`
: ""
return photo +
`<div class="wcv-tip-head">${esc(v.stadium)}</div>` +
`<div class="wcv-tip-sub">${esc(v.city)}, ${esc(v.country)} · capacity ${v.cap.toLocaleString("en-US")}</div>` +
`<div class="wcv-tip-matches">${venueMatchRowsHtml(v.matches)}${venueKoRowsHtml(v.ko)}</div>`
}
function showTip(v) {
tip.innerHTML = tipHtml(v)
tip.classList.toggle("pinned", pinnedKey === v.key)
const rect = svg.getBoundingClientRect()
const s = rect.width / W
tip.style.left = (v._px * s) + "px"
tip.style.top = (v._py * s - 6 * s) + "px"
tip.style.display = "block"
// Flip below the marker when there's no headroom above the map —
// northern venues (Vancouver, Seattle, Toronto) otherwise clip at the top
tip.classList.toggle("below", v._py * s < tip.offsetHeight + 18)
// Clamp horizontally inside the wrap
const tr = tip.getBoundingClientRect()
const wr = wrap.getBoundingClientRect()
const left = parseFloat(tip.style.left)
if (tr.left < wr.left + 2) tip.style.left = (left + (wr.left + 2 - tr.left)) + "px"
else if (tr.right > wr.right - 2) tip.style.left = (left - (tr.right - (wr.right - 2))) + "px"
}
function hideTip() {
tip.style.display = "none"
tip.classList.remove("pinned")
}
function setActive(key) {
for (const [k, c] of markers) c.classList.toggle("active", k === key)
}
// Venue markers — circle area scales with total match count (group +
// knockout, matching rOf(v.total) below and the on-page note)
const rOf = n => 3.6 * Math.sqrt(Math.max(n, 1))
// Draw northernmost first so southern (lower) markers sit on top in overlaps
const ordered = [...venueRows].sort((a, b) => b.lat - a.lat)
for (const v of ordered) {
const p = proj([v.lon, v.lat])
if (!p) { console.warn("[wc-venues] projection failed for:", v.key); continue }
v._px = p[0]; v._py = p[1]
const c = svgEl("circle", {
cx: p[0], cy: p[1], r: rOf(v.total),
class: "wcv-marker", tabindex: "0",
"aria-label": `${v.stadium} — ${v.matches.length} group matches` +
(v.ko.length ? ` and ${v.ko.length} knockout` : "")
})
c.addEventListener("mouseenter", () => { if (!pinnedKey) { setActive(v.key); showTip(v) } })
c.addEventListener("mouseleave", () => { if (!pinnedKey) { setActive(null); hideTip() } })
c.addEventListener("click", (e) => {
e.stopPropagation()
if (pinnedKey === v.key) { pinnedKey = null; setActive(null); hideTip() }
else { pinnedKey = v.key; setActive(v.key); showTip(v) }
})
markers.set(v.key, c)
svg.appendChild(c)
}
// Click anywhere off a marker → unpin
wrap.addEventListener("click", () => {
if (pinnedKey) { pinnedKey = null; setActive(null); hideTip() }
})
const note = document.createElement("p")
note.className = "wcv-map-note text-muted"
note.textContent = "Circle area = total matches hosted, group stage plus knockout. Hover a stadium for its fixtures; click to pin the card and follow the team links. Knockout rows show FIFA's bracket slots — teams TBD until the groups resolve."
const out = document.createElement("div")
out.appendChild(wrap)
out.appendChild(note)
return out
}Venue by venue
The same 16 grounds as cards — every fixture from the group stage to the final, with capacities in FIFA’s World Cup seating configuration (pitch, media and hospitality footprints cut these well below each stadium’s normal capacity). Ordered biggest to smallest.
Show code
// ── Fallback list — the mobile / no-map / print experience ──────
{
const wc = window.wcMaps
const esc = wc.esc
const cards = venueRows.map(v => {
const where = [v.city, v.metro !== v.city ? `${v.metro} metro` : null, v.country]
.filter(Boolean).map(esc).join(" · ")
const ph = venuePhoto(v.key, 800)
// photo + credit share one hideable wrapper — onerror must not leave a
// dangling attribution line crediting a photo that isn't shown
const photo = ph
? `<div class="wcv-photo-block"><a href="${ph.page}" target="_blank" rel="noopener" class="wcv-photo-link">` +
`<img class="wcv-photo" src="${ph.src}" alt="${esc(v.stadium)}" loading="lazy" onerror="this.closest('.wcv-photo-block').style.display='none'">` +
`</a><div class="wcv-photo-credit">${esc(ph.credit)}</div></div>`
: ""
return `
<div class="wcv-card">
${photo}
<div class="wcv-card-head">
<span class="wcv-card-name">${esc(v.stadium)}</span>
<span class="wcv-card-count">${v.total} matches</span>
</div>
<div class="wcv-card-sub">${wc.flagImg(v.country)} ${where}</div>
<div class="wcv-card-cap">Capacity ${v.cap.toLocaleString("en-US")} <span class="wcv-cap-note">(World Cup configuration)</span></div>
<div class="wcv-card-matches">${venueMatchRowsHtml(v.matches)}${venueKoRowsHtml(v.ko)}</div>
</div>`
})
const grid = document.createElement("div")
grid.className = "wcv-grid"
grid.innerHTML = cards.join("")
return grid
}Show code
{
const inner = document.createElement("div")
inner.className = "side-rail-inner"
const { railBlock, btnTile, tableSource } = window.editorial
const btn = railBlock("By the numbers")
btn.appendChild(btnTile("16", [{ text: "Host stadiums" }]))
btn.appendChild(btnTile("3", [{ text: "Countries — first triple-host World Cup" }]))
if (venueRows && venueRows.length > 0) {
const biggest = venueRows.reduce((a, b) => (b.cap > a.cap ? b : a))
btn.appendChild(btnTile(biggest.cap.toLocaleString("en-US"), [
{ text: "Biggest World Cup capacity · " }, { text: biggest.stadium, bold: true }
]))
const most = Math.max(...venueRows.map(v => v.total))
const atMost = venueRows.filter(v => v.total === most)
btn.appendChild(btnTile(String(most), [
{ text: "Most matches at one venue · " },
{ text: atMost.length > 1 ? `${atMost.length} venues tied` : atMost[0].stadium, bold: true }
]))
}
inner.appendChild(btn)
const links = railBlock("Read next")
const l0 = document.createElement("div")
l0.innerHTML = `<a href="world-cup-2026.html"><strong>World Cup 2026 hub</strong></a><br><span class="text-muted" style="font-size:0.78rem">Groups, ratings, everything</span>`
links.appendChild(l0)
const l1 = document.createElement("div"); l1.style.marginTop = "0.7rem"
l1.innerHTML = `<a href="world-cup-simulator.html"><strong>Simulator</strong></a><br><span class="text-muted" style="font-size:0.78rem">Lock results, re-run the odds</span>`
links.appendChild(l1)
const l2 = document.createElement("div"); l2.style.marginTop = "0.7rem"
l2.innerHTML = `<a href="world-cup-matches.html"><strong>Match Predictions</strong></a><br><span class="text-muted" style="font-size:0.78rem">All 72 group-stage fixtures</span>`
links.appendChild(l2)
inner.appendChild(links)
inner.appendChild(tableSource({
source: {
href: "https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026",
text: "FIFA published schedule"
},
license: "CC BY 4.0",
asAt: "Static",
hint: "Capacities in World Cup configuration · all 104 matches"
}))
return inner
}