MapLibre GL for Vector Maps

MapLibre GL JS 34,367 (BSD-3-Clause, 6.11.2, github.com/maplibre/maplibre-gl-js (https://github.com/maplibre/maplibre-gl-js 11,758 ), about 11,700 stars) began as an open-source fork of Mapbox 1,919 GL JS 1.x when Mapbox switched to a non-open-source license in December 2020. It styles and labels vector tiles on the GPU, so maps rotate, tilt and restyle without new images. A JSON style names the tile sources and paint rules; the project's demo tiles need no key:

A MapLibre map on the keyless demo tiles with BookNest's sample store locationsHTMLLive
<link rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.11.2/dist/maplibre-gl.css">
<div id="map" style="width:600px;height:280px"></div>
<script type="module">
import * as maplibregl
  from 'https://cdn.jsdelivr.net/npm/maplibre-gl@6.11.2/dist/maplibre-gl.mjs';
const map = new maplibregl.Map({ container: 'map', center: [128, 0], zoom: 1,
  style: 'https://demotiles.maplibre.org/style.json' });            // keyless demo style
const stores = [[116.07, 5.98, 'Kota Kinabalu'], [103.82, 1.35, 'Singapore'],
  [139.69, 35.69, 'Tokyo'], [151.21, -33.87, 'Sydney']];             // sample data
map.on('load', () => {
  map.addSource('stores', { type: 'geojson', data: { type: 'FeatureCollection',
    features: stores.map(([lng, lat, name]) => ({ type: 'Feature', properties: { name },
      geometry: { type: 'Point', coordinates: [lng, lat] } })) } });
  map.addLayer({ id: 'stores', type: 'circle', source: 'stores', paint: { 'circle-radius': 7,
    'circle-color': '#e09a10', 'circle-stroke-width': 2, 'circle-stroke-color': '#fff' } });
});
map.once('idle', () => { window.__done = true; });                   // tiles drawn
</script>
Browser output of Listing 3.162
Browser output of 162

The orange circles are sample store locations, a GeoJSON source in a circle layer over the tiled countries. For production, point the style at tiles you host (PMTiles 3,061 , Martin 3,948 ) or at a provider such as MapTiler 21,580 ; D3 18,089 (D3.js) draws simpler SVG maps without WebGL.

A tilted MapLibre map styled from an inline JSON style: a graticule, a dashed route and BookNest's sample store locations as GeoJSON layersHTMLLive
<!doctype html>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.11.2/dist/maplibre-gl.css">
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  #map { width: 100%; max-width: 600px; height: 320px; }
  .store { font: bold 11px system-ui; color: #5a3a00; background: #fff8e8; padding: 1px 5px; border-radius: 3px; }
  p { max-width: 600px; margin: 4px 6px; font: 11px monospace; }
</style>
<div id="map"></div>
<p id="out"></p>
<script type="module">
import * as maplibregl from 'https://cdn.jsdelivr.net/npm/maplibre-gl@6.11.2/dist/maplibre-gl.mjs';
// This page's sandbox runs only classic blob: workers, so start MapLibre's module worker from one,
// holding early messages until the import finishes ('#.cjs' tells MapLibre to make a classic worker).
const workerSource = `const early = []; const hold = (e) => early.push(e.data); addEventListener('message', hold);
import('https://cdn.jsdelivr.net/npm/maplibre-gl@6.11.2/dist/maplibre-gl-worker.mjs').then(() => {
  removeEventListener('message', hold); for (const data of early) dispatchEvent(new MessageEvent('message', { data })); });`;
maplibregl.setWorkerUrl(URL.createObjectURL(new Blob([workerSource], { type: 'text/javascript' })) + '#.cjs');
const stores = [[139.69, 35.69, 'Tokyo'], [116.07, 5.98, 'Kota Kinabalu'],
  [103.82, 1.35, 'Singapore'], [151.21, -33.87, 'Sydney']];               // sample data
const lines = [];                                                      // a 10-degree graticule as GeoJSON
for (let lon = 60; lon <= 200; lon += 10) lines.push([[lon, -60], [lon, 60]]);
for (let lat = -60; lat <= 60; lat += 10) lines.push(Array.from({ length: 15 }, (_, i) => [60 + i * 10, lat]));
const geojson = (features) => ({ type: 'geojson', data: { type: 'FeatureCollection', features } });
const map = new maplibregl.Map({
  container: 'map', center: [128, -2], zoom: 1.45, pitch: 25, bearing: -6, attributionControl: false,
  style: {                                  // the whole style inline: no tile server, no glyphs
    version: 8,
    sources: {
      graticule: geojson(lines.map((coordinates) => ({ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates } }))),
      route: geojson([{ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: stores.map(([lng, lat]) => [lng, lat]) } }]),
      stores: geojson(stores.map(([lng, lat, name]) => ({ type: 'Feature', properties: { name }, geometry: { type: 'Point', coordinates: [lng, lat] } }))),
    },
    layers: [
      { id: 'sea', type: 'background', paint: { 'background-color': '#cfe0ea' } },
      { id: 'graticule', type: 'line', source: 'graticule', paint: { 'line-color': '#8fb0c4', 'line-width': 1 } },
      { id: 'route', type: 'line', source: 'route', paint: { 'line-color': '#b5452e', 'line-width': 2, 'line-dasharray': [2, 2] } },
      { id: 'stores', type: 'circle', source: 'stores', paint: { 'circle-radius': 7,
        'circle-color': '#e09a10', 'circle-stroke-width': 2, 'circle-stroke-color': '#fff' } },
    ],
  },
});
for (const [lng, lat, name] of stores) {    // HTML labels ride on the map without a glyph server
  const label = document.createElement('div'); label.className = 'store'; label.textContent = name;
  new maplibregl.Marker({ element: label, anchor: 'bottom', offset: name === 'Singapore' ? [-20, 30] : [0, -10] }).setLngLat([lng, lat]).addTo(map);
}
map.once('idle', () => {
  const gl = map.painter?.context?.gl;
  document.getElementById('out').textContent = `MapLibre GL JS ${maplibregl.getVersion?.() ?? ''} | pitch ${map.getPitch()} deg, ` +
    `bearing ${map.getBearing()} deg` + (gl ? ` | ${gl instanceof WebGL2RenderingContext ? 'WebGL 2' : 'WebGL 1'}` : '');
});
</script>