Skip to content
Back to skills

Leaflet

ASecurity

Builds interactive maps with Leaflet.js, the open-source JavaScript library that shows tile maps with markers, popups and vector shapes. Use when a user asks to create map-based applications, add markers and popups, draw shapes, handle geolocation, or integrate tile layers in web applications.

  • 142 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added May 29, 2026
developmentjavascripttypescriptgojavabashreactnextjsnodeawsgit

Works with

  • terminal
  • cli
  • api

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add TerminalSkills/skills --skill leaflet --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Leaflet?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Leaflet
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/terminalskills-leaflet/badge)](https://www.skillsdirectory.com/skills/terminalskills-leaflet)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: leaflet
description: >-
  Builds interactive maps with Leaflet.js, the open-source JavaScript library that shows tile maps with markers, popups and vector shapes. Use when a user asks to create map-based applications, add markers and popups, draw shapes, handle geolocation, or integrate tile layers in web applications.
license: Apache-2.0
compatibility: "Leaflet 1.9.4 in any modern browser. The React examples need react-leaflet 5 with React 19 (react-leaflet 4 with React 18)."
metadata:
  author: terminal-skills
  version: "1.1.0"
  category: data-ai
  tags: ["maps", "open-source", "geospatial", "markers", "layers"]
  repository: https://github.com/Leaflet/Leaflet
---
# Leaflet — Lightweight Open-Source Maps

## Overview

Leaflet is a lightweight open-source JavaScript library for interactive maps: raster tile layers, markers, popups, vector shapes, GeoJSON and map events, with clustering and other features added by plugins. It has no API key of its own — the tiles come from a provider you choose, such as OpenStreetMap. In React it is used through react-leaflet, which wraps the same Leaflet objects in components.

The stable release is 1.9.4 (May 2023) and it is what `npm install leaflet` installs. Leaflet 2.0 is still an alpha (2.0.0-alpha.1, August 2025): it is ESM-only and drops the global `L` and the factory functions (`L.marker(latlng)` becomes `new Marker(latlng)`), while react-leaflet 5 and plugins written for the global `L` still require 1.9. Use 1.9.4 unless the user asks for 2.0.

## Instructions

### Installation

```bash
npm install leaflet
npm install -D @types/leaflet                      # TypeScript types (Leaflet 1.9 ships none)

# React 19
npm install react-leaflet react-leaflet-cluster    # react-leaflet 5, cluster 4

# React 18 (the unpinned command above fails there with ERESOLVE)
npm install react-leaflet@4 react-leaflet-cluster@3
```

Without a build step, load the pinned CDN files as in Example 1. The map container needs an explicit height and `leaflet.css` must be loaded: without the height the map is invisible, without the CSS the tiles are scattered over the page.

### Plain JavaScript

```js
import L from "leaflet";
import "leaflet/dist/leaflet.css";

const map = L.map("map").setView([52.3738, 4.888], 13);   // id of a <div> with a height

L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
  maxZoom: 19,
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);

L.marker([52.3738, 4.888]).addTo(map).bindPopup("<b>Amsterdam HQ</b><br>Herengracht 182");
L.circle([52.3738, 4.888], { radius: 1500, color: "#2563eb" }).addTo(map);   // radius in metres
L.polygon([[52.38, 4.87], [52.38, 4.91], [52.36, 4.9]]).addTo(map);

map.on("click", (e) => console.log(e.latlng.lat, e.latlng.lng));
```

Coordinates are `[latitude, longitude]` everywhere in Leaflet. GeoJSON files use `[longitude, latitude]`; `L.geoJSON(data)` converts them, so do not swap GeoJSON coordinates by hand.

### React Integration

```tsx
import { MapContainer, TileLayer, Marker, Popup, GeoJSON } from "react-leaflet";
import MarkerClusterGroup from "react-leaflet-cluster";
import "leaflet/dist/leaflet.css";
import "react-leaflet-cluster/dist/assets/MarkerCluster.css";          // required since cluster 3.0
import "react-leaflet-cluster/dist/assets/MarkerCluster.Default.css";

const osm = {
  url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
};

type Store = { id: string; name: string; address: string; hours: string; lat: number; lng: number };

export default function StoreMap({ stores }: { stores: Store[] }) {
  return (
    <MapContainer center={[40.75, -73.98]} zoom={12}
                  style={{ height: "100vh", width: "100%" }}>
      <TileLayer {...osm} />

      {/* Cluster markers when zoomed out */}
      <MarkerClusterGroup chunkedLoading>
        {stores.map((store) => (
          <Marker key={store.id} position={[store.lat, store.lng]}>
            <Popup>
              <h3>{store.name}</h3>
              <p>{store.address}</p>
              <p>Hours: {store.hours}</p>
            </Popup>
          </Marker>
        ))}
      </MarkerClusterGroup>
    </MapContainer>
  );
}

// GeoJSON boundaries (neighborhoods, delivery zones)
function DeliveryZones({ zones, version }: { zones: GeoJSON.FeatureCollection; version: number }) {
  return (
    <MapContainer center={[40.75, -73.98]} zoom={11} style={{ height: 480 }}>
      <TileLayer {...osm} />
      <GeoJSON
        key={version}   /* `data` is read once; change the key to draw new data */
        data={zones}
        style={(feature) => ({
          fillColor: feature?.properties.active ? "#22c55e" : "#ef4444",
          weight: 2,
          opacity: 0.8,
          fillOpacity: 0.3,
        })}
        onEachFeature={(feature, layer) => {
          layer.bindPopup(`
            <b>${feature.properties.name}</b><br/>
            Orders: ${feature.properties.orderCount}<br/>
            Avg delivery: ${feature.properties.avgDeliveryMin} min
          `);
        }}
      />
    </MapContainer>
  );
}
```

`MapContainer` props other than `children` are read only when the map is created: changing `center` or `zoom` later does nothing. Move the map from a child component with `useMap()` (`map.setView`, `map.flyTo`, `map.fitBounds`).

### Custom Icons and Controls

```tsx
import { useEffect, useRef } from "react";
import { useMap, useMapEvents } from "react-leaflet";
import L from "leaflet";

// Custom marker icon — pass it as <Marker icon={storeIcon} />
const storeIcon = new L.Icon({
  iconUrl: "/icons/store-pin.png",
  iconSize: [32, 32],
  iconAnchor: [16, 32],     // the pixel of the image that sits on the coordinate
  popupAnchor: [0, -32],
});

// Custom map control (e.g., "Locate Me" button) placed in Leaflet's top-right corner
function LocateControl() {
  const map = useMap();
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    // Without this a click on the button is also a click on the map
    if (ref.current) L.DomEvent.disableClickPropagation(ref.current);
  }, []);
  return (
    <div className="leaflet-top leaflet-right">
      <div ref={ref} className="leaflet-control leaflet-bar">
        <a href="#" role="button" title="My location"
           onClick={(e) => { e.preventDefault(); map.locate({ setView: true, maxZoom: 16 }); }}>
          ◎
        </a>
      </div>
    </div>
  );
}

// Map events: any Leaflet event name works as a key
function ClickToPin({ onPin }: { onPin: (latlng: L.LatLng) => void }) {
  useMapEvents({
    click: (e) => onPin(e.latlng),
    locationerror: (e) => console.warn("Location unavailable:", e.message),
  });
  return null;
}
```

`useMap`, `useMapEvents` and `useMapEvent` work only in components rendered inside `<MapContainer>`.

### Default marker icon with bundlers

The blue default marker works in the dev server and turns into a broken image after a production build, because the bundler rewrites the image URL that Leaflet reads from its CSS. Import the images once, before the first map renders:

```ts
// leaflet-icons.ts
import L from "leaflet";
import iconUrl from "leaflet/dist/images/marker-icon.png";
import iconRetinaUrl from "leaflet/dist/images/marker-icon-2x.png";
import shadowUrl from "leaflet/dist/images/marker-shadow.png";

delete (L.Icon.Default.prototype as { _getIconUrl?: unknown })._getIconUrl;
L.Icon.Default.mergeOptions({ iconUrl, iconRetinaUrl, shadowUrl });
```

### Next.js

Leaflet touches `window` when it is imported, so the map must not render on the server. Put the map in its own file and load it from a Client Component:

```tsx
"use client";
import dynamic from "next/dynamic";

const StoreMap = dynamic(() => import("./StoreMap"), { ssr: false, loading: () => <p>Loading map…</p> });
```

`ssr: false` is rejected inside a Server Component, so the file that calls `dynamic` needs `"use client"`; the imported file must `export default` the map component.

## Examples

### Example 1: Office locations on a plain HTML page

**User request:** "Put our three offices on a map on the contact page. It's a static site, no build step."

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
        integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
          integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
  <style>#map { height: 420px; }</style>
</head>
<body>
  <div id="map"></div>
  <script>
    const offices = [
      { name: "Amsterdam HQ", address: "Herengracht 182", lat: 52.3738, lng: 4.8880 },
      { name: "Amsterdam Noord", address: "Asterweg 20", lat: 52.3925, lng: 4.9012 },
      { name: "Utrecht", address: "Oudegracht 112", lat: 52.0907, lng: 5.1214 },
    ];

    const map = L.map("map");
    L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
      maxZoom: 19,
      attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
    }).addTo(map);

    const markers = offices.map((o) =>
      L.marker([o.lat, o.lng]).bindPopup(`<b>${o.name}</b><br>${o.address}`)
    );
    const group = L.featureGroup(markers).addTo(map);
    map.fitBounds(group.getBounds(), { padding: [30, 30] });
  </script>
