Skip to content

Map Configuration

Map configuration defines the initial view, interaction settings, controls, and legends for map blocks. All configuration follows the MapLibre GL JS API.

- type: map
id: my-map
config: # Map configuration
center: [lng, lat]
zoom: 12
mapStyle: "..."
pitch: 0
bearing: 0
# ... additional options
state: {...} # Runtime-tunable values (optional)
parameters: {...} # Control metadata for state keys → params panel (optional)
markers: [...] # Standalone DOM pins (optional)
images: {...} # Named images for icons and patterns (optional)
light: {...} # Style-spec light for fill-extrusion shading (optional)
layers: [...] # Map layers
controls: {...} # Map controls (optional)
legend: {...} # Legend (optional)
PropertyTypeDescription
center[number, number]Initial map center [longitude, latitude]
zoomnumberInitial zoom level (0-24)
mapStylestring | objectMapLibre style URL or style object
config:
center: [-74.006, 40.7128]
zoom: 12
mapStyle: "https://demotiles.maplibre.org/style.json"

state: declares runtime-tunable values a map can change after it loads, read from expressions via global-state. It is a document-root key — a sibling of config:, not nested inside it — and it uses the MapLibre style spec’s own shape, { key: { default: value } }:

type: map
id: massing
state:
scenario:
default: built
config:
center: [-73.98, 40.75]
zoom: 12
layers:
- id: buildings
type: fill-extrusion
source: parcels
paint:
fill-extrusion-height:
- case
- ["==", ["global-state", "scenario"], "built"]
- ["get", "built_height"]
- ["get", "zoned_height"]

parameters: carries the presentation metadata a control UI needs to render a picker for a state key — the library owns the parameter’s shape, the host owns how it looks. It holds no default; the value lives in state:.

parameters:
scenario:
label: Massing scenario
type: enum
values: [built, potential]

Declaring parameters: renders the built-in params panel (top-right, in the same corner system as the legend): each entry becomes a control writing its state key via setGlobalStateProperty, seeded from the key’s state: default. Each entry:

PropertyTypeDescription
labelstringControl caption (defaults to the state key)
typestringrange, select (alias enum), or toggle; unrecognized types render a labeled read-only row. Omitted: inferred — values present → select, boolean default → toggle
valuesarrayThe options, for select — authored value types survive (a numeric enum writes numbers)
min / maxnumberBounds, for range (both required — a range without them renders read-only)
stepnumberIncrement, for range

The panel also lists a visibility checkbox for every layer with an authored label: (set toggleable: false to opt a labeled layer out). The label is the opt-in on purpose: toggleable defaults to true on every layer, so it cannot signal intent by itself. Layer toggles write plain layout visibility and work on every supported runtime; parameter controls need maplibre-gl ≥ 5.6 and degrade to one notice below it. Panel writes are observable as ml-map:parameter-change and ml-map:layer-visibility events.

markers: places standalone DOM pins (maplibregl.Marker) — a document-root key, a sibling of config:. Each entry:

PropertyTypeDescription
at[number, number]Marker position [longitude, latitude] (required)
colorcolorPin color (default pin only; ignored when icon is set)
sizenumberScale multiplier over MapLibre’s default pin (1 = default)
iconstringImage URL replacing the default pin
popuppopup contentPopup opened by clicking the marker — same structured content as layer popups
type: map
id: pins
config:
center: [-73.98, 40.75]
zoom: 12
markers:
- at: [-73.985, 40.748]
color: "#e63946"
popup:
- h3:
- str: Empire State Building

Markers export with a fallback (class exports-with-fallback): mlym emit --with-fallbacks lowers them to a symbol layer with generated pin sprites (the pins render; dragging and popup content do not compile), and --strict refuses marker documents. icon: URLs are fetched into the document sprite at compile time, so exported icon markers keep their images. See the export classes reference.

