agentleFS
barisariburnubarisariburnu/skills-collection/skills/maplibre-gl-js/skill.md

maplibre-gl-js

Skill1 starsChanged 10 months agoMITView on GitHub

Expert guidance for Maplibre GL JS - an open-source TypeScript library for rendering interactive maps using WebGL. Use when users need help with map initialization, styling, layers, sources, markers, popups, controls, data visualization, or interactive features.

  • Installs packages

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 2 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill when users ask about:

    line 12
  2. Distinctive:

    If you're using CSP, you may need to add the following directives for MapLibre GL JS:

    line 118

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
skill.md

Maplibre GL JS Expert

Expert guidance for Maplibre GL JS - a powerful, open-source TypeScript library that uses WebGL to render interactive maps from vector tiles.

When to Use This Skill

Use this skill when users ask about:

  • Setup & Installation: Installing Maplibre GL JS, basic map initialization, framework integration
  • Map Configuration: Map options, camera controls, bounds, projections
  • Styling: Map styles, style specification, themes, custom styling
  • Layers: Adding/removing layers, layer types (fill, line, symbol, circle, heatmap, etc.)
  • Sources: Vector tiles, GeoJSON, raster sources, real-time data
  • Markers & Popups: Adding markers, custom markers, popups, tooltips
  • Interactivity: Click events, hover effects, feature queries, user interactions
  • Controls: Zoom, navigation, scale, geolocate, custom controls
  • Data Visualization: Heatmaps, clustering, 3D extrusions, data-driven styling
  • Performance: Optimization, large datasets, vector tiles
  • Integration: React, Vue, Angular, TypeScript usage

Core Concepts

What is Maplibre GL JS?

Maplibre GL JS is an open-source TypeScript library that:

  • ✅ Renders interactive maps using WebGL
  • ✅ Uses vector tiles for efficient, scalable maps
  • ✅ Supports custom styling via MapLibre Style Spec
  • ✅ Provides smooth animations and camera transitions
  • ✅ Works on web, mobile, and desktop platforms
  • ✅ Fork of Mapbox GL JS (v1.x), fully community-driven

Key Features

  • Vector Tiles: Efficient, scalable map rendering
  • WebGL Rendering: Hardware-accelerated graphics
  • Style Specification: JSON-based styling system
  • Interactive: Click, hover, drag, and touch events
  • Data-Driven: Style based on feature properties
  • 3D Support: Terrain, buildings, custom 3D models
  • Open Source: MIT licensed, no API keys required

Installation

Using npm/bun

# Install with bun
bun add maplibre-gl

# Or with npm
npm install maplibre-gl

TypeScript Types are included by default - no separate @types/maplibre-gl package needed.

CSS Import (with Bundlers)

import "maplibre-gl/dist/maplibre-gl.css";

Using CDN

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Maplibre GL JS</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- Maplibre GL JS CSS -->
    <link
      href="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css"
      rel="stylesheet"
    />

    <!-- Maplibre GL JS -->
    <script src="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js"></script>

    <style>
      body {
        margin: 0;
        padding: 0;
      }
      #map {
        position: absolute;
        top: 0;
        bottom: 0;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>

    <script>
      const map = new maplibregl.Map({
        container: "map",
        style: "https://demotiles.maplibre.org/style.json",
        center: [0, 0],
        zoom: 1,
      });
    </script>
  </body>
</html>

Content Security Policy (CSP)

If you're using CSP, you may need to add the following directives for MapLibre GL JS:

Content-Security-Policy: script-src 'self' 'unsafe-eval' 'unsafe-inline' blob: data:; worker-src 'self' blob: data:; connect-src 'self' blob: data:; img-src 'self' blob: data:; font-src 'self' blob: data:; style-src 'self' 'unsafe-inline';

Quick Start

Basic Map Setup

import maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";

const map = new maplibregl.Map({
  container: "map", // div id
  style: "https://demotiles.maplibre.org/style.json", // map style
  center: [-74.5, 40], // starting position [lng, lat]
  zoom: 9, // starting zoom
});

Map Container

HTML:

<div id="map" style="width: 100%; height: 500px;"></div>

Important: The map container must have a defined width and height.

