Vanilla JavaScript
This guide covers using maplibre-yaml in vanilla JavaScript projects - no frameworks, no build tools required.
You have two options:
- Web Component (
<ml-map>) - Zero JavaScript, just HTML (recommended for simplicity) - JavaScript API (
YAMLParser+MapRenderer) - More control and customization
Quick Start: Web Component (Recommended)
Section titled “Quick Start: Web Component (Recommended)”The fastest way to get started - no JavaScript code needed!
-
Create an HTML file
<!DOCTYPE html><html><head><title>My Map</title><linkhref="https://unpkg.com/maplibre-gl@6/dist/maplibre-gl.css"rel="stylesheet"/><!-- Tell the browser where to find maplibre-gl --><script type="importmap">{ "imports": { "maplibre-gl": "https://unpkg.com/maplibre-gl@6/dist/maplibre-gl.mjs" } }</script><!-- Register the <ml-map> web component --><scripttype="module"src="https://unpkg.com/@maplibre-yaml/core/register.js"></script><style>body {margin: 0;}ml-map {display: block;width: 100vw;height: 100vh;}</style></head><body><ml-map src="map.yaml"></ml-map></body></html> -
Create a YAML configuration file
Create
map.yaml:type: mapid: my-first-mapconfig:center: [-74.006, 40.7128]zoom: 12mapStyle: "https://demotiles.maplibre.org/style.json"layers:- id: pointstype: circlesource:type: geojsondata:type: FeatureCollectionfeatures:- type: Featuregeometry:type: Pointcoordinates: [-74.006, 40.7128]properties:name: "New York City"paint:circle-radius: 10circle-color: "#3b82f6" -
Serve and view
Terminal window npx serve .
Quick Start: JavaScript API (More Control)
Section titled “Quick Start: JavaScript API (More Control)”For cases where you need programmatic control or want to use the lower-level APIs:
-
Create an HTML file
<!DOCTYPE html><html><head><title>My Map</title><linkhref="https://unpkg.com/maplibre-gl@6/dist/maplibre-gl.css"rel="stylesheet"/><!-- Tell the browser where to find maplibre-gl --><script type="importmap">{ "imports": { "maplibre-gl": "https://unpkg.com/maplibre-gl@6/dist/maplibre-gl.mjs" } }</script><style>body {margin: 0;}#map {width: 100vw;height: 100vh;}</style></head><body><div id="map"></div><script type="module" src="map.js"></script></body></html> -
Create a YAML configuration file
Create
map.yaml(same as above) -
Create your JavaScript file
Create
map.js:import {YAMLParser,MapRenderer,toModel,} from "https://esm.sh/@maplibre-yaml/core?external=maplibre-gl";// Fetch and parse YAML configurationconst response = await fetch("map.yaml");const yamlText = await response.text();// Parse and validateconst result = YAMLParser.safeParseMapBlock(yamlText);if (!result.success) {console.error("Configuration errors:", result.errors);document.getElementById("map").innerHTML = `<div style="color: red; padding: 20px;"><h3>Configuration Error</h3><pre>${JSON.stringify(result.errors, null, 2)}</pre></div>`;} else {// Render the map: normalize the parsed document, then hand it to the rendererconst renderer = MapRenderer.fromModel(document.getElementById("map"),toModel(result.data));renderer.on("load", () => {console.log("Map loaded successfully!");});} -
Serve and view
Terminal window npx serve .
Which Approach Should I Use?
Section titled “Which Approach Should I Use?”Use the Web Component (<ml-map>) if:
- ✅ You want the simplest possible integration
- ✅ You don’t need programmatic control
- ✅ You’re embedding maps in static pages
- ✅ You want zero JavaScript code
Use the JavaScript API (YAMLParser + MapRenderer) if:
- ✅ You need to programmatically modify the map
- ✅ You want to build custom UI controls
- ✅ You need access to the MapLibre GL instance
- ✅ You’re building a complex interactive application
Installation Options
Section titled “Installation Options”CDN (No Build Tools)
Section titled “CDN (No Build Tools)”For quick prototypes or simple projects. To use the web component, provide maplibre-gl via an import map and load the register bundle:
<script type="importmap"> { "imports": { "maplibre-gl": "https://unpkg.com/maplibre-gl@6/dist/maplibre-gl.mjs" } }</script><script type="module" src="https://unpkg.com/@maplibre-yaml/core/register.js"></script>To use the JavaScript API, import the package from esm.sh, which resolves yaml and zod for you. Keep the same import map and add ?external=maplibre-gl, so the library renders with the maplibre-gl you pinned instead of a second copy esm.sh picks:
<script type="importmap"> { "imports": { "maplibre-gl": "https://unpkg.com/maplibre-gl@6/dist/maplibre-gl.mjs" } }</script><script type="module"> import { YAMLParser, MapRenderer, toModel, } from "https://esm.sh/@maplibre-yaml/core?external=maplibre-gl"; // Your code here</script>npm (With Build Tools)
Section titled “npm (With Build Tools)”For production projects with bundlers:
npm install @maplibre-yaml/core maplibre-gl@^6pnpm add @maplibre-yaml/core maplibre-gl@^6yarn add @maplibre-yaml/core maplibre-gl@^6import { YAMLParser, MapRenderer, toModel } from '@maplibre-yaml/core';import 'maplibre-gl/dist/maplibre-gl.css';Loading YAML Configuration
Section titled “Loading YAML Configuration”Recommended: External YAML Files
Section titled “Recommended: External YAML Files”External files are the recommended approach for most projects:
// Load from fileconst response = await fetch("/configs/map.yaml");const yamlText = await response.text();const result = YAMLParser.safeParseMapBlock(yamlText);Benefits:
- Clean separation of configuration and code
- Easy to edit without touching JavaScript
- Proper YAML indentation preserved
- Can be validated independently
- Cacheable by browsers
Alternative: Inline YAML Strings
Section titled “Alternative: Inline YAML Strings”For simple cases where you want everything in one file:
const yamlConfig = `type: mapid: inline-mapconfig: center: [-74.006, 40.7128] zoom: 12 mapStyle: "https://demotiles.maplibre.org/style.json"layers: []`;
const result = YAMLParser.safeParseMapBlock(yamlConfig);Core APIs
Section titled “Core APIs”YAMLParser
Section titled “YAMLParser”The parser validates YAML against the maplibre-yaml schema.
import { YAMLParser } from "@maplibre-yaml/core";
// Safe parsing (recommended) - returns result objectconst result = YAMLParser.safeParseMapBlock(yamlText);
if (result.success) { console.log("Valid config:", result.data);} else { console.error("Errors:", result.errors); // errors: [{ path: 'config.center', message: 'Expected array' }, ...]}
// Direct parsing - throws on errortry { const config = YAMLParser.parseMapBlock(yamlText);} catch (error) { console.error("Parse error:", error.message);}MapRenderer
Section titled “MapRenderer”The renderer creates and manages the MapLibre GL map.
import { YAMLParser, MapRenderer, toModel } from "@maplibre-yaml/core";
// Render a parsed map document (v1 or Format v2). Errors arrive through the// `onError` option — there is no "error" event.const block = YAMLParser.parseMapBlock(yamlText);const renderer = MapRenderer.fromModel(container, toModel(block), { onError: (error, fatal) => { /* Handle error; fatal === false for runtime errors the map survives */ },});
// Eventsrenderer.on("load", () => { /* Map and every layer ready */});renderer.on("layer:click", ({ layerId, feature, lngLat }) => { /* Handle click */});
// Methodsrenderer.destroy(); // Clean up resourcesMapRenderer.fromModel(container, model, options?) is the one-call way to
render a parsed document. The constructor is the lower-level form,
new MapRenderer(container, config, layers?, options?, sources?), which takes
the document’s parts (block.config, block.layers, …) rather than the whole
document.
Full Event List
Section titled “Full Event List”// Map lifecycle (errors arrive through the onError option, not an event)renderer.on("load", () => {});
// Layer lifecyclerenderer.on("layer:added", ({ layerId }) => {});renderer.on("layer:removed", ({ layerId }) => {});
// Layer data (initial load and every live-data refresh)renderer.on("layer:data-loading", ({ layerId }) => {});renderer.on("layer:data-loaded", ({ layerId, featureCount }) => {});renderer.on("layer:data-error", ({ layerId, error }) => {});
// Interaction eventsrenderer.on("layer:click", ({ layerId, feature, lngLat }) => {});renderer.on("layer:hover", ({ layerId, feature, lngLat }) => {});
// Markers, images, and the params panelrenderer.on("markers:added", ({ count }) => {});renderer.on("marker:click", ({ index, at }) => {});renderer.on("marker:icon-error", ({ index, icon }) => {});renderer.on("image:error", ({ name, url }) => {});renderer.on("parameter:change", ({ key, value }) => {});renderer.on("layer:visibility", ({ layerId, visible }) => {});Working with Live Data
Section titled “Working with Live Data”Polling (Auto-refresh)
Section titled “Polling (Auto-refresh)”type: mapid: live-mapconfig: center: [-120, 37] zoom: 5 mapStyle: "https://demotiles.maplibre.org/style.json"layers: - id: earthquakes type: circle source: type: geojson url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson" refresh: refreshInterval: 60000 # Refresh every 60 seconds updateStrategy: replace paint: circle-radius: 8 circle-color: "#ef4444"Handling Data Updates
Section titled “Handling Data Updates”renderer.on("layer:data-loading", ({ layerId }) => { showSpinner(layerId);});
renderer.on("layer:data-loaded", ({ layerId, featureCount }) => { hideSpinner(layerId); updateStatus(`Loaded ${featureCount} features`);});
renderer.on("layer:data-error", ({ layerId, error }) => { hideSpinner(layerId); showError(`Failed to load ${layerId}: ${error.message}`);});Accessing the MapLibre Instance
Section titled “Accessing the MapLibre Instance”For advanced use cases, access the underlying MapLibre GL map:
import * as maplibregl from "maplibre-gl";
const renderer = MapRenderer.fromModel(container, toModel(block));
renderer.on("load", () => { // Access the MapLibre GL map instance const map = renderer.getMap();
// Use any MapLibre GL API map.flyTo({ center: [-122.4, 37.8], zoom: 14 });
// Add custom controls map.addControl(new maplibregl.NavigationControl());
// Add custom event handlers map.on("moveend", () => { console.log("Map moved to:", map.getCenter()); });});Complete Example: Interactive Dashboard
Section titled “Complete Example: Interactive Dashboard”Here’s a complete example showing a map with live data and UI controls:
index.html:
<!DOCTYPE html><html> <head> <title>Earthquake Dashboard</title> <link href="https://unpkg.com/maplibre-gl@6/dist/maplibre-gl.css" rel="stylesheet" /> <!-- Tell the browser where to find maplibre-gl --> <script type="importmap"> { "imports": { "maplibre-gl": "https://unpkg.com/maplibre-gl@6/dist/maplibre-gl.mjs" } } </script> <style> body { margin: 0; font-family: system-ui, sans-serif; } #map { width: 100vw; height: 100vh; } .controls { position: absolute; top: 10px; left: 10px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); z-index: 1; } .status { margin-top: 10px; font-size: 14px; color: #666; } button { padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; background: white; cursor: pointer; } button:hover { background: #f5f5f5; } button.active { background: #3b82f6; color: white; border-color: #3b82f6; } </style> </head> <body> <div id="map"></div> <div class="controls"> <button id="toggle">Hide Earthquakes</button> <div class="status" id="status">Loading...</div> </div> <script type="module" src="dashboard.js"></script> </body></html>earthquakes.yaml:
type: mapid: earthquake-dashboardconfig: center: [-120, 40] zoom: 4 mapStyle: "https://demotiles.maplibre.org/style.json"layers: - id: earthquakes type: circle source: type: geojson url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson" refresh: refreshInterval: 60000 updateStrategy: replace paint: circle-radius: - interpolate - ["linear"] - ["get", "mag"] - 0 - 4 - 5 - 20 circle-color: - interpolate - ["linear"] - ["get", "mag"] - 0 - "#22c55e" - 3 - "#eab308" - 5 - "#ef4444" circle-opacity: 0.8 interactive: hover: cursor: pointer click: popup: - h3: - property: title - p: - str: "Magnitude: " - property: magdashboard.js:
import { YAMLParser, MapRenderer, toModel,} from "https://esm.sh/@maplibre-yaml/core?external=maplibre-gl";
// Statelet visible = true;let renderer = null;
// DOM elementsconst statusEl = document.getElementById("status");const toggleBtn = document.getElementById("toggle");
function showError(message) { statusEl.textContent = message; statusEl.style.color = "#ef4444";}
// Load and initializeasync function init() { try { // Load YAML configuration const response = await fetch("earthquakes.yaml"); const yamlText = await response.text();
// Parse and validate const result = YAMLParser.safeParseMapBlock(yamlText);
if (!result.success) { showError(`Config error: ${result.errors[0].message}`); return; }
// Create renderer renderer = MapRenderer.fromModel( document.getElementById("map"), toModel(result.data), { onError: (error) => showError(`Error: ${error.message}`) } );
// Fires on the first load and again on every 60-second refresh renderer.on("layer:data-loading", ({ layerId }) => { statusEl.textContent = `Loading ${layerId}...`; });
renderer.on("layer:data-loaded", ({ featureCount }) => { const time = new Date().toLocaleTimeString(); statusEl.textContent = `${featureCount} earthquakes · Updated ${time}`; statusEl.style.color = "#666"; });
renderer.on("layer:data-error", ({ error }) => { showError(`Error: ${error.message}`); }); } catch (error) { showError(`Failed to initialize: ${error.message}`); }}
// Button handler: show or hide the earthquake layertoggleBtn.addEventListener("click", () => { if (!renderer) return;
visible = !visible; renderer.setLayerVisibility("earthquakes", visible); toggleBtn.textContent = visible ? "Hide Earthquakes" : "Show Earthquakes"; toggleBtn.classList.toggle("active", !visible);});
// Initializeinit();Format v2 and Interactions
Section titled “Format v2 and Interactions”Two guides cover capabilities that apply directly to the vanilla-JS path:
- Format v2 — the additive, opt-in
style:/runtime:split. A v2 document parses to the same internal model as its v1 twin, soYAMLParserandMapRendererconsume it unchanged. - Interactions & Export to style.json — how declarative
interactions are authored, why a compiled
style.jsonis inert on its own (the export guarantee), and howattachInteractionswires behavior back onto anymaplibregl.Map. This is also where theemitinteraction and its trusted-policy gate are documented in full.
Upgrading to 0.6
Section titled “Upgrading to 0.6”Three behavior changes are worth knowing when you move to 0.6:
!htmlis trust-gated, not unconditional. A popup!htmlvalue renders as raw markup only under a trusted capability policy; otherwise it is escaped and shown as text.- Malformed inline GeoJSON now warns. A v1 document with malformed inline
GeoJSON
dataemits a warning (promoted to an error undermlym validate --strict/ CI). Pre-check inline geometry. - v2
style.metadataflows through. In Format v2,style.metadatais carried to the emittedstyle.jsonrootmetadataslot.
Troubleshooting
Section titled “Troubleshooting”Map Shows But No Data Appears
Section titled “Map Shows But No Data Appears”Symptoms: Base map renders, but layers with URL data don’t show any features.
Common Causes:
-
CORS blocking the request
Check the browser console for CORS errors. The data URL must allow cross-origin requests.
// Check network tab - is the request being made?renderer.on("layer:data-error", ({ layerId, error }) => { console.error(`${layerId} failed:`, error);});-
URL returns invalid GeoJSON
Test the URL directly in your browser to verify it returns valid GeoJSON.
-
Data loaded but features have no geometry
renderer.on("layer:data-loaded", ({ layerId, featureCount }) => { console.log(`${layerId}: ${featureCount} features`); // If featureCount is 0, the GeoJSON may be empty or malformed});YAML Parsing Errors
Section titled “YAML Parsing Errors”Symptoms: safeParseMapBlock returns success: false with errors.
Common Causes:
-
Indentation issues
YAML requires consistent indentation (2 spaces recommended):
# ✅ Correct config: center: [-74, 40] zoom: 10
# ❌ Wrong - inconsistent indentation config: center: [-74, 40] zoom: 10- Missing required fields
# ❌ Missing required fields type: map layers: []
# ✅ Correct - has all required fields type: map id: my-map config: center: [0, 0] zoom: 2 mapStyle: "https://..." layers: []- Invalid values
# ❌ center must be [longitude, latitude] config: center: "New York"
# ✅ Correct config: center: [-74.006, 40.7128]Template Literal YAML Issues
Section titled “Template Literal YAML Issues”Symptoms: YAML works in a file but fails when inline in JavaScript.
Cause: A template literal keeps the indentation of your source code, so
the first line and the lines after it can end up at different depths. The
parser reports a yaml-syntax error such as “Nested mappings are not allowed
in compact mappings”.
// ❌ WRONG - first line starts at column 0, the rest are indented with the codefunction load() { const yaml = `type: map id: my-map config: center: [-74, 40]`;}
// ✅ CORRECT - every line at the same depth (a leading newline is fine)const yaml = `type: mapid: my-mapconfig: center: [-74, 40] zoom: 10 mapStyle: "https://demotiles.maplibre.org/style.json"layers: []`;Indent with spaces only: YAML rejects tab indentation.
Best Practice: Use external YAML files whenever possible.
Map Container Has Zero Size
Section titled “Map Container Has Zero Size”Symptoms: Map doesn’t render, or renders as a tiny sliver.
Cause: The container element needs explicit dimensions.
/* ❌ Container has no size */#map {}
/* ✅ Explicit dimensions */#map { width: 100%; height: 400px;}
/* ✅ Or fill viewport */#map { width: 100vw; height: 100vh;}Events Not Firing
Section titled “Events Not Firing”Symptoms: renderer.on('load', ...) callback never runs.
Common Causes:
- Event registered after map loaded
// ❌ Map might already be loadedconst renderer = MapRenderer.fromModel(container, toModel(block));setTimeout(() => { renderer.on("load", () => {}); // Might miss the event}, 1000);
// ✅ Register immediatelyconst renderer = MapRenderer.fromModel(container, toModel(block));renderer.on("load", () => {});- An error prevented the load
MapRenderer reports errors through the onError option rather than an
event; pass it when you create the renderer:
const renderer = MapRenderer.fromModel(container, toModel(block), { onError: (error, fatal) => { console.error("Map failed:", error, { fatal }); },});Next Steps
Section titled “Next Steps”- Format v2 - The
style:/runtime:split - Interactions & Export to style.json - Interactions, the export guarantee, and
attachInteractions examples/vanilla/export-and-attach.html- A runnable export →attachInteractionsdemo: compile a document to a self-containedstyle.json, render it in a plainnew maplibregl.Map(...), then reattach the declarative interactions so a click opens a popup- Web Components Integration - Even simpler HTML-only approach
- Astro Integration - For Astro-based sites
- Live Data Guide - Deep dive into polling and streaming
- TypeScript API Reference - Complete API documentation, generated from the source
@maplibre-yaml/coreREADME - Entry points and the parser/renderer API