popups: opens standalone popups at coordinates: a popup with no layer under it and no marker above it (MapLibre’s new Popup().setLngLat(...) .addTo(map)). It is a document-root key, a sibling of config:. Each entry:

PropertyTypeDescription
at[number, number]Popup anchor [longitude, latitude] (required)
contentpopup contentSame structured content as layer popups (required)
closeButtonbooleanShow the close button (MapLibre default: true)
closeOnClickbooleanClose when the map is clicked (MapLibre default: true)
maxWidthstringCSS max-width, e.g. "300px" (MapLibre default: 240px)
type: map
id: hello
config:
center: [-96, 37.8]
zoom: 3
popups:
- at: [-96, 37.8]
closeOnClick: false
content:
- h1:
- str: Hello World!

Popups are open from load. Content runs through the same trust gate as every other popup: tags are allowlisted, str: values are escaped, and there is no feature, so property: lookups resolve empty. A marker with its pin hidden can’t stand in for this. A marker’s popup opens on a click on the pin, so a pinless marker’s popup could never open.

Popups don’t export (class no-export). mlym emit reports them as a contract warning, and the emitted style has no popups.

images: declares named images for symbol layers and patterns — a document-root key, a sibling of config:. Each entry is a URL string or an options object:

PropertyTypeDescription
urlstringImage URL (required; a bare string is shorthand for { url })
sdfbooleanTreat as a signed-distance-field icon (tintable via icon-color)
pixelRationumberSource density — 2 means the file is @2x its CSS size
type: map
id: iconic
config:
center: [0, 0]
zoom: 2
images:
poi: https://example.com/poi.png
arrow:
url: https://example.com/arrow.png
sdf: true
layers:
- id: spots
type: symbol
source: pts
layout:
icon-image: poi

Live, every image loads before layers are added, so references resolve on first render. Images load with crossOrigin="anonymous" (a WebGL texture must be untainted), so the host must allow cross-origin access on every response in the chain, redirects included — a host that serves plain <img> tags fine can still fail here. A failed image warns once and the document keeps rendering. On export the construct fully compiles (class exports): mlym emit fetches each URL into the document sprite and rewrites literal references (icon-image, fill-pattern, line-pattern, background-pattern, fill-extrusion-pattern) to mlym:<name> — --strict accepts image documents. Dynamic references (["image", ["get", "icon"]]) resolve live but cannot be rewritten at compile time; emit reports them, and runtime values naming declared images must spell mlym:<name> themselves. A *-pattern (or icon-image) may be an expression that selects among declared names — fill-pattern: [match, [get, class], sand, earth, landuse] — and every literal output is rewritten, with no warning. Relative URLs render live but cannot be fetched at compile time — emit reports them as lossy, so --strict refuses them.

Three document-root keys, siblings of config:, author the map-level 3D scene. Each one is the style spec’s own root property, in the spec’s own shape, so what you write is exactly what an exported style.json carries. In a format-v2 document they live under style: instead (style.terrain, style.sky, style.projection).

type: map
id: alps
config:
center: [11.39, 47.27]
zoom: 12
pitch: 70
maxPitch: 85
mapStyle: "https://demotiles.maplibre.org/style.json"
sources:
terrainSource:
type: raster-dem
url: "https://tiles.mapterhorn.com/tilejson.json"
encoding: terrarium
terrain:
source: terrainSource
exaggeration: 1.5
sky:
sky-color: "#199EF3"
horizon-color: "#ffffff"
fog-color: "#ffffff"
fog-ground-blend: 0.1
atmosphere-blend: ["interpolate", ["linear"], ["zoom"], 0, 1, 5, 1, 7, 0]
projection:
type: globe
controls:
navigation: true
globe: true
terrain: true
PropertyTypeDescription
sourcestringName of a raster-dem source: one in sources:, or one the basemap supplies (required)
exaggerationnumber ≥ 0Vertical exaggeration (spec default 1)

