Examples
Basic map
Plot Evidence data as points
Import a plugin (lasso / draw)
How it works
The body of the tag is JavaScript that draws a map. It runs in a sandboxed iframe with three things already in scope:container— the element to render the map into.mapgl— the map library (Mapbox GL, or MapLibre GL when there is no Mapbox token). Write standard Mapbox / MapLibre code against it.evidence— your page’s data and state: run queries, read and write filters, and follow the report theme.
evidence.query, add sources and layers, and import any map plugin from a CDN. Because the code runs in an isolated iframe it cannot reach the rest of the page — it interacts with the report only through evidence. The full list of what is in scope is below.
Globals available to your code
The tag body runs as JavaScript inside an isolated iframe with these globals already in scope:Basemaps and keys
You don’t need a Mapbox key in Studio. Maps render with a Mapbox basemap and the token is applied for you, sonew mapboxgl.Map({ container }) works with no accessToken.
Where no token is configured — such as the open-source CLI or local development — maps use MapLibre GL with a free OpenFreeMap basemap instead. mapgl points at whichever library is active, so new mapgl.Map({ container }) works either way.
provider="maplibre"always uses the keyless MapLibre basemap.token="..."uses your own Mapbox account.
evidence.theme.mode:
- Mapbox —
mapbox://styles/mapbox/light-v11,mapbox://styles/mapbox/dark-v11 - MapLibre —
https://tiles.openfreemap.org/styles/positron(light),https://tiles.openfreemap.org/styles/dark
Theming panels and overlays
Custom panels, legends, and controls you add to the map match the report theme with these CSS variables — set for you and updated on light/dark toggle (a sandboxed iframe has no host CSS otherwise, so use these rather than assuming a background):var(--evidence-background)— surface backgroundvar(--evidence-foreground)— textvar(--evidence-muted-foreground)— secondary textvar(--evidence-border)— borders / dividers
evidence.theme.mode (light / dark) plus evidence.onThemeChange(cb) let you swap the basemap style to match dark mode.
Reacting to page filters and variables
A map and the rest of the page interact in three separate ways. Pick the one that matches what you need: 1. Filter the map’s data by a page input (dropdown, date range). A filter takes effect where a query references it — passing it viavariables= does not filter anything. Reference it in the SQL of the query the map runs, then evidence.query() that query:
city_sales; call evidence.query again from evidence.subscribe / evidence.filters.subscribe to redraw with the filtered rows.
2. Read a filter/variable value in map JS (labels, client-side logic — not filtering). Pass it via variables= as a quoted {{ }} expression — use .literal for the raw value or .selected for a SQL-quoted value. This gives you the value in evidence.variables; it does not filter any query.
evidence.filters and reference the filter in your query’s SQL — the large-dataset pattern where you can’t ship every row to the browser. A viewport-driven re-query on pan/zoom:
evidence.filters.create('min_lng', -130) once at startup for any filter the page’s SQL references before the user interacts.
When you write a selection back as a list of ids, an empty list is ambiguous — it can mean “nothing selected yet” or “selected, matched nothing.” Track selection state explicitly rather than checking the array length. And because id IN () is invalid SQL, write a value that matches no rows (e.g. ['__none__']) rather than an empty array when a selection matches nothing.
PDF and PNG export
Map tiles load after your setup code returns, so signal completion once the map has painted or export captures a blank frame:preserveDrawingBuffer — custom_map turns it on for you, so image export works by default. (PDF export uses a page screenshot and needs nothing extra.) On a very heavy map you can trade image export for a small perf win by opting out:
Adding plugins
Import any map-ecosystem library from a CDN — mapbox-gl-draw (lasso / box select), deck.gl, turf, h3-js, and so on. They load as normal dynamic imports. A plugin with its own UI (buttons, icons) also needs its stylesheet loaded, or its controls render blank:@maplibre/maplibre-gl-draw.
Common issues
Blank map. If you passed atoken, check it is valid — an invalid token still loads Mapbox but then fails to fetch tiles. Remove the token to fall back to the keyless MapLibre basemap.
Export shows a blank map. Tiles load after your setup code returns, so call evidence.ready() once the map has painted: map.on('idle', () => evidence.ready()).
Different basemap locally than when published. Local development and the CLI often have no Mapbox token, so they use MapLibre while Studio uses Mapbox. Set PUBLIC_MAPBOX_TOKEN locally to match, or set provider to pin one everywhere. If you rely on a Mapbox-only plugin, pin provider="mapbox".
Attributes
string
Title shown above the map
string
Subtitle shown below the title
string
Information tooltip text (can only be used with title)
string
URL to link the info text to (can only be used with info)
string
Custom text for the info link
string
Mapbox access token. Not required in Studio — a token is provided for you. Pass one to use your own Mapbox account. With no token, maps use keyless MapLibre.
string
Force a basemap provider. Defaults to Mapbox when a token is available, otherwise MapLibre.Allowed values:
mapboxmaplibre
object
Page values to expose to the map as
evidence.variables — e.g. variables={ region=$region label="static" } (Markdoc object syntax: whitespace-separated key=value, no commas). Changing a value fires evidence.onVariablesChange(cb) / evidence.subscribe(cb).number
Set the width of this component (in percent) relative to the page width
number
Set a fixed height for the chart in pixels