Map Options

const map = new maplibregl.Map({
  container: "map",
  style: "https://demotiles.maplibre.org/style.json",

  // Initial view
  center: [lng, lat],
  zoom: 12,
  bearing: 0, // rotation (0-360)
  pitch: 0, // tilt (0-60)

  // Bounds (alternative to center/zoom)
  // bounds: [[-74.5, 40], [-73.5, 41]],

  // Interaction
  interactive: true,
  dragPan: true,
  dragRotate: true,
  scrollZoom: true,
  doubleClickZoom: true,
  touchZoomRotate: true,
  touchPitch: true,
  keyboard: true,

  // Limits
  minZoom: 0,
  maxZoom: 22,
  minPitch: 0,
  maxPitch: 60,
  maxBounds: [
    [-180, -90],
    [180, 90],
  ], // restrict panning

  // Performance
  preserveDrawingBuffer: false,
  antialias: false,

  // Hash in URL
  hash: false, // set to true for #map=zoom/lat/lng

  // Terrain
  terrain: {
    source: "terrain-source",
    exaggeration: 1.5,
  },
});

Working with Styles

Using Pre-made Styles

// Maplibre demo tiles (free)
const map = new maplibregl.Map({
  style: "https://demotiles.maplibre.org/style.json",
});

// Custom style URL
const map = new maplibregl.Map({
  style: "https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY",
});

Inline Style Object

const map = new maplibregl.Map({
  container: "map",
  style: {
    version: 8,
    sources: {
      "osm-tiles": {
        type: "raster",
        tiles: [
          "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
          "https://b.tile.openstreetmap.org/{z}/{x}/{y}.png",
          "https://c.tile.openstreetmap.org/{z}/{x}/{y}.png",
        ],
        tileSize: 256,
        attribution: "© OpenStreetMap contributors",
      },
    },
    layers: [
      {
        id: "osm-layer",
        type: "raster",
        source: "osm-tiles",
      },
    ],
  },
  center: [0, 0],
  zoom: 2,
});

Changing Style Dynamically

// Load new style
map.setStyle("https://demotiles.maplibre.org/style.json");

// Wait for style to load
map.on("style.load", () => {
  console.log("Style loaded");
});

Camera Controls

Jump (Instant)

map.jumpTo({
  center: [-74.5, 40],
  zoom: 12,
  bearing: 45,
  pitch: 30,
});

Ease (Animated)

map.easeTo({
  center: [-74.5, 40],
  zoom: 12,
  duration: 2000, // milliseconds
  easing: (t) => t, // linear easing
});

Fly (Curved Path)

map.flyTo({
  center: [-74.5, 40],
  zoom: 12,
  speed: 1.2, // animation speed
  curve: 1, // curve intensity
  essential: true, // animation runs even if prefers-reduced-motion
});

Fit Bounds

// Fit map to bounding box
map.fitBounds(
  [
    [-74.5, 40], // southwest
    [-73.5, 41], // northeast
  ],
  {
    padding: 20, // pixels
    maxZoom: 15,
    duration: 1000,
  }
);

Get Current Camera

const center = map.getCenter(); // LngLat object
const zoom = map.getZoom();
const bearing = map.getBearing();
const pitch = map.getPitch();

console.log(`Center: ${center.lng}, ${center.lat}`);
console.log(`Zoom: ${zoom}`);

Adding Markers

Basic Marker

import maplibregl from "maplibre-gl";

const marker = new maplibregl.Marker().setLngLat([-74.5, 40]).addTo(map);

Custom Color

const marker = new maplibregl.Marker({ color: "#FF0000" })
  .setLngLat([-74.5, 40])
  .addTo(map);

Custom HTML Marker

// Create custom element
const el = document.createElement("div");
el.className = "custom-marker";
el.style.backgroundImage = "url(/marker-icon.png)";
el.style.width = "32px";
el.style.height = "32px";
el.style.backgroundSize = "100%";

const marker = new maplibregl.Marker({ element: el })
  .setLngLat([-74.5, 40])
  .addTo(map);

Draggable Marker

const marker = new maplibregl.Marker({ draggable: true })
  .setLngLat([-74.5, 40])
  .addTo(map);