Declare the DEM as a named source. Give a hillshade layer its own DEM source rather than sharing the terrain’s; one source serving both renders at reduced quality. A source that names nothing, or a non-DEM source, warns once and the map renders flat.

PropertyTypeDescription
sky-colorcolor or expressionThe sky above the horizon
horizon-colorcolor or expressionThe band where sky meets fog
fog-colorcolor or expressionFog over distant 3D terrain
sky-horizon-blend0–1 or expressionHow far the horizon color blends up into the sky
horizon-fog-blend0–1 or expressionHow far the horizon color blends down into the fog
fog-ground-blend0–1 or expressionWhere fog starts: 0 at the camera, 1 at the horizon
atmosphere-blend0–1 or expressionGlobe atmosphere opacity; draws under globe projection only

sky: {} turns on MapLibre’s default sky. The sky is only visible when a pitched camera can see the horizon, and fog only affects 3D terrain.

PropertyTypeDescription
typemercator | globemercator is the default

The live map applies each key with its MapLibre setter. Each setter is feature-detected: on a runtime that lacks one, the document logs one console warning naming the minimum version and keeps rendering without that effect. It never throws.

KeyNeeds maplibre-glOn an older runtime
terrain:2.2.0 (setTerrain)Every supported peer version has it
sky:4.5.0 (setSky)Renders without sky or fog
sky.atmosphere-blend5.0.0 (draws under globe only)No atmosphere (there is no globe)
projection: globe5.0.0 (setProjection)Renders in mercator
controls.globe5.0.0 (GlobeControl)The control is omitted

The minimums come from the maplibre-gl changelog: 3D terrain arrived in 2.2.0, the spec sky in 4.5.0, and globe projection with its atmosphere in 5.0.0.

All three keys are class exports. mlym emit copies them verbatim to the style.json root. When the basemap also defines one, the document’s value replaces the basemap’s wholesale, as it does live, and emit notes the override. A terrain.source that resolves to no raster-dem source, after the basemap merge, is dropped with a lossy warning rather than shipped as a style MapLibre would reject. With --target below a key’s floor, emit keeps the key (it still validates on 4.x) and reports the visual difference as lossy, so --strict --target 4.7.1 refuses a globe document. See the export classes reference.

light: is the style-spec root light — the single light that shades fill-extrusion faces. A document-root key, a sibling of config: (in format v2 it lives in the style: half). Every property is optional and accepts a zoom expression:

PropertyTypeDefaultDescription
anchormap | viewportviewportmap fixes the light to the map, so shading stays put as the map turns; viewport fixes it to the screen
position[radial, azimuthal°, polar°][1.15, 210, 30]Distance from the base of an object, direction clockwise from north (at anchor: map), and angle from overhead
colorcolorwhiteLight color
intensitynumber 0–10.5Higher is more extreme contrast between faces
type: map
id: lit
config:
center: [-74.0118, 40.7075]
zoom: 15.9
pitch: 60
light:
anchor: map
position: [1.15, 239, 30]
intensity: 0.5

The object is closed: an unknown key is a validation error, not a silently ignored setting. Live, the renderer applies it with map.setLight once the style loads. On export it compiles through verbatim to the style’s light (class exports), replacing any light the basemap carried. The crosshatch classic uses it to shade its hatched buildings.

Control the camera position and orientation.

PropertyTypeDefaultRangeDescription
pitchnumber00-85Camera tilt angle in degrees
bearingnumber0-180 to 180Camera rotation in degrees
boundsarray--Fit map to geographic bounds
fitToobject--Fit the initial camera to a GeoJSON source’s data (see below)
config:
center: [-122.4194, 37.7749]
zoom: 15
pitch: 60
bearing: 30
mapStyle: "https://demotiles.maplibre.org/style.json"
config:
center: [-74.006, 40.7128] # Fallback center
zoom: 12 # Fallback zoom
bounds: [-74.3, 40.5, -73.7, 40.9] # [west, south, east, north]
mapStyle: "https://demotiles.maplibre.org/style.json"

