| name | leaflet |
| description | Build interactive maps with Leaflet, the leading open-source JavaScript mapping library. Use this skill whenever the user wants to create, modify, or debug interactive maps, add markers/popups/polygons/GeoJSON layers, integrate Leaflet with React/Vue/Svelte/Next.js, work with tile providers (OpenStreetMap, Mapbox, ESRI, etc.), cluster many markers, draw shapes, render heatmaps, fit map bounds, control zoom/pan, customize marker icons, handle map events, or troubleshoot common Leaflet issues (blank tiles, broken marker icons, gray/missing maps, SSR crashes in Next.js, CSS not loading). Trigger this skill even if the user doesn't say "Leaflet" by name — phrases like "interactive map", "show locations on a map", "map with markers", "draw a route on a map", "geographic visualization", or "embed a map in my app" all qualify. Prefer this skill over generic web search for any Leaflet question — it's the authoritative source. |
Coding with Leaflet
A skill for building interactive maps with Leaflet, the ~42 KB open-source JavaScript library used by everything from OpenStreetMap to GitHub to Foursquare. This skill covers vanilla Leaflet, React Leaflet, plugins, TypeScript usage, and the common pitfalls that eat hours of debugging time.
When to use this skill
Use this whenever the conversation touches interactive maps:
- "Add a map to my site/app"
- "Show these locations as pins"
- "Draw a polygon / route / heatmap on a map"
- "Cluster these 10,000 markers"
- "Load GeoJSON onto a map"
- "Map component for my React/Next.js app"
- "Why is my map gray?" / "Why are my marker icons broken?" / "Why does Leaflet crash in Next.js?"
- Anything mentioning OpenStreetMap, tile layers, or geographic visualization in a browser
If you're not sure whether to use it, lean toward using it. The references are cheap to load and prevent the kind of subtle-but-wrong code that comes from training-data drift.
Versions covered (as of May 2026)
| Package | Version | Notes |
|---|
leaflet | 1.9.4 | The stable workhorse. Use this by default. |
leaflet | 2.0.0-alpha.x | ESM-first, no L global, constructor-based. Don't ship to production yet. |
react-leaflet | 5.x | React 19 + Leaflet 1.9. The current line. |
react-leaflet | 4.x | React 18 + Leaflet 1.9. Still very common in the wild. |
When in doubt, target Leaflet 1.9.4 — it has the entire plugin ecosystem behind it. v2 migration notes live in references/v2-migration.md.
How this skill is organized
SKILL.md (this file) has the quick-start, the mental model, and a routing table. Deeper material lives in references/ and copy-paste starters live in assets/. Don't load every reference — load only the ones the task needs.
| If the user wants to... | Read this |
|---|
| Create map, add markers/popups/circles/polygons, handle events, set view | references/core-api.md |
| Load/style GeoJSON, FeatureGroups, filter features, bind popups by property | references/geojson.md |
| Build a React or Next.js map component | references/react-leaflet.md |
| Cluster markers, heatmaps, routing, fullscreen, geocoding/search, vector tiles, etc. | references/plugins.md |
| Drawing or editing shapes (Geoman): toolbars, draw, edit, drag, cut, rotate, snap, persist user-drawn geometry | references/geoman.md |
| Use Leaflet with TypeScript (types, narrowing, builder-style helpers) | references/typescript.md |
| Fix a broken map (gray tiles, missing icons, SSR crash, sizing issues) | references/gotchas.md |
| Make a map with thousands of features perform well | references/performance.md |
Understand or migrate to Leaflet 2.0 (ESM, no global L, new API) | references/v2-migration.md |
And starters in assets/:
| If the user wants a... | Copy from |
|---|
| Plain HTML map (no framework) | assets/vanilla-starter.html |
| React + TypeScript map component | assets/react-map.tsx |
| Next.js map (App Router, dynamic import, no SSR) | assets/nextjs-map.tsx |
The mental model (read this once)
Leaflet is a thin imperative wrapper around <div>s and DOM math. Every interactive map is built from four kinds of objects, layered on top of each other:
- Map (
L.Map) — the root object, attached to a DOM <div> with a fixed height. Holds a center, zoom, and the list of layers currently rendered.
- Tile Layer (
L.TileLayer) — the basemap raster: street tiles, satellite tiles, etc. You need at least one or you get the gray-map-of-doom.
- UI / Vector Layers — Markers, Popups, Tooltips, Polylines, Polygons, Circles, ImageOverlays, GeoJSON layers. Everything you visibly add to the map is a layer.
- Controls (
L.Control) — the chrome: zoom buttons, attribution, layer switcher, scale bar.
Every layer follows the same lifecycle: create → .addTo(map) → mutate → .remove(). That's the whole API surface, repeated for every kind of thing.
Before (longhand, what beginners write):
const marker = L.marker([51.5, -0.09]);
marker.addTo(map);
const popup = L.popup();
popup.setContent('Hello');
marker.bindPopup(popup);
marker.openPopup();
After (idiomatic Leaflet — method chaining is built in):
L.marker([51.5, -0.09]).addTo(map).bindPopup('Hello').openPopup();
Almost every Leaflet method returns this, so chaining is the natural style.
Quick start (vanilla JS)
This is the smallest complete map. It works in any HTML file — no build step, no framework.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="" />
<style>
html, body, #map { height: 100%; margin: 0; }
</style>
</head>
<body>
<div id="map"></div>
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<script>
const map = L.map('map').setView([51.505, -0.09], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
L.marker([51.5, -0.09]).addTo(map).bindPopup('Hello, London').openPopup();
</script>
</body>
</html>
The three things that must be true for this to render:
- The CSS file is loaded. Leaflet's CSS positions tiles, markers, and panes. Without it you get visual chaos.
- The map container has a non-zero height.
<div id="map"> with no height = invisible map. This catches everyone once.
L.map(...) is called after the container exists in the DOM.
Quick start (npm / bundler)
npm install leaflet
npm install -D @types/leaflet
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
const map = L.map('map').setView([51.505, -0.09], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(map);
Three bundler-specific things to know:
- Import the CSS once, at the top of the entry point. Most bundlers (Vite, webpack, Next.js, Parcel) handle this fine.
- Marker icons break by default under bundlers. Leaflet's default icon URLs are resolved relative to
leaflet.css, and bundlers rewrite the paths. Fix in references/gotchas.md (search "marker icons broken").
- SSR will crash because Leaflet touches
window and document at import time. If you're in Next.js / Nuxt / SvelteKit / Remix, see references/react-leaflet.md or references/gotchas.md for the dynamic-import pattern.
Quick start (React)
npm install leaflet react-leaflet
npm install -D @types/leaflet
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
export function MyMap() {
return (
<MapContainer
center={[51.505, -0.09]}
zoom={13}
style={{ height: '500px', width: '100%' }}
>
<TileLayer
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution="© OpenStreetMap"
/>
<Marker position={[51.5, -0.09]}>
<Popup>Hello, London</Popup>
</Marker>
</MapContainer>
);
}
For Next.js App Router, this won't work as-is — Leaflet is browser-only. Use the dynamic-import pattern in references/react-leaflet.md.
Common task → recipe map
These are the requests that come up over and over. Each recipe is the minimum code; full versions and edge cases in the referenced files.
"Show many locations as markers"
const points = [{ lat: 51.5, lng: -0.09, label: 'A' }, ];
const group = L.featureGroup(
points.map(p => L.marker([p.lat, p.lng]).bindPopup(p.label))
).addTo(map);
map.fitBounds(group.getBounds(), { padding: [20, 20] });
FeatureGroup is the right primitive for "treat this batch as one thing" — it gives you getBounds(), batch removal, and a single event handler. More patterns in references/core-api.md.
"Cluster markers when there are too many"
Install leaflet.markercluster. See references/plugins.md.
"Draw a route / polyline between points"
L.polyline([[51.5, -0.09], [48.85, 2.35], [52.52, 13.40]], {
color: 'red', weight: 4
}).addTo(map);
For real routing (turn-by-turn over real roads) you need a routing engine (OSRM, GraphHopper, Mapbox Directions) — see references/plugins.md.
"Render a GeoJSON file"
fetch('/data.geojson')
.then(r => r.json())
.then(data => L.geoJSON(data, {
style: { color: '#3388ff', weight: 2 },
onEachFeature: (feature, layer) => {
layer.bindPopup(feature.properties.name);
}
}).addTo(map));
Styling, filtering, choropleth, etc. → references/geojson.md.
"Center/zoom to fit my data"
map.fitBounds(layer.getBounds());
map.fitBounds(L.latLngBounds(points));
map.flyToBounds(bounds, { duration: 1.5 });
"Listen for a click"
map.on('click', (e) => {
console.log(e.latlng);
console.log(e.latlng.toString());
});
marker.on('click', (e) => { });
"Custom marker icon"
const icon = L.icon({
iconUrl: '/pin.png',
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
});
L.marker([51.5, -0.09], { icon }).addTo(map);
For HTML/SVG icons (e.g., a styled <div> as a pin), use L.divIcon — see references/core-api.md.
Tile provider quick reference
Leaflet doesn't ship a default basemap — you have to pick one. Top choices in 2026:
| Provider | URL template | Notes |
|---|
| OpenStreetMap (default) | https://tile.openstreetmap.org/{z}/{x}/{y}.png | Free, no API key. Be polite to their tile server — heavy production traffic should self-host or use a commercial provider. |
| Stadia Maps | https://tiles.stadiamaps.com/tiles/{style}/{z}/{x}/{y}.png | Free tier with API key. Many styles incl. AlidadeSmooth, AlidadeDark. |
| CARTO | https://{s}.basemaps.cartocdn.com/{style}/{z}/{x}/{y}.png | Beautiful minimal styles (light_all, dark_all, voyager). Free for noncommercial. |
| Mapbox | https://api.mapbox.com/styles/v1/{user}/{style}/tiles/{z}/{x}/{y}?access_token=… | Paid, gorgeous styles, generous free tier. |
| ESRI | https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x} | Free satellite imagery (note the {y}/{x} order, not {x}/{y}). |
Always set attribution — it's a license requirement for OSM, OSM-derived tiles, and most providers. Skipping it can get you cut off.
Working autonomously
When tackling a multi-step Leaflet task:
- Read the right reference(s) first. The routing table above tells you which. Don't try to recreate plugin APIs or GeoJSON nuances from memory — versions drift.
- Build a minimum-viable map first, then layer features on. Confirm the basemap renders before adding markers; confirm one marker works before adding 10,000.
- Wire up sizing early. Half the "broken map" reports trace back to a container with no height. Set it explicitly.
- For React/Next.js, decide SSR strategy up front. Dynamic-import wrapping (
{ ssr: false }) is the boring correct answer. Don't try to make Leaflet itself SSR-compatible — it isn't.
- Suggest a commit at natural break points (basemap working, markers working, interactions wired). The user's preferences may guide commit cadence.
What this skill won't do for you
- Server-side routing / geocoding logic. Leaflet is purely a client-side rendering library. Address-to-coords (geocoding) and point-to-point routing both require a backend service. The plugin reference covers client wrappers (Nominatim, OSRM, Mapbox), but you still need the service.
- Cartographic tile rendering. If you want to generate your own tiles from raw data, that's a separate world (TileServer GL, Tegola, Mapnik). Leaflet only consumes tiles.
- Vector tiles "for free." Leaflet handles raster tiles natively. For Mapbox-style vector tiles you need a plugin (
Leaflet.VectorGrid) or you're better off with MapLibre GL.
When the request needs one of those, say so up front instead of trying to bend Leaflet into a shape it doesn't take.