Binding Data-Driven Properties to Vector Layers
Bind data-driven style properties by retaining the required attribute keys at tile-generation time, then referencing them in MapLibre GL expressions inside the layer’s paint or layout block — the renderer evaluates each expression against the feature’s property dictionary at draw time, with no tile regeneration required.
When to use this technique
Use data-driven expressions whenever visual properties — color, width, opacity, label text — must vary per feature based on an attribute value. This is the right approach when:
- You need categorically styled features (road class → line color, land use → fill color).
- You need continuously scaled properties (population density → circle radius, elevation → opacity).
- The same cached
.mbtilesor.pmtilesfile must serve multiple style themes without being regenerated. - A runtime state (maintenance alert, traffic level) must update visuals without touching the spatial database.
The alternative — a separate tile layer per style variant — costs additional storage and CDN capacity, and forces tile regeneration on every data change. Data-driven expressions eliminate that overhead by keeping geometry and attributes static in the cache while the frontend handles all visual variation.
Data flow: from attribute to rendered pixel
The diagram below shows the pipeline from source attribute through tile generation and CDN delivery to final expression evaluation in the browser.
Geometry and attributes are frozen at stage ②. Stages ③ and ④ never mutate the tile — only the style document changes.
Specification detail: expression syntax reference
MapLibre GL expressions used for data-driven binding follow a Lisp-style array syntax. The table below covers the operators relevant to per-feature property access.
| Operator | Syntax | Returns | Requires fallback? |
|---|---|---|---|
get |
["get", "key"] |
attribute value (any type) | No |
match |
["match", input, v1, r1, …, fallback] |
one of the result values | Yes — last element |
interpolate |
["interpolate", method, input, stop, val, …] |
interpolated number/color | No (clamped to last stop) |
case |
["case", cond1, r1, …, fallback] |
conditional result | Yes — last element |
step |
["step", input, default, stop, val, …] |
stepped value | No (default before first stop) |
coalesce |
["coalesce", expr1, expr2, …] |
first non-null result | No |
Version requirements: MapLibre GL JS 1.0+ and Mapbox GL JS 0.41+ support all operators above. ["distance"] and ["within"] (geometry-based) require MapLibre GL JS 3.0+.
Attribute retention at tile-generation time
Data-driven expressions fail silently if the attribute key was stripped during encoding. Tile generators drop most metadata by default to keep .pbf payloads under the 500 KB per-tile budget. You must whitelist every key you intend to reference in a style expression.
Tippecanoe
Use -y (--include) once per attribute to build an explicit allowlist. All other properties are excluded from the encoded tiles. This is the primary way to drop unused attributes and reduce tile size while keeping the keys you need for binding.
tippecanoe -o roads.mbtiles \
-y road_class \
-y speed_limit \
-y maintenance_status \
--drop-densest-as-needed \
--maximum-zoom=14 \
input.geojson
Verify the keys survived encoding before writing expressions:
tippecanoe-decode roads.mbtiles 14 8192 5432 | python3 -m json.tool | grep -E '"road_class"|"speed_limit"'
PostGIS ST_AsMVT
ST_AsMVT promotes every non-geometry column in its row input into MVT properties. Control inclusion with an explicit column list in the subquery:
SELECT ST_AsMVT(t, 'transportation', 4096, 'geom')
FROM (
SELECT
ST_AsMVTGeom(
geom,
ST_TileEnvelope(14, 8192, 5432),
4096, 0, true
) AS geom,
road_class,
speed_limit,
maintenance_status
FROM roads
WHERE geom && ST_TileEnvelope(14, 8192, 5432)
) AS t;
ST_TileEnvelope requires PostGIS 3.0+. The 4096 argument is the tile extent (standard MVT coordinate space). The 0 argument is the buffer in tile units; increase to 8–64 for polygon layers to avoid edge-clipping artifacts.
Production layer definition
The block below is a complete, copy-pasteable MapLibre GL layer that binds three paint properties to the three retained attributes. Paste it into a MapLibre GL JSON style document inside the layers array.
map.addLayer({
id: 'roads-data-driven',
type: 'line',
source: 'vector-tile-cache', // registered source pointing to your .mbtiles or .pmtiles
'source-layer': 'transportation', // must match the layer name used in tippecanoe -l / ST_AsMVT arg 2
paint: {
// categorical color: fast switch on road_class string
'line-color': [
'match',
['get', 'road_class'],
'highway', '#E53E3E',
'arterial', '#DD6B20',
'collector','#D69E2E',
'local', '#718096',
'#A0AEC0' // fallback for any unmatched category
],
// zoom-interpolated width, scaled per road class
'line-width': [
'interpolate', ['exponential', 1.2], ['zoom'],
8, ['match', ['get', 'road_class'], 'highway', 4, 1.5],
16, ['match', ['get', 'road_class'], 'highway', 10, 3]
],
// conditional opacity: dim closed roads
'line-opacity': [
'case',
['==', ['get', 'maintenance_status'], 'closed'], 0.3,
1
]
}
});
Key decisions in this block:
["match"]is a compile-time lookup table — it is faster than a chain of["=="]conditions for categorical data.["interpolate", ["exponential", 1.2], ["zoom"], …]produces smooth width growth between zoom stops; the1.2exponent matches the visual zoom-in feeling of web Mercator maps.- The nested
["match"]inside["interpolate"]is evaluated per-feature before the zoom interpolation; MapLibre compiles both into a single GPU shader pass.
Interaction effects
With --drop-densest-as-needed (Tippecanoe)
When Tippecanoe’s density-based drop flags are active, some features are removed at lower zoom levels. If your expression references an attribute that only makes sense at high zooms (e.g., maintenance_status on local roads), test that the fallback branch renders correctly on the tiles where those features no longer appear.
With filter on the layer
A filter expression narrows which features receive the paint rule. Filters and data-driven paint expressions share the same property access mechanism (["get", "key"]), but they evaluate independently. A feature invisible due to a filter still consumes no paint evaluation time:
filter: ['==', ['get', 'road_class'], 'highway'], // only highways reach the paint block
For per-attribute style validation workflows that catch expression errors before production deployment, run the MapLibre style spec validator against your full style JSON.
With ["coalesce"] for nullable attributes
If maintenance_status is absent on some features (e.g., tiles encoded before the column was added to the source schema), wrap ["get"] in ["coalesce"] to supply a safe default without triggering an expression evaluation error:
'line-opacity': [
'case',
['==', ['coalesce', ['get', 'maintenance_status'], 'open'], 'closed'], 0.3,
1
]
Performance impact
| Factor | Effect | Guideline |
|---|---|---|
Number of ["match"] branches |
O(1) lookup — compiled to a hash table by the GL driver | Up to ~30 branches before style JSON size matters more than evaluation time |
Nested expressions (["match"] inside ["interpolate"]) |
Single GPU shader pass; no meaningful overhead vs. flat expression | Prefer nesting over two separate addLayer calls |
| Attributes per feature in the tile | Each key adds ~5–15 bytes per feature in the .pbf |
Keep to ≤ 7 keys per feature to stay under 500 KB/tile at z14 urban density |
["interpolate"] with ["zoom"] |
Recalculated every zoom-delta frame during flyTo/pitchBy | Use ["step"] instead if smooth interpolation is not required — it avoids per-frame recalculation |
| Cache invalidation | Style-only changes need no tile regeneration | Bump the style document version string, not the tile URL, when only expressions change |
Attribute retention controlled by -y in Tippecanoe attribute-filtering rules is the single highest-leverage knob for tile payload size. Dropping one verbose string attribute from a dense urban dataset can reduce tile sizes by 20–40%.
Common mistakes
1. Expression evaluates to null — layer renders with fallback style only
Symptom: All features render in the fallback color; no categorical variation is visible.
Cause: The attribute key was stripped at encoding time, or there is a case mismatch between the tile property name (Road_Class) and the expression string (road_class).
Diagnosis:
# Decode a specific tile and inspect one feature's properties
tippecanoe-decode roads.mbtiles 14 8192 5432 | python3 -c "
import json, sys
data = json.load(sys.stdin)
feature = data['features'][0]
print(json.dumps(feature['properties'], indent=2))
"
Fix: Add the missing -y road_class flag to your Tippecanoe command and re-encode, or normalize the column name in your source data with ogr2ogr -sql "SELECT road_class AS road_class FROM layer".
2. ["interpolate"] throws a runtime error on non-numeric input
Symptom: MapLibre console error: Input/output pairs for "interpolate" expressions must be defined using literal numeric values.
Cause: The attribute stored in the tile is a string (e.g., "50") rather than a number, or the column was encoded as string type by Tippecanoe.
Fix: Coerce the type at encoding time:
tippecanoe -o output.mbtiles \
-y speed_limit \
-T speed_limit:int \ # --attribute-type: coerce to integer in the encoded tile
input.geojson
Or cast in the PostGIS query: speed_limit::integer AS speed_limit.
3. source-layer name mismatch — layer produces no output
Symptom: The layer is added without errors, but no geometry appears on the map. MapLibre DevTools shows the source loaded successfully.
Cause: The source-layer value in the layer definition does not match the layer name baked into the .pbf. Tippecanoe uses the input filename (without extension) as the default layer name; ST_AsMVT uses the second argument.
Diagnosis:
# List layer names in an .mbtiles file
sqlite3 roads.mbtiles "SELECT value FROM metadata WHERE name='json'" | python3 -m json.tool | grep '"vector_layers"' -A 20
# Or with the pmtiles CLI for .pmtiles output:
pmtiles show roads.pmtiles
Fix: Pass -l transportation to Tippecanoe to set an explicit layer name, and use the same string in 'source-layer': 'transportation' in every layer definition referencing that source.
Related
- MapLibre GL JSON Structure — the full style document format that hosts the
layersarray where data-driven expressions live. - Structuring MapLibre Styles for Multi-Source Tiles — how to organize a single style document that combines several vector tile sources, each with their own data-driven layers.
- Dropping Unused Attributes to Reduce Tile Size — the Tippecanoe
-yflag in depth, with payload size benchmarks across zoom levels. - Style Validation Workflows — catch expression reference errors and missing source-layer names before production deployment.