Skip to content

JavaScript API

This is the advanced route: maplibre-yaml’s JavaScript API, YAMLParser to parse and validate a document and MapRenderer to draw it into a <div> your code owns. Most pages should use the <ml-map> element instead: it does the same with no code, and adds the error card, the notices and the loading message, which the API leaves to you. A page with <ml-map> can still reach the map from a script (mapReady()), and getRenderer() returns the MapRenderer behind it. Use the API when your own code must create, replace and destroy maps, or parse documents without drawing them.

  1. Create index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>My map</title>
    <link
    rel="stylesheet"
    href="https://unpkg.com/maplibre-gl@6/dist/maplibre-gl.css"
    />
    <!-- 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 { height: 100vh; }
    </style>
    </head>
    <body>
    <div id="map"></div>
    <script type="module" src="map.js"></script>
    </body>
    </html>
  2. Create map.yaml next to it

    type: map
    id: quakes
    config:
    mapStyle: "https://tiles.openfreemap.org/styles/liberty"
    center: [-120, 37]
    zoom: 4
    layers:
    - id: quakes
    type: circle
    source:
    type: geojson
    url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson"
    paint:
    circle-radius: 5
    circle-color: "#dc2626"
  3. Create map.js

    import {
    YAMLParser,
    MapRenderer,
    toModel,
    } from "https://esm.sh/@maplibre-yaml/core@0.10?external=maplibre-gl";
    const container = document.getElementById("map");
    const response = await fetch("map.yaml");
    const result = YAMLParser.safeParseMapBlock(await response.text());
    if (!result.success) {
    // Each error is { path, message }, with a line number when it has one.
    container.textContent = result.errors
    .map((e) => `${e.line ? `line ${e.line}: ` : ""}${e.message}`)
    .join("\n");
    } else {
    const renderer = MapRenderer.fromModel(container, toModel(result.data), {
    onError: (error, fatal) => console.error(error, { fatal }),
    });
    renderer.on("load", () => console.log("map loaded"));
    }
  4. Serve the folder and open it

    Terminal window
    npx serve .

    Open the address it prints (for example http://localhost:3000). The map shows California with the past week’s earthquakes, and the console logs “map loaded”.

esm.sh’s ?external=maplibre-gl makes the library use the maplibre-gl in your import map, not a second copy of its own. With npm and a bundler, see With npm and a bundler.

Every YAMLParser method validates against the same schemas <ml-map> and the CLI use, and returns the same errors. The safe… methods return { success, data, errors, warnings } and never throw; the others return the document or throw.

MethodTakes
safeParseMapBlock(yaml) / parseMapBlock(yaml)A type: map document, as YAML text
safeParseScrollytellingBlock(yaml) / parseScrollytellingBlock(yaml)A type: scrollytelling story file
safeParseAny(yaml)Either, when you don’t know which: returns { blockType, result }
safeParseMapBlockValue(value)A document you already hold as an object

Each error has a path (such as layers[0].paint) and a message, plus a line and column when it comes from YAML text. To read YAML yourself with the yaml package and get the same result as the library, use its parse options: Parsing & validating in your own code.

MapRenderer.fromModel(container, model, options?) draws a parsed document into a container that has a height. toModel() turns the parsed document into the renderer’s model, the shape format v1 and format v2 documents share, so either renders the same:

import { YAMLParser, MapRenderer, toModel } from "@maplibre-yaml/core";
const block = YAMLParser.parseMapBlock(yamlText);
const renderer = MapRenderer.fromModel(container, toModel(block), {
// There is no "error" event: problems arrive here.
// fatal is false for a problem the map survives (a layer that failed).
onError: (error, fatal) => console.error(error, { fatal }),
});
renderer.on("load", () => {
/* the basemap and every layer are in */
});
renderer.on("layer:click", ({ layerId, feature, lngLat }) => {
console.log(layerId, feature.properties);
});

The renderer’s events match <ml-map>’s, without the ml-map: prefix (load, layer:click, layer:data-loaded, …); the full list with each payload is MapRendererEvents. Register listeners right after fromModel, before the map can load.

renderer.getMap() returns the MapLibre map. Once load has fired, anything MapLibre can do works:

renderer.on("load", () => {
const map = renderer.getMap();
map.flyTo({ center: [-122.4, 37.8], zoom: 9 });
map.on("moveend", () => console.log("now at", map.getCenter()));
});

The renderer manages the sources and layers in your document. Layers you add with MapLibre’s own map.addLayer() are yours; keep their ids different from the document’s.

Call renderer.update(next, hints?) with a new parsed and modeled document (the same shape fromModel takes) and the running map changes in place: layers and sources that differ are added, removed or updated by id, the legend and panel follow, and the camera moves only if the document’s camera changed. The map object, your listeners and your own layers (added with map.addLayer()) stay. It returns a Promise<UpdateResult> that resolves with what happened:

renderer.on("update:applied", ({ summary }) => {
console.log("changed", summary); // added, removed, propertiesSet, ...
});
renderer.on("update:rejected", ({ kind, errors }) => {
console.warn(kind, errors); // the map is as it was
});
const doc = structuredClone(renderer.getDocument()); // the document the map shows
doc.layers[0].paint["circle-color"] = "#2563eb";
const result = await renderer.update(toModel(doc));
console.log(result.outcome); // "applied"

An invalid document is refused: update:rejected fires with the diagnostics and the map stays as it was. A changed mapStyle rebuilds the map at the current camera; everything else applies in place.

For a page whose readers pan the map themselves (a dashboard whose data refreshes), pass camera: "host" to fromModel’s options so a document change never moves the view:

MapRenderer.fromModel(container, model, { camera: "host" });

A document’s state: values are read with renderer.getState() and written with renderer.setState({ scenario: "flat" }); each write fires state:change.

renderer.destroy() removes the map and stops its live-data timers, for tearing down the container entirely (the page is leaving, or the map is being swapped for something else). To show a different document in the same container, prefer renderer.update() above — it keeps the map object, your listeners and your own layers. Call destroy() and create a fresh renderer only when nothing of the old one should remain:

renderer.destroy();
renderer = MapRenderer.fromModel(container, toModel(nextBlock));

A layer whose source has a refresh: block refetches on its own (Live data). Each fetch fires layer:data-loading, then layer:data-loaded or layer:data-error:

renderer.on("layer:data-loaded", ({ layerId, featureCount }) => {
status.textContent = `${layerId}: ${featureCount} features`;
});
renderer.on("layer:data-error", ({ layerId, error }) => {
status.textContent = `${layerId} failed: ${error.message}`;
});

There is no method to pause polling or force a refresh.

On a GitHub Pages project site, paths in the document that start with / point at the domain root. Put them under the site’s base before rendering, as <ml-map base> does:

import { MapRenderer, toModel, applyDocumentBase } from "@maplibre-yaml/core";
const model = applyDocumentBase(toModel(block), "/<repo>/");
MapRenderer.fromModel(container, model);

For a single URL of your own, resolveDocumentUrl(url, "/<repo>/"). Which paths the base covers: Publishing & subpaths.

Install @maplibre-yaml/core and maplibre-gl, and import the API from the package in your entry file. Point maplibre-gl 6 at its worker first, as Installation: using a bundler shows, or every map fails with Worker failed to load:

// src/main.js (Vite)
import { setWorkerUrl } from "maplibre-gl";
import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url";
import "maplibre-gl/dist/maplibre-gl.css";
import { YAMLParser, MapRenderer, toModel } from "@maplibre-yaml/core";
setWorkerUrl(workerUrl);

Put map.yaml and its data files in Vite’s public/ folder, so the build copies them.

A template literal keeps every space you type, so keep the YAML’s lines at the same depth as its first line. A leading newline is fine:

const yamlText = `
type: map
id: inline-map
config:
mapStyle: "https://tiles.openfreemap.org/styles/liberty"
center: [-74.006, 40.7128]
zoom: 12
`;
const result = YAMLParser.safeParseMapBlock(yamlText);

A separate YAML file avoids the problem, and you can check it with npx maplibre-yaml@latest validate.

Problems every map shares are in Troubleshooting, for example the map has no height. With MapRenderer there is no error card: safeParseMapBlock returns the problems as errors, and the renderer reports the rest through onError.

The basemap draws, but a layer with a url: shows nothing.

  • The request was blocked. The browser console shows a CORS error: the data’s server must allow requests from your page (CORS).
  • The data failed to load. Listen for layer:data-error, which names the layer and the error.
  • The file loaded but is empty. layer:data-loaded reports featureCount: 0.
  • The site is under a subpath and the paths start with /: see Sites under a subpath.

YAML that works in a file fails inline, with an error such as “YAML indentation error: Line 2 is indented differently from line 1 above it”. The first line of the template literal sits at a different depth from the rest:

// Wrong: the first line starts at column 0, the rest are indented with the code
function load() {
const yaml = `type: map
id: my-map`;
}

Start the YAML on the line after the backtick, as in YAML in a JavaScript string. Indent with spaces: YAML rejects tabs.

A load listener registered long after fromModel (in a setTimeout, say) misses the event. Register listeners right after creating the renderer. If load never fires, the onError option says why.

Every renderer event and its payload is in MapRendererEvents.

Use the <ml-map> element unless your own code must own the map; see the top of this page.