// Listen to drag events
marker.on("dragend", () => {
  const lngLat = marker.getLngLat();
  console.log(`Marker moved to: ${lngLat.lng}, ${lngLat.lat}`);
});

Marker with Popup

const popup = new maplibregl.Popup({ offset: 25 }).setHTML(
  "<h3>Location Name</h3><p>Description here</p>"
);

const marker = new maplibregl.Marker()
  .setLngLat([-74.5, 40])
  .setPopup(popup)
  .addTo(map);

Remove Marker

marker.remove();

Working with Popups

Basic Popup

const popup = new maplibregl.Popup()
  .setLngLat([-74.5, 40])
  .setHTML("<h3>Hello World!</h3>")
  .addTo(map);

Popup Options

const popup = new maplibregl.Popup({
  closeButton: true,
  closeOnClick: true,
  closeOnMove: false,
  anchor: "bottom", // 'top', 'bottom', 'left', 'right'
  offset: 25, // pixels
  className: "custom-popup",
  maxWidth: "300px",
})
  .setLngLat([-74.5, 40])
  .setHTML("<h3>Popup Content</h3>")
  .addTo(map);

Popup on Click

map.on("click", "poi-layer", (e) => {
  const coordinates = e.features[0].geometry.coordinates.slice();
  const description = e.features[0].properties.description;

  new maplibregl.Popup().setLngLat(coordinates).setHTML(description).addTo(map);
});

// Change cursor on hover
map.on("mouseenter", "poi-layer", () => {
  map.getCanvas().style.cursor = "pointer";
});

map.on("mouseleave", "poi-layer", () => {
  map.getCanvas().style.cursor = "";
});

Adding Sources and Layers

GeoJSON Source

map.on("load", () => {
  // Add source
  map.addSource("points", {
    type: "geojson",
    data: {
      type: "FeatureCollection",
      features: [
        {
          type: "Feature",
          geometry: {
            type: "Point",
            coordinates: [-74.5, 40],
          },
          properties: {
            title: "Location 1",
            description: "Description here",
          },
        },
      ],
    },
  });

  // Add layer
  map.addLayer({
    id: "points-layer",
    type: "circle",
    source: "points",
    paint: {
      "circle-radius": 8,
      "circle-color": "#FF0000",
      "circle-stroke-width": 2,
      "circle-stroke-color": "#FFFFFF",
    },
  });
});

Vector Tile Source

map.addSource("vector-tiles", {
  type: "vector",
  tiles: ["https://example.com/tiles/{z}/{x}/{y}.pbf"],
  minzoom: 0,
  maxzoom: 14,
});

map.addLayer({
  id: "buildings",
  type: "fill-extrusion",
  source: "vector-tiles",
  "source-layer": "building", // layer name in vector tile
  paint: {
    "fill-extrusion-color": "#aaa",
    "fill-extrusion-height": ["get", "height"],
    "fill-extrusion-base": ["get", "min_height"],
    "fill-extrusion-opacity": 0.6,
  },
});

MapLibre Tiles (MLT) Encoding

MapLibre GL JS supports MapLibre Tiles (MLT) encoding for vector sources:

map.addSource("vector-tiles-mlt", {
  type: "vector",
  tiles: ["https://example.com/tiles/{z}/{x}/{y}.mlt"],
  encoding: "mlt", // Use MLT encoding
  minzoom: 0,
  maxzoom: 14,
});

map.addLayer({
  id: "buildings-mlt",
  type: "fill-extrusion",
  source: "vector-tiles-mlt",
  "source-layer": "building",
  paint: {
    "fill-extrusion-color": "#aaa",
    "fill-extrusion-height": ["get", "height"],
    "fill-extrusion-base": ["get", "min_height"],
    "fill-extrusion-opacity": 0.6,
  },
});

Raster Source

map.addSource("satellite", {
  type: "raster",
  tiles: ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"],
  tileSize: 256,
});

map.addLayer({
  id: "satellite-layer",
  type: "raster",
  source: "satellite",
  paint: {
    "raster-opacity": 0.8,
  },
});

Update GeoJSON Source

