Skip to content

Vanilla JavaScript

This guide covers using maplibre-yaml in vanilla JavaScript projects - no frameworks, no build tools required.

You have two options:

  1. Web Component (<ml-map>) - Zero JavaScript, just HTML (recommended for simplicity)
  2. JavaScript API (YAMLParser + MapRenderer) - More control and customization

The fastest way to get started - no JavaScript code needed!

  1. Create an HTML file

    <!DOCTYPE html>
    <html>
    <head>
    <title>My Map</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>
    <!-- Register the <ml-map> web component -->
    <script
    type="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>
  2. Create a YAML configuration file

    Create map.yaml:

    type: map
    id: my-first-map
    config:
    center: [-74.006, 40.7128]
    zoom: 12
    mapStyle: "https://demotiles.maplibre.org/style.json"
    layers:
    - id: points
    type: circle
    source:
    type: geojson
    data:
    type: FeatureCollection
    features:
    - type: Feature
    geometry:
    type: Point
    coordinates: [-74.006, 40.7128]
    properties:
    name: "New York City"
    paint:
    circle-radius: 10
    circle-color: "#3b82f6"
  3. Serve and view

    Terminal window
    npx serve .

    Open http://localhost:3000

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:

  1. Create an HTML file

    <!DOCTYPE html>
    <html>
    <head>
    <title>My Map</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;
    }
    #map {
    width: 100vw;
    height: 100vh;
    }
    </style>
    </head>
    <body>
    <div id="map"></div>
    <script type="module" src="map.js"></script>
    </body>
    </html>
  2. Create a YAML configuration file

    Create map.yaml (same as above)

  3. 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 configuration
    const response = await fetch("map.yaml");
    const yamlText = await response.text();
    // Parse and validate
    const 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 renderer
    const renderer = MapRenderer.fromModel(
    document.getElementById("map"),
    toModel(result.data)
    );
    renderer.on("load", () => {
    console.log("Map loaded successfully!");
    });
    }
  4. Serve and view

    Terminal window
    npx serve .

    Open http://localhost:3000

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

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>

For production projects with bundlers:

Terminal window
npm install @maplibre-yaml/core maplibre-gl@^6
import { YAMLParser, MapRenderer, toModel } from '@maplibre-yaml/core';
import 'maplibre-gl/dist/maplibre-gl.css';

External files are the recommended approach for most projects:

// Load from file
const 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

For simple cases where you want everything in one file:

const yamlConfig = `type: map
id: inline-map
config:
center: [-74.006, 40.7128]
zoom: 12
mapStyle: "https://demotiles.maplibre.org/style.json"
layers: []`;
const result = YAMLParser.safeParseMapBlock(yamlConfig);

The parser validates YAML against the maplibre-yaml schema.

import { YAMLParser } from "@maplibre-yaml/core";
// Safe parsing (recommended) - returns result object
const 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 error
try {
const config = YAMLParser.parseMapBlock(yamlText);
} catch (error) {
console.error("Parse error:", error.message);
}

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 */
},
});
// Events
renderer.on("load", () => {
/* Map and every layer ready */
});
renderer.on("layer:click", ({ layerId, feature, lngLat }) => {
/* Handle click */
});
// Methods
renderer.destroy(); // Clean up resources

MapRenderer.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.

// Map lifecycle (errors arrive through the onError option, not an event)
renderer.on("load", () => {});
// Layer lifecycle
renderer.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 events
renderer.on("layer:click", ({ layerId, feature, lngLat }) => {});
renderer.on("layer:hover", ({ layerId, feature, lngLat }) => {});
// Markers, images, and the params panel
renderer.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 }) => {});
map.yaml
type: map
id: live-map
config:
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"
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}`);
});

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());
});
});

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: map
id: earthquake-dashboard
config:
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: mag

dashboard.js:

import {
YAMLParser,
MapRenderer,
toModel,
} from "https://esm.sh/@maplibre-yaml/core?external=maplibre-gl";
// State
let visible = true;
let renderer = null;
// DOM elements
const statusEl = document.getElementById("status");
const toggleBtn = document.getElementById("toggle");
function showError(message) {
statusEl.textContent = message;
statusEl.style.color = "#ef4444";
}
// Load and initialize
async 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 layer
toggleBtn.addEventListener("click", () => {
if (!renderer) return;
visible = !visible;
renderer.setLayerVisibility("earthquakes", visible);
toggleBtn.textContent = visible ? "Hide Earthquakes" : "Show Earthquakes";
toggleBtn.classList.toggle("active", !visible);
});
// Initialize
init();

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, so YAMLParser and MapRenderer consume it unchanged.
  • Interactions & Export to style.json — how declarative interactions are authored, why a compiled style.json is inert on its own (the export guarantee), and how attachInteractions wires behavior back onto any maplibregl.Map. This is also where the emit interaction and its trusted-policy gate are documented in full.

Three behavior changes are worth knowing when you move to 0.6:

  • !html is trust-gated, not unconditional. A popup !html value 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 data emits a warning (promoted to an error under mlym validate --strict / CI). Pre-check inline geometry.
  • v2 style.metadata flows through. In Format v2, style.metadata is carried to the emitted style.json root metadata slot.

Symptoms: Base map renders, but layers with URL data don’t show any features.

Common Causes:

  1. 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);
});
  1. URL returns invalid GeoJSON

    Test the URL directly in your browser to verify it returns valid GeoJSON.

  2. 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
});

Symptoms: safeParseMapBlock returns success: false with errors.

Common Causes:

  1. 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
  1. 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: []
  1. Invalid values
# ❌ center must be [longitude, latitude]
config:
center: "New York"
# ✅ Correct
config:
center: [-74.006, 40.7128]

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 code
function 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: map
id: my-map
config:
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.

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;
}

Symptoms: renderer.on('load', ...) callback never runs.

Common Causes:

  1. Event registered after map loaded
// ❌ Map might already be loaded
const renderer = MapRenderer.fromModel(container, toModel(block));
setTimeout(() => {
renderer.on("load", () => {}); // Might miss the event
}, 1000);
// ✅ Register immediately
const renderer = MapRenderer.fromModel(container, toModel(block));
renderer.on("load", () => {});
  1. 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 });
},
});