HTML pages: the <ml-map> element
The <ml-map> element draws a map document on an HTML page. It is a web
component (a custom HTML element), so it works in any page: load it from a
CDN, point its src at a YAML file, and give it a height. There is no
JavaScript to write.
This page is for plain HTML pages. On an Astro site, use the Astro components; in React, Vue or Svelte, see React & other frameworks.
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><!-- Registers <ml-map> --><scripttype="module"src="https://unpkg.com/@maplibre-yaml/core@0.10/register.js"></script></head><body><ml-map src="map.yaml" style="display: block; height: 400px;"></ml-map></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" -
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 as red dots.
Open the page from a web server, not by double-clicking it: a page opened as
file:// can’t fetch map.yaml, and the map shows “Open this page through a
local web server” instead. The Quick start
walks through the same page step by step, and
Your first map, explained explains each line
of the YAML.
Common tasks
Section titled “Common tasks”Load a map from a YAML file
Section titled “Load a map from a YAML file”src is the address of the map document, relative to the page. Keeping the
YAML in its own file keeps the page short, and lets you check the file with
npx maplibre-yaml@latest validate map.yaml before you publish.
<ml-map src="maps/city.yaml" style="display: block; height: 400px;"></ml-map>Paths inside the YAML (url: "data/points.geojson") resolve against the
page, not the YAML file
(Loading data › How paths are read).
Changing src fetches the new document and applies it to the running map
in place (Change the map after it loads);
reload() fetches the same one again and applies what changed.
Give every <ml-map> display: block and a height. A custom element is
inline and zero-height by default, so without them the map is invisible, and
the element shows a note asking for a height in its place.
Sites under a subpath (GitHub Pages)
Section titled “Sites under a subpath (GitHub Pages)”On a GitHub Pages project site (https://<user>.github.io/<repo>/), add
base="/<repo>/" and leave the YAML as it is. src and every path to a file
of your own then load under the subpath. Which paths it covers and how to
preview it locally:
Publishing & subpaths.
<ml-map src="maps/city.yaml" base="/<repo>/" style="display: block; height: 400px;"></ml-map>Put your own controls over the map
Section titled “Put your own controls over the map”Children with a slot attribute go in a corner of the map: top-left,
top-right, bottom-left or bottom-right, stacked with the built-in
legend and params panel. A slot="legend" child replaces the built-in
legend. Buttons, captions and credits sit over the map without any
positioning CSS:
<ml-map src="map.yaml" style="display: block; height: 400px;"> <div slot="top-right" class="my-controls"> <button data-action="zoom-in">+</button> <button data-action="reset">Reset</button> </div> <p slot="bottom-left">Data: USGS</p></ml-map>
<script type="module"> const el = document.querySelector("ml-map"); const home = await el.mapReady().then((map) => ({ center: map.getCenter(), zoom: map.getZoom(), })); document.querySelector(".my-controls").addEventListener("click", (e) => { const map = el.getMap(); // read at click time: a basemap change makes a new map const action = e.target.closest("[data-action]")?.dataset.action; if (action === "zoom-in") map.zoomIn(); if (action === "reset") map.jumpTo(home); });</script>Slot children are shown once the map loads, and stay through reload() and
a new document. How they stack and behave is under
<ml-map> slots.
Reach the MapLibre map
Section titled “Reach the MapLibre map”mapReady() resolves with the MapLibre map once the document has loaded (at
once if it already has), and rejects if the document fails. From there,
anything MapLibre can do works:
<script type="module"> const el = document.querySelector("ml-map"); const map = await el.mapReady();
map.flyTo({ center: [-122.42, 37.77], zoom: 9 }); map.on("moveend", () => console.log("now at", map.getCenter()));</script>Layers you add with MapLibre’s own map.addLayer() are yours to manage; keep
their ids different from the document’s. A new document never moves or
removes them. getMap() returns the map without waiting (null before it
loads) and keeps returning the same map through document changes (only a
basemap change builds a new one), and getRenderer() returns the
MapRenderer behind the element.
Change the map after it loads
Section titled “Change the map after it loads”Assign a different document to config (or change src) 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 stay. update() does the same and resolves with what happened:
<script type="module"> const el = document.querySelector("ml-map"); await el.mapReady();
el.addEventListener("ml-map:update-applied", ({ detail }) => { console.log("changed", detail.summary); // added, removed, propertiesSet, ... }); el.addEventListener("ml-map:update-rejected", ({ detail }) => { console.warn(detail.kind, detail.errors); // the map is as it was });
const doc = structuredClone(el.getDocument()); // the document the map shows doc.layers[0].paint["circle-color"] = "#2563eb"; const { outcome } = await el.update(doc); // "applied"</script>An invalid document is refused: ml-map:update-rejected carries the
diagnostics, the map stays as it was, and the notice shows a hint while you
develop. A changed mapStyle rebuilds the map at the current camera and
fires ml-map:load again; everything else applies in place.
For a page whose readers pan the map themselves (a dashboard whose data
refreshes), add camera="host" so a new document never moves the view:
<ml-map src="map.yaml" camera="host" style="display: block; height: 400px;"></ml-map>A document’s state: values are read with getState() and written with
setState({ scenario: "flat" }); each write fires ml-map:state-change
(State and parameters).
The rules in full, your own layers included, are under
Updating the document.
React to map events
Section titled “React to map events”The element dispatches bubbling ml-map:* events: when the map loads, when a
layer’s data arrives or refreshes, when a feature is clicked or hovered, and
more. Each carries its data on event.detail:
<script type="module"> const el = document.querySelector("ml-map"); el.addEventListener("ml-map:layer-click", (event) => { const { layerId, feature } = event.detail; console.log(layerId, feature.properties.title); });</script>Every event, with its detail, is under
<ml-map> events.
When something goes wrong
Section titled “When something goes wrong”<ml-map> shows problems on the page, not only in the console: an error
card in place of a map that can’t render, a notice over a map when part
of it didn’t load, and author hints in that notice while you develop
(localhost), or anywhere with the debug attribute. quiet hides the
notice on a finished site; the error card always shows. What each one means,
and the common fixes, are in
Troubleshooting. A page
script can read the same problems from ml-map:error:
el.addEventListener("ml-map:error", (event) => { // A document that can't load or validate: errors, each { path, message }. // A failure while the map runs: error, with fatal true or false. console.error(event.detail.errors ?? event.detail.error);});Multiple maps on one page
Section titled “Multiple maps on one page”Each <ml-map> is independent and loads its own document. The page needs the
import map and register.js only once:
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;"> <ml-map src="map-1.yaml" style="display: block; height: 400px;"></ml-map> <ml-map src="map-2.yaml" style="display: block; height: 400px;"></ml-map></div>Story maps: <ml-scrollytelling>
Section titled “Story maps: <ml-scrollytelling>”The same register.js also defines <ml-scrollytelling>, which shows a
type: scrollytelling story file: a sticky map with chapters scrolling over
it. See Story maps in plain HTML.
Reference
Section titled “Reference”Every attribute (src, config, base, camera, debug, quiet),
property, method, event, slot and styling hook of <ml-map> is in the
<ml-map> reference. What a map document can
contain is in the YAML reference.
Troubleshooting
Section titled “Troubleshooting”Most problems look the same whichever way the map is on the page; they are in
Troubleshooting:
the map has no height,
<ml-map> isn’t defined,
an error card instead of the map,
and the basemap draws but your layer doesn’t.
Events not firing
Section titled “Events not firing”An ml-map:load listener added after the map has loaded never runs: the
event has already happened. Use await el.mapReady() instead, which resolves
whether the map loaded before or after you ask. If the map never loads, an
ml-map:error listener tells you why.
Alternatives
Section titled “Alternatives”src with a YAML file, above, is the recommended way. Use these when it
doesn’t fit.
Inline YAML (script tag)
Section titled “Inline YAML (script tag)”For a page that must be one file, put the YAML in a <script type="text/yaml">
inside the element. Start the YAML on the line after the opening tag, and
keep every line at the same depth relative to the first (indenting the whole
block to match your HTML is fine):
<ml-map style="display: block; height: 400px;"> <script type="text/yaml">type: mapid: inline-mapconfig: mapStyle: "https://tiles.openfreemap.org/styles/liberty" center: [-74.006, 40.7128] zoom: 12 </script></ml-map><script type="application/yaml"> works the same way, and
<script type="application/json"> takes the document written as JSON:
<ml-map style="display: block; height: 400px;"> <script type="application/json"> { "type": "map", "id": "inline-json", "config": { "mapStyle": "https://tiles.openfreemap.org/styles/liberty", "center": [-74.006, 40.7128], "zoom": 12 } } </script></ml-map>A document built in JavaScript (config)
Section titled “A document built in JavaScript (config)”When your code builds the document (from a form, or an API response), set
the config property to the object, or the config attribute to a JSON
string:
<ml-map id="dynamic-map" style="display: block; height: 400px;"></ml-map>
<script type="module"> const doc = { type: "map", id: "dynamic", config: { mapStyle: "https://tiles.openfreemap.org/styles/liberty", center: [-74.006, 40.7128], zoom: 12, }, }; document.getElementById("dynamic-map").config = doc; // or: el.setAttribute("config", JSON.stringify(doc));</script>The document is validated like a YAML file; an invalid first document shows
the error card. A changed config is applied to the running map in place:
the map and its camera stay, and only what differs changes
(Change the map after it loads); an invalid
one is refused with ml-map:update-rejected and the map kept. One equal to
the document already shown does nothing. A config assigned right after the
element upgrades renders without a passing “No configuration provided”
error.
With npm and a bundler
Section titled “With npm and a bundler”Install @maplibre-yaml/core and maplibre-gl, import
@maplibre-yaml/core/register and MapLibre’s CSS once in your entry file, and
point maplibre-gl 6 at its worker:
Installation: using a bundler.
Put the YAML and data files in the bundler’s public/ folder. The element
then works in your HTML as above.
In a framework
Section titled “In a framework”React, Vue, Svelte and other frameworks render <ml-map> like any other tag.
How each passes config, listens for events and types the element is on
React & other frameworks.
Format v2 documents
Section titled “Format v2 documents”A format v2 document (version: 2, with style: and
runtime:) works in every way shown on this page and draws the same map as
its format v1 twin.
Your own JavaScript instead of the element
Section titled “Your own JavaScript instead of the element”To parse and render a document from your own code, into a <div> you
control, see the JavaScript API.
Moved sections
Section titled “Moved sections”Installation options
Section titled “Installation options”Loading from a CDN, versions in CDN URLs and supported maplibre-gl versions are in Installation & versions. Hosting maplibre-yaml’s own files is in Publishing & subpaths.
Trust and attribution
Section titled “Trust and attribution”How attribution text is sanitized is in the
<ml-map> reference. Why
click.emit does nothing under <ml-map> is under
Host events.
Controlling live data
Section titled “Controlling live data”Polling follows the document’s refreshInterval
(Live data); each refresh fires
ml-map:layer-data-loaded (events).