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.
Quick start
Section titled “Quick start”-
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><linkrel="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> -
Create
map.yamlnext to ittype: mapid: quakesconfig:mapStyle: "https://tiles.openfreemap.org/styles/liberty"center: [-120, 37]zoom: 4layers:- id: quakestype: circlesource:type: geojsonurl: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson"paint:circle-radius: 5circle-color: "#dc2626" -
Create
map.jsimport {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"));} -
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.
Common tasks
Section titled “Common tasks”Parse and validate a document
Section titled “Parse and validate a document”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.
| Method | Takes |
|---|---|
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.
Render a document
Section titled “Render a document”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.
Reach the MapLibre map
Section titled “Reach the MapLibre map”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.
Update the document
Section titled “Update the document”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 showsdoc.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.
Replace or remove a map
Section titled “Replace or remove a map”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));Follow live data
Section titled “Follow live data”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.
Sites under a subpath
Section titled “Sites under a subpath”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.
With npm and a bundler
Section titled “With npm and a bundler”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.
YAML in a JavaScript string
Section titled “YAML in a JavaScript string”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: mapid: inline-mapconfig: 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.
Reference
Section titled “Reference”- TypeScript API:
YAMLParser,MapRenderer,MapRendererOptionsandMapRendererEvents, generated from the source; everything else is in the@maplibre-yaml/coreAPI. - Parsing & validating in your own code: parse YAML yourself exactly as the library does.
- Export to style.json: turn a document into a plain
MapLibre style, and wire its interactions onto any map with
attachInteractions. The runnableexamples/vanilla/export-and-attach.htmldoes both. - YAML reference: every key of a map document.
Troubleshooting
Section titled “Troubleshooting”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.
Map shows but no data appears
Section titled “Map shows but no data appears”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-loadedreportsfeatureCount: 0. - The site is under a subpath and the paths start with
/: see Sites under a subpath.
Template literal YAML issues
Section titled “Template literal YAML issues”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 codefunction 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.
Events not firing
Section titled “Events not firing”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.
Moved sections
Section titled “Moved sections”Full event list
Section titled “Full event list”Every renderer event and its payload is in
MapRendererEvents.
Which approach should I use?
Section titled “Which approach should I use?”Use the <ml-map> element unless your own
code must own the map; see the top of this page.