Skip to content

Expression fields

A field whose name ends in -expr takes a raw MapLibre expression — the same JSON array form MapLibre uses, not a rewrite of it.

"landuse": {
"op": "fill-solid",
"features": "@landuse_f",
"fill-expr": ["match", ["get", "kind"], "park", "#a6c084", "forest", "#8fae74", "#e8e4d8"]
}

Evaluation is by maplibre-expr at 100 % conformance against MapLibre’s official spec fixtures. See expression conformance.

Where a paint value can be data-driven, the op offers both forms, and the -expr field wins when both are present:

Literal field Expression field
fill fill-expr
color color-expr
width-px width-expr
radius-px radius-expr
opacity opacity-expr

The per-op tables in the node catalog are generated from the registry, so they are the authoritative list of which fields an op actually accepts.

An -expr does not stand in for the literal

Section titled “An -expr does not stand in for the literal”

The expression wins at paint time, but it does not satisfy the field. A required literal stays required:

// ✅ `width-px` defaults to 1, so the expression alone is enough
"roads": { "op": "stroke", "features": "@r", "color": "#000000",
"width-expr": ["interpolate", ["linear"], ["zoom"], 0, 1, 10, 4] }
// ❌ missing required field `amplitude-px`
"wob": { "op": "wave", "features": "@r", "wavelength-px": 20,
"amplitude-px-expr": ["literal", 3] }

The difference is not arbitrary. An expression is evaluated per feature while the tile is being painted, and some fields must be known before that: a field that decides canvas padding is one, since padding is fixed when the graph is built. Supply the literal as well — it is the value used when the expression is absent or fails for a feature, and for a padding-determining field it is also what the canvas is sized by. See padding and neighbours.

filter-expr on a features node is the same mechanism used as a predicate:

"water_f": { "op": "features", "layer": "water",
"filter-expr": ["all", ["==", ["get", "kind"], "water"], ["has", "name"]] }

Legacy-form filters — bare field names, !in, !has, none — are accepted and converted with MapLibre’s own pre-compile conversion, strict-type semantics included.

Where Runs Sees
a paint op’s *-expr once per feature that feature’s properties, and the tile’s zoom
filter-expr once per feature the same
the expr op once per tile the tile’s zoom (no feature)

So a ["zoom"]-only curve is cheaper as an expr node — evaluated once, emitted as a Scalar, cached like any other node:

"w": { "op": "expr", "expr": ["interpolate", ["linear"], ["zoom"], 10, 0.5, 16, 4] },
"roads": { "op": "stroke", "features": "@roads_f", "width-px": "@w" }

Zoom is the tile’s integer zoom. There is no fractional zoom, because there is no camera — a tile is rendered at its own zoom level. A MapLibre curve therefore evaluates at integer steps rather than continuously; this is the main visible consequence of tile-based rendering on zoom functions.

All three produce a number, and they differ in when the value is fixed:

"sigma": ["interpolate", ] // ❌ not a thing — expressions go on `-expr` fields
"sigma": 3 // fixed in the document
"sigma": "$softness" // overridable per render by the caller
"sigma": "@w" // computed by the graph (expr / math / zoom)

Choose $param when a caller should be able to change it (CLI flag, query string, slider). Choose an expression or a scalar chain when the value is a property of the style. See params and functions.

  • An expression is part of the node’s cache key, so using one costs nothing in cacheability.
  • heatmap-style colour functions over density are baked into a 256-entry ramp per tile (ramp-expr on color-ramp).
  • What can be type-checked statically is reported at build time; the rest is reported per feature at render time.

Map renders on this site are made fromOpenStreetMap data viaProtomaps (© OpenStreetMap contributors), elevation from Re:Earth Terrain,Mapterhorn andEGM2008 (NGA), and aerial imagery from GSI Japan(© 国土地理院). The painterly styles use CC0 brushes byDavid Revoy.