DEV Community

Vigoss Luke
Vigoss Luke

Posted on • Originally published at wc26-map.pages.dev

I Built a World Cup 2026 Interactive Venue Map with Leaflet.js — Here's the Code

I Built a World Cup 2026 Interactive Venue Map with Leaflet.js — Here's the Code

World Cup 2026 kicks off in 3 days. 48 teams, 16 stadiums across the US, Canada, and Mexico. I wanted to see where all the matches were happening — not on a static image, not in a slow third-party iframe, but on a fast, zoomable, clickable map that I actually built myself.

So I spent an afternoon with Leaflet.js and Cloudflare Pages. Here's what came out of it.

Why Another Map?

I searched "World Cup 2026 stadiums map" and found:

  • A Mapme embed that took 4 seconds to load
  • A ZeeMaps link with ads all over it
  • A bunch of JPG images with tiny text

None of them let you click a stadium and see all the matches happening there. None were fast. None felt like they were built by someone who actually wanted to use them.

So I built my own. The goal: load in under 500ms, run on a phone, and let you tap any stadium to see every match.

The Stack: Leaflet.js, JSON, and Nothing Else

No React. No Next.js. No API. No backend.

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
Enter fullscreen mode Exit fullscreen mode

That's it. Two CDN links, a single HTML file, a JSON blob of stadiums and matches, and Cloudflare Pages to serve it.

The Stadium Data

I structured everything as a flat JSON array. Each stadium has coordinates, capacity, and its match list:

const stadiums = [
  {
    name: "MetLife Stadium",
    city: "East Rutherford, NJ",
    coords: [40.8136, -74.0744],
    capacity: 82500,
    matches: [
      { date: "2026-07-19", stage: "Final", time: "15:00 ET" },
      { date: "2026-06-13", stage: "Group C", teams: "Brazil vs Morocco" },
      { date: "2026-06-22", stage: "Group I", teams: "Norway vs Senegal" },
      { date: "2026-07-05", stage: "Round of 32", teams: "TBD" },
      { date: "2026-07-05", stage: "Round of 16", teams: "TBD" }
    ]
  },
  // ... 15 more stadiums
];
Enter fullscreen mode Exit fullscreen mode

The JSON is about 4KB. That's the entire data layer. No database, no API calls, nothing to go down.

The Map Init

Leaflet makes this embarrassingly simple:

const map = L.map('map').setView([39.8, -98.5], 4);

L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
  attribution: '&copy; OpenStreetMap contributors',
  maxZoom: 18
}).addTo(map);
Enter fullscreen mode Exit fullscreen mode

Centered on middle America at zoom 4 so all three countries fit on screen. CartoDB's light tiles keep it clean — no visual noise competing with the stadium markers.

Adding the Markers

For each stadium, I create a custom marker with a football emoji and bind a popup:

stadiums.forEach(stadium => {
  const icon = L.divIcon({
    html: '',
    className: 'stadium-marker',
    iconSize: [32, 32],
    iconAnchor: [16, 16]
  });

  const matchList = stadium.matches.map(m =>
    `<div class="match-row">
      <span class="match-date">${m.date}</span>
      <span class="match-stage">${m.stage}</span>
      <span class="match-teams">${m.teams || m.stage}</span>
    </div>`
  ).join('');

  const marker = L.marker(stadium.coords, { icon })
    .bindPopup(`
      <div class="stadium-popup">
        <h3>${stadium.name}</h3>
        <p class="stadium-location">${stadium.city} · ${stadium.capacity.toLocaleString()} seats</p>
        <div class="match-schedule">${matchList}</div>
        <a class="popup-link" href="https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/scores-fixtures" target="_blank">
          Full schedule →
        </a>
      </div>
    `)
    .addTo(map);
});
Enter fullscreen mode Exit fullscreen mode

The divIcon with an emoji is a cheat code — no custom image assets, no sprite sheet, just one character that renders perfectly at any zoom level.

Mobile Matters

The one CSS tweak that made a real difference:

.stadium-marker {
  font-size: 28px;
  text-align: center;
  line-height: 32px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.leaflet-popup-content {
  min-width: 240px;
  max-width: 320px;
  font-size: 14px;
}

.match-row {
  display: flex;
  justify-content: space-between;
  padding: 4px 0;
  border-bottom: 1px solid #eee;
  font-size: 13px;
}
Enter fullscreen mode Exit fullscreen mode

On desktop, it's a map. On a phone, it's still a functional tap-to-explore experience. The 32×32px tap target on markers is right at the recommended minimum — I didn't want to inflate them and lose precision when markers are close.

What I Learned

You don't need a framework to ship a useful interactive tool in an afternoon.

The entire project is:

  • 1 HTML file (147 lines)
  • 1 JSON data file (4KB)
  • 2 CDN links
  • Deployed to Cloudflare Pages in 30 seconds

No build step. No npm install. No state management to debug. Just a map that loads fast and does exactly one thing well.

The real lesson: Leaflet.js in 2026 is still the best tool for this kind of project. It hasn't had a major version bump in years, and that's a feature — the API is stable, the docs are complete, and it just works.

The Bigger Picture

I've been on a kick lately of building lightweight, single-purpose tools. If you're into that kind of thing, I've also been writing about:

  • TokenCut — practical ways to cut your AI coding token costs by 30-50%, including a breakdown of why Markdown is 3-8x more token-efficient than HTML for LLM pipelines
  • MarkItDown Pro — batch-converting hundreds of documents to clean Markdown for LLM ingestion, using Microsoft's open-source converter

The common thread: build small, ship fast, and let the tool do one thing really well.

World Cup kicks off June 11. If you want to build your own map, grab the full code from the companion repo below. You've got 3 days — that's 2.5 more than you need.

GitHub: github.com/Jakeshadow/world-cup-venue-map
Live map: wc26-map.pages.dev
More tools: tokencut.org · markitdown-pro.com

Top comments (0)