const source = map.getSource("points");
if (source && source.type === "geojson") {
  source.setData({
    type: "FeatureCollection",
    features: newFeatures,
  });
}

Layer Types

Circle Layer

map.addLayer({
  id: "circles",
  type: "circle",
  source: "points",
  paint: {
    "circle-radius": 10,
    "circle-color": "#007cbf",
    "circle-opacity": 0.8,
    "circle-stroke-width": 2,
    "circle-stroke-color": "#fff",
  },
});

Symbol Layer (Icons/Text)

// First, load the image
map.loadImage("/marker-icon.png", (error, image) => {
  if (error) throw error;
  map.addImage("custom-marker", image);

  // Then add the layer
  map.addLayer({
    id: "symbols",
    type: "symbol",
    source: "points",
    layout: {
      "icon-image": "custom-marker",
      "icon-size": 1,
      "icon-allow-overlap": true,
      "text-field": ["get", "title"],
      "text-font": ["Open Sans Semibold"],
      "text-offset": [0, 1.5],
      "text-anchor": "top",
    },
    paint: {
      "text-color": "#000",
      "text-halo-color": "#fff",
      "text-halo-width": 2,
    },
  });
});

Line Layer

map.addLayer({
  id: "route",
  type: "line",
  source: "route-source",
  layout: {
    "line-join": "round",
    "line-cap": "round",
  },
  paint: {
    "line-color": "#888",
    "line-width": 8,
    "line-opacity": 0.8,
  },
});

Fill Layer

map.addLayer({
  id: "polygon",
  type: "fill",
  source: "polygon-source",
  paint: {
    "fill-color": "#088",
    "fill-opacity": 0.4,
    "fill-outline-color": "#000",
  },
});

Heatmap Layer

map.addLayer({
  id: "heatmap",
  type: "heatmap",
  source: "earthquakes",
  paint: {
    "heatmap-weight": [
      "interpolate",
      ["linear"],
      ["get", "magnitude"],
      0,
      0,
      6,
      1,
    ],
    "heatmap-intensity": ["interpolate", ["linear"], ["zoom"], 0, 1, 9, 3],
    "heatmap-color": [
      "interpolate",
      ["linear"],
      ["heatmap-density"],
      0,
      "rgba(0,0,255,0)",
      0.1,
      "royalblue",
      0.3,
      "cyan",
      0.5,
      "lime",
      0.7,
      "yellow",
      1,
      "red",
    ],
    "heatmap-radius": 20,
    "heatmap-opacity": 0.8,
  },
});

Event Handling

Map Events

// Map loaded
map.on("load", () => {
  console.log("Map loaded");
});

// Map moved
map.on("move", () => {
  console.log("Map is moving");
});

// Zoom changed
map.on("zoom", () => {
  console.log(`Zoom level: ${map.getZoom()}`);
});

// Map clicked
map.on("click", (e) => {
  console.log(`Clicked at: ${e.lngLat.lng}, ${e.lngLat.lat}`);
});

Layer Events

// Click on layer
map.on("click", "poi-layer", (e) => {
  const feature = e.features[0];
  console.log("Clicked feature:", feature.properties);
});

// Hover on layer
map.on("mouseenter", "poi-layer", (e) => {
  map.getCanvas().style.cursor = "pointer";

  // Optional: highlight feature
  if (e.features.length > 0) {
    const feature = e.features[0];
    map.setFeatureState({ source: "points", id: feature.id }, { hover: true });
  }
});

map.on("mouseleave", "poi-layer", () => {
  map.getCanvas().style.cursor = "";
});

Controls

Navigation Control

map.addControl(new maplibregl.NavigationControl(), "top-right");

Scale Control

map.addControl(
  new maplibregl.ScaleControl({
    maxWidth: 100,
    unit: "metric", // or 'imperial'
  }),
  "bottom-left"
);

Fullscreen Control

map.addControl(new maplibregl.FullscreenControl(), "top-right");

Geolocate Control

const geolocate = new maplibregl.GeolocateControl({
  positionOptions: {
    enableHighAccuracy: true,
  },
  trackUserLocation: true,
  showUserHeading: true,
});

map.addControl(geolocate, "top-right");

// Trigger on load
map.on("load", () => {
  geolocate.trigger();
});

