FIFA 2026 World Cup
  • Home

FIFA 2026 World Cup

Schedule and results

This page rebuilds about every 10 minutes during match windows, with live scores from football-data.org’s paid API tier. An in-progress match shows a running scoreline (e.g. “1–0 (live)”). Because the page is a periodic snapshot, the scoreline can trail the live match by a few minutes.

Last rebuilt: June 12, 2026 at 03:08 EDT

teams   = transpose(teams_data).sort((a, b) => a.name.localeCompare(b.name))
matches = transpose(matches_data)
  .map(d => ({...d, utc_date: new Date(d.utc_date_str)}))
  .sort((a, b) => a.utc_date - b.utc_date)
now = new Date()
localTz = Intl.DateTimeFormat().resolvedOptions().timeZone
tzOptions = new Map([
  [`Your local time (${localTz})`, localTz],
  ["UTC", "UTC"],
  ["US Eastern",  "America/New_York"],
  ["US Central",  "America/Chicago"],
  ["US Mountain", "America/Denver"],
  ["US Pacific",  "America/Los_Angeles"],
  ["Mexico City", "America/Mexico_City"],
  ["São Paulo",   "America/Sao_Paulo"],
  ["London",      "Europe/London"],
  ["Central Europe (Paris/Berlin/Madrid)", "Europe/Paris"],
  ["Athens / Cairo", "Europe/Athens"],
  ["Moscow",      "Europe/Moscow"],
  ["Dubai",       "Asia/Dubai"],
  ["India",       "Asia/Kolkata"],
  ["Tokyo / Seoul", "Asia/Tokyo"],
  ["Beijing / Singapore", "Asia/Shanghai"],
  ["Sydney",      "Australia/Sydney"],
  ["Auckland",    "Pacific/Auckland"]
])
viewof selectedTz = Inputs.select(tzOptions, {label: "Time zone", value: localTz})
tz = selectedTz

Pick your time zone above — both tabs update. (Defaults to your device’s time zone.)

  • By team
  • By date
viewof selectedTeamName = Inputs.select(
  teams.map(t => t.name),
  {label: "Team", value: "Bosnia-Herzegovina"}
)
selectedTeam = teams.find(t => t.name === selectedTeamName)
teamMatches  = matches.filter(
  m => m.home_team_id === selectedTeam.id || m.away_team_id === selectedTeam.id
)
nextMatch    = teamMatches.find(
  m => (m.utc_date >= now || liveStatuses.has(m.status)) &&
       !finishedStatuses.has(m.status) &&
       !inactiveStatuses.has(m.status)
)
pastMatches  = teamMatches.filter(
  m => m.utc_date < now &&
       !liveStatuses.has(m.status) &&
       !inactiveStatuses.has(m.status)
)
upcoming     = teamMatches.filter(
  m => (m.utc_date >= now || liveStatuses.has(m.status)) &&
       !finishedStatuses.has(m.status) &&
       !inactiveStatuses.has(m.status)
)
html`<div class="team-header">
  <img src="${selectedTeam.crest_url}" alt="${selectedTeam.name} crest" class="crest-lg">
  <div>
    <h2 class="team-name">${selectedTeam.name}</h2>
    <div class="team-meta">${selectedTeam.tla} · ${groupLabelFor(selectedTeam)}</div>
  </div>
</div>`

Next match

nextMatchCard(nextMatch, selectedTeam)

Upcoming matches

upcoming.length > 0
  ? scheduleTable(upcoming, selectedTeam)
  : html`<p class="muted">No remaining matches on the schedule.</p>`

Past results

pastMatches.length > 0
  ? scheduleTable(pastMatches, selectedTeam)
  : html`<p class="muted">${selectedTeam.name} has not played any matches yet.</p>`
matchesByDay = d3.groups(
  matches,
  m => dayKey(m.utc_date)
).sort(([a], [b]) => a.localeCompare(b))
viewof dayChoice = Inputs.select(
  ["All days", ...matchesByDay.map(([d]) => d)],
  {label: "Day"}
)
dayList = dayChoice === "All days"
  ? matchesByDay
  : matchesByDay.filter(([d]) => d === dayChoice)
html`${dayList.map(([day, dayMatches]) => html`
  <section class="day-block">
    <h3 class="day-heading">${formatDayHeading(day)}</h3>
    ${matchListTable(dayMatches)}
  </section>
`)}`
liveStatuses     = new Set(["IN_PLAY","PAUSED","EXTRA_TIME","PENALTY_SHOOTOUT"])
finishedStatuses = new Set(["FINISHED","AWARDED"])
inactiveStatuses = new Set(["CANCELLED","POSTPONED","SUSPENDED"])

teamById = new Map(teams.map(t => [t.id, t]))

function crestFor(name, id) {
  const t = teamById.get(id);
  return t
    ? html`<img src="${t.crest_url}" alt="" class="crest-sm"> ${name}`
    : html`<span class="tbd">${name ?? "TBD"}</span>`;
}

function groupLabelFor(team) {
  const m = matches.find(
    x => (x.home_team_id === team.id || x.away_team_id === team.id) &&
         x.stage === "GROUP_STAGE"
  );
  return m && m.group ? `Group ${m.group.replace("GROUP_", "")}` : "—";
}