fitTo is the data-driven sibling of bounds. Instead of literal coordinates it names a GeoJSON entry in sources:, and the camera frames whatever that source holds.

PropertyTypeDescription
sourcestringName of a GeoJSON entry in sources: (required)
paddingnumberPadding in pixels around the fitted bounds
maxZoomnumberUpper zoom limit for the fit. Without it, a single point zooms all the way in.
type: map
id: route
config:
center: [-77.02, 38.89] # Before the data arrives, and the fallback
zoom: 12
fitTo:
source: route
padding: 20
sources:
route:
type: geojson
url: https://example.com/route.geojson
layers:
- id: route
type: line
source: route
  • Inline data:: the map is constructed already framed, with no camera jump.
  • url:: the map opens at center/zoom, then frames the data when the first fetch lands. Refreshes don’t re-fit, because the user owns the camera after load. An ml-map:camera-fit event fires when the fit applies.
  • vector / raster / raster-dem: not supported. Tiled sources have no client-side extent. The renderer warns once and keeps center/zoom.
  • fitTo wins over bounds when both are set. In format v2 it lives at runtime.fitTo.

On export fitTo exports with a fallback (class exports-with-fallback). mlym emit --with-fallbacks computes the fitted center/zoom from an inline source for a 1024×768 reference viewport. That’s lossy, because a live map fits its own container. For a fetched source the authored camera stands in, also reported as lossy. --strict refuses fitTo documents.

Limit user interaction to specific zoom levels, pitch angles, or geographic areas.

PropertyTypeDescription
minZoomnumberMinimum zoom level (0-24)
maxZoomnumberMaximum zoom level (0-24)
minPitchnumberMinimum pitch angle (0-85)
maxPitchnumberMaximum pitch angle (0-85)
maxBoundsarrayMaximum geographic bounds [west, south, east, north]
config:
center: [-74.006, 40.7128]
zoom: 12
minZoom: 8
maxZoom: 18
maxBounds: [-74.3, 40.5, -73.7, 40.9]
mapStyle: "https://demotiles.maplibre.org/style.json"

Control user interaction capabilities.

PropertyTypeDefaultDescription
interactivebooleantrueEnable all map interactions
scrollZoombooleantrueEnable scroll to zoom
boxZoombooleantrueEnable shift+drag box zoom
dragRotatebooleantrueEnable right-drag to rotate
dragPanbooleantrueEnable drag to pan
keyboardbooleantrueEnable keyboard shortcuts
doubleClickZoombooleantrueEnable double-click zoom
touchZoomRotatebooleantrueEnable touch zoom/rotate
touchPitchbooleantrueEnable two-finger touch pitch

Create a static map for presentation:

config:
center: [-74.006, 40.7128]
zoom: 12
interactive: false
mapStyle: "https://demotiles.maplibre.org/style.json"

Disable specific interactions:

config:
center: [-74.006, 40.7128]
zoom: 12
dragRotate: false # Disable rotation
touchPitch: false # Disable pitch on mobile
doubleClickZoom: false # Disable double-click zoom
mapStyle: "https://demotiles.maplibre.org/style.json"

Control map display and behavior.

PropertyTypeDefaultDescription
hashbooleanfalseSync map state with URL hash
attributionControlbooleantrueShow attribution control
logoPositionstring"bottom-left"MapLibre logo position
fadeDurationnumber300Fade duration in milliseconds
crossSourceCollisionsbooleantrueCheck for cross-source symbol collisions
config:
center: [-74.006, 40.7128]
zoom: 12
hash: true # Enable URL hash syncing
attributionControl: false # Hide attribution
logoPosition: bottom-right # Move logo
fadeDuration: 500 # Slower fade
mapStyle: "https://demotiles.maplibre.org/style.json"

Advanced rendering configuration.

