Skip to content

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.

earthquakes.yaml
# yaml-language-server: $schema=https://docs.maplibre-yaml.org/schema/latest/map.schema.json
type: map
id: earthquakes-week
title: "Earthquakes this week" # the map's name, for screen readers
description: "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: mag
markers:
- at: [-122.2585, 37.8719]
color: "#2563eb"
popup:
- p: [{ str: "Seismology lab, UC Berkeley" }]
controls:
navigation: true
scale: true
A week of earthquakes, a marker, two controls and a legend

Edit this map in the playground

Only 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: 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 is the map under your layers: streets, terrain, labels. Set it with config.mapStyle, in one of four forms:

FormExampleUse it for
A style URLmapStyle: "https://tiles.openfreemap.org/styles/liberty"A hosted basemap (OpenFreeMap, MapTiler, Stadia …) or a style.json on your site
A style objectmapStyle: { version: 8, sources: {…}, layers: […] }A small style written in the document
A Protomaps basemapmapStyle: { pmtiles: data/nyc.pmtiles, flavor: dark }A full basemap from one .pmtiles file, with no API key or tile server
A .pmtiles pathmapStyle: data/nyc.pmtilesThe 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).

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]?

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.

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: map
id: alpine-countries
config:
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: 9
sources:
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 center and zoom, then frames the data when it arrives. The element’s fitToWaitMs property changes the wait. ml-map:camera-fit fires 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: 40 sets it in pixels; padding: 0 frames the exact extent.
  • maxZoom keeps 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. fitTo on one warns once and keeps center and zoom.
  • fitTo: countries is short for fitTo: { source: countries }. When both fitTo and bounds are set, fitTo wins. A refresh of the data doesn’t re-fit: after the map opens, the reader owns the camera.

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"

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: 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: map
id: alps
config:
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: terrarium
terrain:
source: elevation
exaggeration: 1.5
sky:
sky-color: "#199EF3"
horizon-color: "#ffffff"
fog-color: "#ffffff"
fog-ground-blend: 0.1
projection:
type: globe
controls:
navigation: true
terrain: true # a 3D on/off button
  • Set encoding. It defaults to mapbox, even when the elevation source’s TileJSON says otherwise. Mapterhorn’s tiles are terrarium.
  • Give a hillshade layer its own raster-dem source. One source serving both terrain and a hillshade layer 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; mercator is the default.
  • A terrain.source that names no raster-dem source warns once, and the map stays flat.

Gallery: 3D terrain, Sky, fog, terrain, Display a globe with an atmosphere.

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:

KeyNeeds maplibre-glOn an older runtime
terrain:2.2Every supported version has it
sky:4.5No sky or fog
sky.atmosphere-blend5.0 (globe only)No atmosphere
projection: globe5.0A flat (mercator) map
controls.globe5.0No 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: map
id: lit
config:
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.5
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: "#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.

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: 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: 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: 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: map
id: icons
config:
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: true
sources:
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 sdf images 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 emit downloads each image into the exported sprite. It can’t download a relative path, and it can’t rewrite an icon-image computed 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: map
id: massing
config:
center: [-73.985, 40.748]
zoom: 15.5
pitch: 55
mapStyle: "https://tiles.openfreemap.org/styles/liberty"
state:
scenario:
default: built
parameters:
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]
- 3

Choosing “flat” in the panel lowers every building to 3 metres.

  • The value lives in state:, with its default. parameters: holds only how the control looks: a label, and a type of select (with values), range (with min, max and an optional step) or toggle. Without type, a parameter with values is a select and a true/false default is a toggle. A parameter the panel can’t draw as a control, such as a range without min and max, shows as a read-only row.
  • Layers with a label: get a checkbox in the same panel, to show and hide them; toggleable: false leaves 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-change and ml-map:layer-visibility (<ml-map> events).
  • A page script reads and writes the same values with the element’s getState() and setState(): el.setState({ scenario: "flat" }) lowers the buildings and moves the panel’s select to “flat”. setState takes declared keys only; a key the state: block doesn’t declare throws, and nothing is written. Every write, from a script or from the panel, fires one ml-map:state-change with values (every current value) and changed (the keys that changed); a panel write also fires ml-map:parameter-change for 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 (setState or the panel), in which case the reader’s value stays; a key the new document no longer declares is removed and named in changed. The values also survive a basemap change and any style load, including getMap().setStyle().
  • An export for a maplibre-gl older than 5.6 replaces each value with its default (Export classes).

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.

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.

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

  • An error card says “mapStyle is required for standalone maps”: add a mapStyle: under config: (The basemap).
  • A warning says a key “belongs at the top level”: move it out of config:, to the same indent as config:.
  • 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.
  • fitTo doesn’t frame anything: it works on GeoJSON, FlatGeobuf, GeoParquet and COG sources only, not on tiles.

More: Troubleshooting.

defaultMapStyle is an option of the Astro map builders: Astro › Global configuration.

The TypeScript types for a map document are in the TypeScript API; see Format overview › TypeScript types.