</body>
</html>
```

Result: a 420 px map zoomed so that all three markers fit (zoom 10 on a desktop-width page), with "© OpenStreetMap contributors" in the corner. Clicking a marker opens a popup with the office name in bold and its address. The `integrity` values are the ones published on the Leaflet download page for 1.9.4; the browser refuses the files if they are changed.

### Example 2: Click to drop a pin, with a "my location" button

**User request:** "In our React app, let people click the map to mark where the pothole is, and add a button that jumps to their own position."

```tsx
import { useState } from "react";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import type L from "leaflet";
import "leaflet/dist/leaflet.css";
import "./leaflet-icons";                    // the default-icon fix above
// osm, LocateControl and ClickToPin are the snippets from the Instructions

export default function ReportMap() {
  const [pins, setPins] = useState<L.LatLng[]>([]);
  return (
    <MapContainer center={[40.75, -73.98]} zoom={12} style={{ height: 480 }}>
      <TileLayer {...osm} />
      <LocateControl />
      <ClickToPin onPin={(latlng) => setPins((prev) => [...prev, latlng])} />
      {pins.map((p, i) => (
        <Marker key={i} position={p}>
          <Popup>Pothole {i + 1}: {p.lat.toFixed(5)}, {p.lng.toFixed(5)}</Popup>
        </Marker>
      ))}
    </MapContainer>
  );
}
```

Result: each click on the map adds a marker at that point; a click on the ◎ button in the top-right corner does not add one. The button asks the browser for the position and recentres the map on it, zooming in to 16 at most; when the user refuses or the browser cannot find a position, the `locationerror` handler logs `Location unavailable: Geolocation error: …` and the map stays where it was.

## Guidelines

1. **Tile provider** — `tile.openstreetmap.org` needs no key but is a donated service with no SLA: visible attribution is mandatory, the URL is `https://tile.openstreetmap.org/{z}/{x}/{y}.png` (the old `{s}.` subdomains are not needed), and heavy or commercial traffic can be blocked without notice. For production traffic or custom styling use a tile provider with a plan (Stadia Maps, MapTiler, Mapbox) or host tiles yourself
2. **Marker clustering** — Use `react-leaflet-cluster` (or `leaflet.markercluster` without React) for 100+ markers; import its two CSS files, otherwise the clusters are unstyled
3. **GeoJSON for regions** — Use GeoJSON layers for boundaries, zones, and polygons; style dynamically based on data, and change the `key` when the data changes
4. **Lazy load** — Leaflet is about 42 KB of JavaScript gzipped plus its CSS; lazy load the map component when the map is below the fold
5. **SSR compatibility** — Leaflet requires `window`; in Next.js load the map with `dynamic(..., { ssr: false })` from a Client Component
6. **No offline tile downloads from OpenStreetMap** — the tile usage policy forbids bulk downloading, prefetching and "download for offline" features; normal browser caching is fine. Offline maps need a provider that allows it or your own tiles
7. **Geolocation** — `map.locate()` works only on HTTPS pages (and `localhost`) and only after the user grants permission; always handle `locationerror`
8. **Escape popup content** — `bindPopup` with a string inserts HTML, so user-supplied names must be escaped or passed as a DOM node; in react-leaflet, `<Popup>` children are ordinary React nodes and are safe
9. **When not to use Leaflet** — it draws raster tiles and SVG/Canvas overlays; for vector tiles, map rotation, 3D terrain or tens of thousands of animated points use a WebGL library such as MapLibre GL JS

Files in this skill

  • SKILL.md4.6 KB
  • _scores.json1.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…