PropertyTypeDefaultDescription
antialiasbooleanfalseEnable antialiasing
refreshExpiredTilesbooleantrueRefresh expired tiles
renderWorldCopiesbooleantrueRender multiple world copies
maxTileCacheSizenumber-Maximum tiles to cache
preserveDrawingBufferbooleanfalsePreserve drawing buffer for export
failIfMajorPerformanceCaveatbooleanfalseFail if WebGL performance is poor
config:
center: [-74.006, 40.7128]
zoom: 12
antialias: true # Smoother rendering
preserveDrawingBuffer: true # Enable map.getCanvas().toDataURL()
maxTileCacheSize: 100 # Cache more tiles
mapStyle: "https://demotiles.maplibre.org/style.json"

Customize UI text for different languages.

config:
center: [-74.006, 40.7128]
zoom: 12
locale:
"AttributionControl.ToggleAttribution": "Show attribution"
"FullscreenControl.Enter": "Enter fullscreen"
"FullscreenControl.Exit": "Exit fullscreen"
"GeolocateControl.FindMyLocation": "Find my location"
"NavigationControl.ResetBearing": "Reset bearing"
"NavigationControl.ZoomIn": "Zoom in"
"NavigationControl.ZoomOut": "Zoom out"
mapStyle: "https://demotiles.maplibre.org/style.json"

Configure which controls are displayed and where.

controls:
navigation: true | { enabled: true, position: "top-right" }
geolocate: true | { enabled: true, position: "top-right" }
scale: true | { enabled: true, position: "bottom-left" }
fullscreen: true | { enabled: true, position: "top-right" }
attribution: true | { enabled: true, position: "bottom-right" }
ControlDefault PositionDescription
navigationtop-rightZoom and rotation controls
geolocatetop-rightUser geolocation button
scalebottom-leftDistance scale indicator
fullscreentop-rightFullscreen toggle
attributionbottom-rightAttribution text
globetop-rightGlobe/mercator projection toggle (maplibre-gl ≥ 5.0.0; older runtimes warn and omit it)
terraintop-right3D terrain on/off toggle for the document’s terrain: (warns and is omitted when the document declares none)
  • top-left
  • top-right
  • bottom-left
  • bottom-right
controls:
navigation: true
geolocate: true
scale: true
fullscreen: true
controls:
navigation:
enabled: true
position: top-left
scale:
enabled: true
position: bottom-right
fullscreen:
enabled: true
position: top-right
controls:
navigation: true
scale: true

Display a map legend showing layer information.

legend:
title: "Map Legend"
position: top-left
collapsed: false
items:
- color: "#ff0000"
label: "Category 1"
shape: circle
- color: "#00ff00"
label: "Category 2"
shape: square
PropertyTypeDefaultDescription
positionstring"top-left"Legend position
titlestring-Legend title
collapsedbooleanfalseStart collapsed
itemsarray-Custom legend items
PropertyTypeRequiredDefaultDescription
colorstringYes-Item color (CSS color)
labelstringYes-Item label text
shapestringNo"square"Symbol shape: circle, square, line, icon
iconstringNo-Icon name or URL (when shape: icon)

If items is not provided, legend items are extracted from layers with legend configuration:

