← Files MapboxARCHIVED FILE
skills/mapbox-style-patterns/references/interactions.md
5.3 KB · Oct 2, 2026 · 00:26 UTC
# Interactions: addInteraction, setFeatureState, and appearances
Three complementary mechanisms, not competing alternatives. They form a pipeline:
**`addInteraction` (bind the event) -> `setFeatureState` (flip the state) -> `appearances` or raw feature-state expressions (apply the visual change)**
## addInteraction + setFeatureState
`addInteraction` (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.
```javascript
// 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
`appearances` is a **style-spec layer property** that defines an array of named, condition-gated property bundles:
```json
{
"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](https://github.com/mapbox/mapbox-gl-js/blob/main/CHANGELOG.md) for the exact version in your SDK.
## When to use each
| 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](../SKILL.md) and [performance.md](../../mapbox-data-visualization-patterns/references/performance.md)) |
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.
SHA-256: 99c3cbb7efd3c19633142a43f746de7111b9cb0b85e37cd98441d34ef406eb23