← Files MapboxARCHIVED FILE

skills/mapbox-style-patterns/references/interactions.md

5.3 KB · Oct 2, 2026 · 00:26 UTC

↓ Download file

# 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