legend:
title: "Earthquake Magnitude"
position: top-left
legend:
title: "Features"
position: top-right
collapsed: false
items:
- color: "#e74c3c"
label: "High Priority"
shape: circle
- color: "#f39c12"
label: "Medium Priority"
shape: circle
- color: "#2ecc71"
label: "Low Priority"
shape: circle
legend:
title: "Routes"
position: bottom-left
items:
- color: "#3b82f6"
label: "Highway"
shape: line
- color: "#10b981"
label: "Local Road"
shape: line
legend:
title: "Points of Interest"
position: top-left
items:
- color: "#000000"
label: "Restaurant"
shape: icon
icon: "restaurant"
- color: "#000000"
label: "Hotel"
shape: icon
icon: "hotel"
- type: map
id: basic-map
config:
center: [-74.006, 40.7128]
zoom: 12
mapStyle: "https://demotiles.maplibre.org/style.json"
layers:
- id: points
type: circle
source:
type: geojson
url: "https://example.com/points.geojson"
paint:
circle-radius: 8
circle-color: "#3b82f6"
controls:
navigation: true
scale: true
- type: map
id: 3d-map
config:
center: [-122.4194, 37.7749]
zoom: 15
pitch: 60
bearing: -45
mapStyle: "https://demotiles.maplibre.org/style.json"
layers:
- id: buildings
type: fill-extrusion
source:
type: vector
url: "https://demotiles.maplibre.org/tiles/tiles.json"
source-layer: building
paint:
fill-extrusion-color: "#aaa"
fill-extrusion-height: ["get", "height"]
fill-extrusion-opacity: 0.8
controls:
navigation:
enabled: true
position: top-left
fullscreen:
enabled: true
position: top-right
- type: map
id: constrained-map
config:
center: [-74.006, 40.7128]
zoom: 12
minZoom: 8
maxZoom: 16
maxBounds: [-74.3, 40.5, -73.7, 40.9]
mapStyle: "https://demotiles.maplibre.org/style.json"
layers:
- id: zones
type: fill
source:
type: geojson
url: "https://example.com/zones.geojson"
paint:
fill-color:
- match
- ["get", "type"]
- "residential"
- "#fef3c7"
- "commercial"
- "#dbeafe"
- "industrial"
- "#f3e8ff"
- "#e5e7eb"
fill-opacity: 0.6
controls:
navigation: true
scale: true
legend:
title: "Zone Types"
position: top-right
items:
- color: "#fef3c7"
label: "Residential"
shape: square
- color: "#dbeafe"
label: "Commercial"
shape: square
- color: "#f3e8ff"
label: "Industrial"
shape: square
- type: map
id: static-map
style: "height: 400px; width: 600px;"
config:
center: [0, 20]
zoom: 2
interactive: false
attributionControl: false
mapStyle: "https://demotiles.maplibre.org/style.json"
layers:
- id: earthquakes
type: circle
source:
type: geojson
url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson"
paint:
circle-radius: 6
circle-color: "#e74c3c"
circle-opacity: 0.8
- type: map-fullpage
id: immersive-map
config:
center: [-122.4194, 37.7749]
zoom: 13
pitch: 45
bearing: 0
hash: true
mapStyle: "https://demotiles.maplibre.org/style.json"
layers:
- id: buildings
type: fill-extrusion
source:
type: vector
url: "https://demotiles.maplibre.org/tiles/tiles.json"
source-layer: building
paint:
fill-extrusion-color: "#aaa"
fill-extrusion-height: ["get", "height"]
controls:
navigation: true
geolocate: true
fullscreen: true
scale: true

Define a default map style once in global config:

config:
defaultMapStyle: "https://demotiles.maplibre.org/style.json"
pages:
- path: "/"
title: "Home"
blocks:
- type: map
id: map1
config:
center: [-74, 40]
zoom: 12
# mapStyle inherited from global config
- type: map
id: map2
config:
center: [139, 35]
zoom: 12
# mapStyle inherited from global config
import {
type MapConfig,
type ControlsConfig,
type LegendConfig,
type MapBlock
} from '@maplibre-yaml/core/schemas';
const mapConfig: MapConfig = {
center: [-74.006, 40.7128],
zoom: 12,
mapStyle: "https://demotiles.maplibre.org/style.json",
pitch: 45,
bearing: 0
};
const controls: ControlsConfig = {
navigation: true,
scale: {
enabled: true,
position: "bottom-right"
}
};
const legend: LegendConfig = {
title: "Map Legend",
position: "top-left",
collapsed: false,
items: [
{ color: "#ff0000", label: "High", shape: "circle" }
]
};