Custom Control

class CustomControl {
  onAdd(map) {
    this._map = map;
    this._container = document.createElement("div");
    this._container.className = "maplibregl-ctrl";

    const button = document.createElement("button");
    button.textContent = "Custom Action";
    button.onclick = () => {
      console.log("Custom control clicked");
    };

    this._container.appendChild(button);
    return this._container;
  }

  onRemove() {
    this._container.parentNode.removeChild(this._container);
    this._map = undefined;
  }
}

map.addControl(new CustomControl(), "top-left");

Best Practices

Wait for Map Load

// ✅ Good: Wait for map to load
map.on("load", () => {
  map.addSource("data", {
    /* ... */
  });
  map.addLayer({
    /* ... */
  });
});

// ❌ Bad: Don't add layers immediately
map.addLayer({
  /* ... */
}); // May fail if map isn't loaded

Remove Event Listeners

function handleClick(e) {
  console.log("Clicked:", e.lngLat);
}

// Add listener
map.on("click", handleClick);

// Remove when done
map.off("click", handleClick);

Clean Up on Unmount

// React example
useEffect(() => {
  const map = new maplibregl.Map({
    /* ... */
  });

  return () => {
    map.remove(); // Clean up
  };
}, []);

Performance: Use Clustering

map.addSource("earthquakes", {
  type: "geojson",
  data: "/earthquakes.geojson",
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50,
});

// Cluster circles
map.addLayer({
  id: "clusters",
  type: "circle",
  source: "earthquakes",
  filter: ["has", "point_count"],
  paint: {
    "circle-color": [
      "step",
      ["get", "point_count"],
      "#51bbd6",
      100,
      "#f1f075",
      750,
      "#f28cb1",
    ],
    "circle-radius": ["step", ["get", "point_count"], 20, 100, 30, 750, 40],
  },
});

// Cluster count
map.addLayer({
  id: "cluster-count",
  type: "symbol",
  source: "earthquakes",
  filter: ["has", "point_count"],
  layout: {
    "text-field": "{point_count_abbreviated}",
    "text-font": ["DIN Offc Pro Medium"],
    "text-size": 12,
  },
});

Common Patterns

Loading Indicator

const loader = document.getElementById("loader");

map.on("dataloading", () => {
  loader.style.display = "block";
});

map.on("idle", () => {
  loader.style.display = "none";
});

Query Features

// Query rendered features at a point
map.on("click", (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ["poi-layer"],
  });

  if (features.length > 0) {
    console.log("Found features:", features);
  }
});

// Query all features from a source
const features = map.querySourceFeatures("my-source", {
  sourceLayer: "my-source-layer",
  filter: ["==", "type", "restaurant"],
});

Reference Documentation

For detailed API reference, advanced patterns, and examples:

  • API Reference: Complete Map, Marker, Popup API
  • Examples: Common use cases and patterns

Troubleshooting

Map Not Displaying

// Check container has dimensions
#map {
  width: 100%;
  height: 500px; /* Must have explicit height */
}

// Check style is loaded
map.on('error', (e) => {
  console.error('Map error:', e);
});

Images Not Loading

// Load image before using in layer
map.loadImage("/icon.png", (error, image) => {
  if (error) {
    console.error("Failed to load image:", error);
    return;
  }
  map.addImage("custom-icon", image);
});

TypeScript Errors

// Import types
import maplibregl, { Map, Marker, Popup } from "maplibre-gl";

// Type the map
const map: Map = new maplibregl.Map({
  /* ... */
});

Quick Reference

// Installation
bun add maplibre-gl

// Basic map
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://demotiles.maplibre.org/style.json',
  center: [0, 0],
  zoom: 2
});

// Add marker
new maplibregl.Marker()
  .setLngLat([0, 0])
  .addTo(map);

// Add popup
new maplibregl.Popup()
  .setLngLat([0, 0])
  .setHTML('<h3>Hello</h3>')
  .addTo(map);

// Add GeoJSON
map.on('load', () => {
  map.addSource('data', {
    type: 'geojson',
    data: geojsonData
  });

  map.addLayer({
    id: 'layer',
    type: 'circle',
    source: 'data'
  });
});

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.