function stageLabel(stage, group) {
  if (!stage) return "";
  if (stage === "GROUP_STAGE") return group ? `Group ${group.replace("GROUP_", "")}` : "Group stage";
  return stage.replace(/_/g, " ").toLowerCase()
    .replace(/\b\w/g, c => c.toUpperCase());
}

// All times follow the selected `tz` (see the time-zone picker). Building a
// fresh formatter per call keeps things reactive when `tz` changes.
function dayKey(d) {
  // e.g. "2026-06-12" — the calendar date in the selected zone.
  return new Intl.DateTimeFormat("en-CA", {timeZone: tz}).format(d);
}

function formatDate(d) {
  return d.toLocaleString("en-US", {
    weekday: "long", month: "long", day: "numeric", year: "numeric",
    hour: "numeric", minute: "2-digit", timeZone: tz, timeZoneName: "short"
  });
}
function formatDateShort(d) {
  return d.toLocaleDateString("en-US", {weekday: "short", month: "short", day: "numeric", timeZone: tz});
}
function formatTime(d) {
  return d.toLocaleTimeString("en-US", {hour: "numeric", minute: "2-digit", timeZone: tz, timeZoneName: "short"});
}
function formatDayHeading(isoDay) {
  // isoDay is already the selected-zone calendar date; format it as UTC midday
  // so the weekday/month/day read back out without a second timezone shift.
  const d = new Date(isoDay + "T12:00:00Z");
  return d.toLocaleDateString("en-US", {weekday: "long", month: "long", day: "numeric", year: "numeric", timeZone: "UTC"});
}

function statusBadge(m) {
  if (liveStatuses.has(m.status))            return html`<span class="badge live">LIVE</span>`;
  if (m.status === "POSTPONED")              return html`<span class="badge postponed">Postponed</span>`;
  if (m.status === "CANCELLED")              return html`<span class="badge cancelled">Cancelled</span>`;
  if (m.status === "SUSPENDED")              return html`<span class="badge suspended">Suspended</span>`;
  return "";
}

function nextMatchCard(m, team) {
  if (!m) {
    return html`<div class="next-card empty">No upcoming matches on the schedule.</div>`;
  }
  const isHome = m.home_team_id === team.id;
  const opp_id = isHome ? m.away_team_id : m.home_team_id;
  const opp_t  = teamById.get(opp_id);
  const oppName = isHome ? m.away_team : m.home_team;
  return html`
    <div class="next-card">
      <div class="next-date">${formatDate(m.utc_date)}</div>
      <div class="next-matchup">
        <div class="opponent">
          ${opp_t ? html`<img src="${opp_t.crest_url}" class="crest-md" alt="">` : ""}
          <div>
            <div class="vs-label">${isHome ? "vs" : "at"}</div>
            <div class="opponent-name">${oppName ?? "TBD"}</div>
          </div>
        </div>
        <div class="next-stage">${stageLabel(m.stage, m.group)}${statusBadge(m)}</div>
      </div>
      ${m.venue ? html`<div class="next-venue">${m.venue}</div>` : ""}
    </div>`;
}

// `score_display` from R is always home–away. On a team page we want the
// selected team's goals first, so the same match reads as a win for one
// side and a loss for the other. Non-numeric strings ("in progress",
// "no score available yet", "postponed", "") pass through unchanged.
function flipScore(s) {
  if (!s) return s;
  const m = s.match(/^(\d+)–(\d+)(\s*\((?:(\d+)–(\d+)\s*PK|live)\))?$/);
  if (!m) return s;
  const [, h, a, suffix, pkh, pka] = m;
  let out = `${a}–${h}`;
  if (suffix) {
    out += pkh !== undefined ? ` (${pka}–${pkh} PK)` : ` (live)`;
  }
  return out;
}

function scheduleTable(rows, team) {
  return html`<table class="schedule">
    <thead>
      <tr>
        <th>Date</th><th>Kick-off</th><th>Opponent</th><th>H/A</th>
        <th>Stage</th><th>Score</th>
      </tr>
    </thead>
    <tbody>
    ${rows.map(m => {
      const isHome = m.home_team_id === team.id;
      const opp_id = isHome ? m.away_team_id : m.home_team_id;
      const oppName = isHome ? m.away_team : m.home_team;
      const score = isHome ? m.score_display : flipScore(m.score_display);
      return html`<tr>
        <td>${formatDateShort(m.utc_date)}</td>
        <td>${formatTime(m.utc_date)}</td>
        <td>${crestFor(oppName, opp_id)}</td>
        <td>${isHome ? "H" : "A"}</td>
        <td>${stageLabel(m.stage, m.group)}</td>
        <td class="score">${score || ""} ${statusBadge(m)}</td>
      </tr>`;
    })}
    </tbody>
  </table>`;
}

function matchListTable(rows) {
  return html`<table class="day-table">
    <tbody>
    ${rows.map(m => html`<tr>
      <td class="time">${formatTime(m.utc_date)}</td>
      <td class="team home">${crestFor(m.home_team, m.home_team_id)}</td>
      <td class="vs">vs</td>
      <td class="team away">${crestFor(m.away_team, m.away_team_id)}</td>
      <td class="stage">${stageLabel(m.stage, m.group)}</td>
      <td class="score">${m.score_display || ""} ${statusBadge(m)}</td>
    </tr>`)}
    </tbody>
  </table>`;
}

Data: football-data.org · Code: worldcup26 R package