Map documents
A map document is one YAML file that starts with type: map. <ml-map>
and the Astro <Map> component draw it. This page explains the parts that
aren’t data and layers: the camera, the basemap, 3D, the extras placed on the
map, and values readers can change. Sources and layers have their own page,
Sources & layers. For every key with its type
and default, see the map reference.
A complete map document
Section titled “A complete map document”# yaml-language-server: $schema=https://docs.maplibre-yaml.org/schema/latest/map.schema.jsontype: mapid: earthquakes-weektitle: "Earthquakes this week" # the map's name, for screen readersdescription: "Every earthquake the USGS recorded in the past seven days, sized by magnitude."config: # camera, basemap, MapLibre map options center: [-120, 37] zoom: 4 mapStyle: "https://tiles.openfreemap.org/styles/liberty"sources: # beside config:, not inside it quakes: type: geojson url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson"layers: - id: quakes type: circle source: quakes paint: circle-radius: ["interpolate", ["linear"], ["get", "mag"], 1, 3, 6, 18] circle-color: "#ef4444" circle-opacity: 0.7 legend: { label: "Earthquake", color: "#ef4444", shape: circle } interactive: hover: cursor: pointer click: popup: - h3: [{ property: place }] - p: - str: "Magnitude " - property: magmarkers: - at: [-122.2585, 37.8719] color: "#2563eb" popup: - p: [{ str: "Seismology lab, UC Berkeley" }]controls: navigation: true scale: trueOnly type, id, config.center and config.zoom are required. Leaving
out mapStyle is allowed, but it gets a warning and <ml-map> shows an
error card instead of the map, so treat it as required too
(below).
config: and the keys beside it
Section titled “config: and the keys beside it”config: holds the camera, the basemap, and any other
MapLibre map option
(cooperativeGestures, hash, maxZoom …), which go to MapLibre as written.
Everything else sits beside config:, at the top level of the document:
title, description, sources, layers, markers, popups, images,
terrain, sky, projection, light, state, parameters, controls
and legend.
This is the most common mistake in a map document. A top-level key nested
inside config: does nothing, and validation warns:
"markers" belongs at the top level, next to config:, not inside it.A key under config: that is neither a MapLibre option nor one of these warns
as Unknown key "fooBar" in config.
The basemap: mapStyle
Section titled “The basemap: mapStyle”The basemap is the map under your layers: streets, terrain, labels. Set it
with config.mapStyle, in one of four forms:
| Form | Example | Use it for |
|---|---|---|
| A style URL | mapStyle: "https://tiles.openfreemap.org/styles/liberty" | A hosted basemap (OpenFreeMap, MapTiler, Stadia …) or a style.json on your site |
| A style object | mapStyle: { version: 8, sources: {…}, layers: […] } | A small style written in the document |
| A Protomaps basemap | mapStyle: { pmtiles: data/nyc.pmtiles, flavor: dark } | A full basemap from one .pmtiles file, with no API key or tile server |
A .pmtiles path | mapStyle: data/nyc.pmtiles | The same, with the default light flavor |
An object with version, sources or layers is a style object; any other
object is the Protomaps form. Making and hosting the .pmtiles file, and the
flavors and label languages, are in
Host your own basemap.
Without mapStyle, mlym validate warns “No mapStyle: this map has no
basemap”, and <ml-map> shows an error card in place of the map. The Astro
map builders are the exception: they can fill it in from a site-wide
defaultMapStyle (Global configuration).
The camera
Section titled “The camera”center ([longitude, latitude]) and zoom place the camera when the map
opens. pitch tilts it, in degrees from straight down, and bearing turns
it, in degrees clockwise from north.
Coordinates are always longitude first, though many map sites show
latitude first. When a center: only makes sense the other way round (it
would land among the document’s own data or markers), validation and the map
both say so:
center is [40.75, -73.98] — coordinates are [longitude, latitude]; did you mean [-73.98, 40.75]?Fit to bounds
Section titled “Fit to bounds”bounds frames an area instead of a point. It takes either of MapLibre’s
forms, longitude first:
config: center: [-74.006, 40.7128] # used if the bounds can't apply zoom: 12 bounds: [-74.3, 40.5, -73.7, 40.9] # west, south, east, north # or: bounds: [[-74.3, 40.5], [-73.7, 40.9]] mapStyle: "https://tiles.openfreemap.org/styles/liberty"A value with latitude and longitude swapped gets an error or a warning with
the corrected value to copy. Longitudes past ±180 are allowed, so a box can
cross the antimeridian ([100, -50, 290, 62] frames the Pacific).
maxBounds and a story chapter’s fitBounds take the same forms.
Fit to a source’s data
Section titled “Fit to a source’s data”fitTo frames whatever a source holds, so the camera follows the data
rather than coordinates you typed. Name a GeoJSON, FlatGeobuf, GeoParquet or
COG source:
type: mapid: alpine-countriesconfig: center: [0, 20] # before the data arrives, and if it never does zoom: 2 mapStyle: "https://tiles.openfreemap.org/styles/liberty" fitTo: source: countries maxZoom: 9sources: countries: type: geojson url: "https://docs.maplibre-yaml.org/gallery-assets/alpine-countries.geojson"layers: - id: borders type: line source: countries paint: line-color: "#e63946" line-width: 1.5- The map opens already framed, with no jump.
<ml-map>starts loading the file as soon as it reads the document and builds the map on the data’s extent. The file is downloaded once; the source uses the same copy. - A slow file doesn’t hold up the map. After 1.5 seconds the map opens at
centerandzoom, then frames the data when it arrives. The element’sfitToWaitMsproperty changes the wait.ml-map:camera-fitfires when the fit applies. - Padding. By default the data keeps a margin so features at the edge
are drawn whole, plus room for the controls along the bottom.
padding: 40sets it in pixels;padding: 0frames the exact extent. maxZoomkeeps a single point, or a small area, from zooming all the way in.- Tiled sources (vector tiles, PMTiles, raster tiles) have no extent the
browser can read.
fitToon one warns once and keepscenterandzoom. fitTo: countriesis short forfitTo: { source: countries }. When bothfitToandboundsare set,fitTowins. A refresh of the data doesn’t re-fit: after the map opens, the reader owns the camera.
Limit where readers can go
Section titled “Limit where readers can go”minZoom, maxZoom, minPitch, maxPitch and maxBounds keep readers
near the data. They help most on a small map in a page, where a stray scroll
can lose the data:
config: center: [-74.006, 40.7128] zoom: 12 minZoom: 9 maxBounds: [-74.3, 40.5, -73.7, 40.9] mapStyle: "https://tiles.openfreemap.org/styles/liberty"How readers can move the map
Section titled “How readers can move the map”Each way of moving the map is a MapLibre option in config:, on by default.
interactive: false makes a still map, for an illustration. To turn off
single gestures:
config: center: [-74.006, 40.7128] zoom: 12 scrollZoom: false # the page scrolls past the map dragRotate: false # north stays up touchPitch: false # two fingers don't tilt it on a phone mapStyle: "https://tiles.openfreemap.org/styles/liberty"hash: true keeps the view in the page’s address (#12/40.7128/-74.006),
so a reader can bookmark or share it. The full list of options is under
config in the map reference.
Gallery: Disable scroll zoom,
Display a non-interactive map.
Terrain, sky & projection
Section titled “Terrain, sky & projection”terrain:, sky: and projection: sit beside config: and take MapLibre’s
own shapes, so what you write is what an exported style.json carries.
Terrain needs elevation data: a raster-dem source, declared under
sources: and named in terrain.source:
type: mapid: alpsconfig: center: [11.39, 47.27] zoom: 12 pitch: 70 maxPitch: 85 mapStyle: "https://tiles.openfreemap.org/styles/liberty"sources: elevation: type: raster-dem url: "https://tiles.mapterhorn.com/tilejson.json" encoding: terrariumterrain: source: elevation exaggeration: 1.5sky: sky-color: "#199EF3" horizon-color: "#ffffff" fog-color: "#ffffff" fog-ground-blend: 0.1projection: type: globecontrols: navigation: true terrain: true # a 3D on/off button- Set
encoding. It defaults tomapbox, even when the elevation source’s TileJSON says otherwise. Mapterhorn’s tiles areterrarium. - Give a hillshade layer its own
raster-demsource. One source serving both terrain and ahillshadelayer draws the shading at lower quality. - The sky shows only when the camera can see the horizon: tilt it with
pitch. Fog only affects 3D terrain.sky: {}turns on MapLibre’s default sky. projection: { type: globe }draws the world as a globe when zoomed out;mercatoris the default.- A
terrain.sourcethat names noraster-demsource warns once, and the map stays flat.
Gallery: 3D terrain, Sky, fog, terrain, Display a globe with an atmosphere.
Runtime requirements
Section titled “Runtime requirements”Each of these needs a minimum maplibre-gl. On an older one, the map logs one console warning naming the version and draws without that part:
| Key | Needs maplibre-gl | On an older runtime |
|---|---|---|
terrain: | 2.2 | Every supported version has it |
sky: | 4.5 | No sky or fog |
sky.atmosphere-blend | 5.0 (globe only) | No atmosphere |
projection: globe | 5.0 | A flat (mercator) map |
controls.globe | 5.0 | No globe button |
light: sets the one light that shades fill-extrusion faces, such as 3D
buildings. It takes the style spec’s
light properties:
anchor (map keeps the shading fixed as the map turns), position,
color and intensity.
type: mapid: litconfig: center: [-74.0118, 40.7075] zoom: 15.9 pitch: 60 mapStyle: "https://tiles.openfreemap.org/styles/liberty"light: anchor: map position: [1.15, 239, 30] # distance, direction from north, angle from overhead intensity: 0.5sources: omt: type: vector url: "https://tiles.openfreemap.org/planet"layers: - id: buildings type: fill-extrusion source: omt source-layer: building paint: fill-extrusion-color: "#e5e7eb" fill-extrusion-height: ["coalesce", ["get", "render_height"], 10]A key the light doesn’t have is a validation error. The
crosshatch classic uses light: to shade
its buildings.
On export
Section titled “On export”Terrain, sky, projection and light all export to style.json as written,
replacing any the basemap had. Export notes a version difference when you
target an older maplibre-gl (Export classes).
Markers, fixed popups and images
Section titled “Markers, fixed popups and images”Markers
Section titled “Markers”markers: places pins on the map, each with an optional popup that opens
when the pin is clicked:
markers: - at: [-73.985, 40.748] color: "#e63946" popup: - h3: [{ str: "Empire State Building" }]at is [longitude, latitude]. icon: replaces the pin with an image, and
size: scales it. A marker is a page element, not a map layer: for many
points, use a circle or symbol layer, which also gets popups and hover.
mlym emit --with-fallbacks exports markers as a symbol layer with generated
pins, without their popups (Export classes).
Popups at fixed places
Section titled “Popups at fixed places”popups: opens a popup at a coordinate when the map loads, with no feature
or marker under it: a caption, a welcome note, a label for an area.
popups: - at: [-96, 37.8] closeOnClick: false # stays open when the map is clicked content: - h3: [{ str: "Hello from the middle of the US" }]content is written like any popup (Popups › Choose what the popup
shows), but there is no
feature, so property: items show nothing. Popups that open when a feature
is clicked belong on the layer:
Popups, hover & controls. Fixed popups are left out of an
exported style.json.
Images
Section titled “Images”images: names pictures for symbol layers (icon-image) and patterns
(fill-pattern, line-pattern). Each is a URL, or an object with
sdf: true for a one-colour icon you can tint with icon-color:
type: mapid: iconsconfig: center: [-73.98, 40.75] zoom: 12 mapStyle: "https://tiles.openfreemap.org/styles/liberty"images: logo: "https://maplibre.org/maplibre-gl-js/docs/assets/osgeo-logo.png" pin: url: "https://maplibre.org/maplibre-gl-js/docs/assets/custom_marker.png" sdf: truesources: places: type: geojson data: type: FeatureCollection features: - type: Feature geometry: { type: Point, coordinates: [-74.0, 40.76] } properties: { kind: logo } - type: Feature geometry: { type: Point, coordinates: [-73.96, 40.74] } properties: { kind: pin }layers: - id: logos type: symbol source: places filter: ["==", ["get", "kind"], "logo"] layout: icon-image: logo icon-allow-overlap: true - id: pins type: symbol source: places filter: ["==", ["get", "kind"], "pin"] layout: icon-image: pin icon-allow-overlap: true paint: icon-color: "#e63946" # sdf: true makes the pin tintable- Images load before the layers, so the first frame already has them.
- Keep
sdfimages and ordinary ones in separate layers, as here: MapLibre can’t draw both kinds in one symbol layer. - The image’s server must allow cross-origin requests (CORS), including
on any redirect. A map draws an image as a texture, which the browser only
allows from a server that permits it, so an image that shows fine in an
<img>can still fail here. A failed image warns once and the rest of the map draws. - On export,
mlym emitdownloads each image into the exported sprite. It can’t download a relative path, and it can’t rewrite anicon-imagecomputed from a property; it reports both. Export only documents whose image URLs you trust: the files are copied into what you publish.
Values readers can change: state and parameters
Section titled “Values readers can change: state and parameters”state: declares values the map can change after it loads. Layers read them
with MapLibre’s global-state expression. parameters: describes a control
for each value, and declaring it shows a panel of those controls on the map:
type: mapid: massingconfig: center: [-73.985, 40.748] zoom: 15.5 pitch: 55 mapStyle: "https://tiles.openfreemap.org/styles/liberty"state: scenario: default: builtparameters: scenario: label: Massing scenario type: select values: [built, flat]sources: omt: type: vector url: "https://tiles.openfreemap.org/planet"layers: - id: buildings type: fill-extrusion source: omt source-layer: building paint: fill-extrusion-color: "#9ca3af" fill-extrusion-height: - case - ["==", ["global-state", "scenario"], "built"] - ["coalesce", ["get", "render_height"], 10] - 3Choosing “flat” in the panel lowers every building to 3 metres.
- The value lives in
state:, with itsdefault.parameters:holds only how the control looks: alabel, and atypeofselect(withvalues),range(withmin,maxand an optionalstep) ortoggle. Withouttype, a parameter withvaluesis a select and atrue/falsedefault is a toggle. A parameter the panel can’t draw as a control, such as arangewithoutminandmax, shows as a read-only row. - Layers with a
label:get a checkbox in the same panel, to show and hide them;toggleable: falseleaves one out. - It needs maplibre-gl 5.6 or later. On an older version the controls are replaced by one notice, while the layer checkboxes still work.
- The panel’s changes fire
ml-map:parameter-changeandml-map:layer-visibility(<ml-map>events). - A page script reads and writes the same values with the element’s
getState()andsetState():el.setState({ scenario: "flat" })lowers the buildings and moves the panel’s select to “flat”.setStatetakes declared keys only; a key thestate:block doesn’t declare throws, and nothing is written. Every write, from a script or from the panel, fires oneml-map:state-changewithvalues(every current value) andchanged(the keys that changed); a panel write also firesml-map:parameter-changefor its key, as before (<ml-map>methods). - A reader’s choice survives a changed document. When a new document
changes a key’s
default, the map takes the new default unless a reader wrote that key (setStateor the panel), in which case the reader’s value stays; a key the new document no longer declares is removed and named inchanged. The values also survive a basemap change and any style load, includinggetMap().setStyle(). - An export for a maplibre-gl older than 5.6 replaces each value with its default (Export classes).
Controls, legend and language
Section titled “Controls, legend and language”Map controls
Section titled “Map controls”controls: adds MapLibre’s buttons: navigation, scale, fullscreen,
geolocate, globe, terrain and attribution, each in its usual corner unless
you give a position. Which controls there are, and where each goes, is in
Popups, hover & controls › Add map controls.
Every option is in the map reference.
Legend
Section titled “Legend”A map with a classified layer, or a layer with its own legend: entry, gets
a legend without asking. A top-level legend: block places and titles it,
and legend: false turns it off. How legends are built is in
Thematic maps › The legend.
Localization
Section titled “Localization”config.locale translates the text the map draws: MapLibre’s control labels
and maplibre-yaml’s own, such as the legend’s header.
Accessibility & languages
lists the keys, and how to name the map for screen readers with title: and
description:.
Troubleshooting
Section titled “Troubleshooting”- An error card says “mapStyle is required for standalone maps”: add a
mapStyle:underconfig:(The basemap). - A warning says a key “belongs at the top level”: move it out of
config:, to the same indent asconfig:. - The map opens in the wrong place, or in the ocean: the coordinates are probably latitude first. Validation suggests the swapped value.
- An image or icon doesn’t show, and the console has a CORS error: the image’s server doesn’t allow cross-origin use. Host the image on your own site.
- No sky, globe or terrain: check the console for the maplibre-gl version it needs (Runtime requirements), and tilt the camera to see the sky.
fitTodoesn’t frame anything: it works on GeoJSON, FlatGeobuf, GeoParquet and COG sources only, not on tiles.
More: Troubleshooting.
Using global default style
Section titled “Using global default style”defaultMapStyle is an option of the Astro map builders:
Astro › Global configuration.
TypeScript types
Section titled “TypeScript types”The TypeScript types for a map document are in the TypeScript API; see Format overview › TypeScript types.