Skill 16 · Mapbox Style Patterns
Subchapter 16.6
references/interactions.mdMarkdown5 KBView on GitHub
Three complementary mechanisms, not competing alternatives. They form a pipeline:
addInteraction (bind the event) -> setFeatureState (flip the state) -> or raw feature-state expressions (apply the visual change)
appearancesaddInteraction (GL JS v3.9+) binds click/mouseenter/mouseleave events to features, replacing map.on(event, layerId, handler). Its handler is where you call setFeatureState to record what happened.
// Custom layer: target by layerId
map.addInteraction('building-hover', {
type: 'mouseenter',
target: { layerId: 'buildings-layer' },
handler: ({ feature }) => {
if (hoveredBuilding) {
map.setFeatureState(hoveredBuilding, { highlight: false });
}
hoveredBuilding = feature;
map.setFeatureState(feature, { highlight: true });
map.getCanvas().style.cursor = 'pointer';
}
});
// Standard Style / style-import featureset: target by featuresetId + importId
map.addInteraction('place-click', {
type: 'click',
target: { featuresetId: 'place-labels', importId: 'basemap' },
handler: ({ feature }) => {
if (selectedPlace) {
map.setFeatureState(selectedPlace, { select: false });
}
selectedPlace = feature;
map.setFeatureState(feature, { select: true });
}
});
// Omit `target` for a map-wide interaction (e.g. clicking empty space to deselect)
map.addInteraction('map-click', {
type: 'click',
handler: () => {
if (selectedPlace) map.setFeatureState(selectedPlace, { select: false });
selectedPlace = null;
}
});setFeatureState(feature, state) works with any source/layer type, given a stable feature id (promoteId/generateId for GeoJSON/vector sources).
appearances is a style-spec layer property that defines an array of named, condition-gated property bundles:
{
"id": "poi-labels",
"type": "symbol",
"appearances": [
{
"name": "selected",
"condition": ["feature-state", "select"],
"properties": {
"icon-size": 1.3,
"text-color": "#ff0000",
"text-halo-color": "#c0caff"
}
},
{
"name": "highlighted",
"condition": ["feature-state", "highlighted"],
"properties": {
"icon-size": 2,
"text-color": "#4264fb"
}
}
]
}If multiple conditions are true, only the first matching appearance applies. setFeatureState(feature, { select: true }) is what drives the condition here — appearances doesn’t replace setFeatureState, it replaces writing the same ["case", ["boolean", ["feature-state", "x"], false], ...] expression into every paint property you want to bundle under one named state.
Constraint: appearances currently only works on symbol layers, and only on properties tagged “Works with appearances” (icon-*, text-*, symbol-z-offset) — it cannot be used on fill, fill-extrusion, circle, or line layers. Known issue: appearances combined with text-variable-anchor on the same layer doesn’t work correctly. Available in recent Mapbox GL JS v3.x releases — check the CHANGELOG (opens in a new tab) for the exact version in your SDK.
| Need | Use |
|---|---|
| Bind a click/hover event to a feature | addInteraction (new code, works with custom layers via layerId or Standard Style/style-import featuresets via featuresetId+importId) |
| Bind a click/hover event, targeting a v2-era or non-featureset setup | Legacy map.on(event, layerId, handler) — still works, but has no featureset targeting |
| Apply the visual change on a symbol layer, several properties at once, under one named condition | appearances — cleaner than repeating the condition per property |
Apply the visual change on fill, fill-extrusion, circle, or line, or need finer per-property control | Raw ["feature-state", ...] expressions written directly into each paint property (see SKILL.md and performance.md (opens in a new tab)) |
Either way, setFeatureState is the mechanism that actually flips the state — addInteraction decides when to call it, appearances/raw expressions decide what happens once it’s called.