DEV Community

Chris Whong for Mapbox

Posted on

Introducing Appearances: A Simpler Way to Dynamically Style Mapbox Map Icons

Mapbox maps just got a major usability upgrade with the new appearances functionality, making it easier than ever to change icon images based on user interaction—without the verbose, error-prone workarounds of the past.

Screen capture of an example map showing feature-state and appearances

This screen capture shows appearances in action, displaying different icons for four distinct feature states:

  • default icon
  • feature hovered
  • feature selected
  • feature was previously selected

What Are Appearances?

Appearances allow you to define how a symbol layer's icons should change in response to changes in feature state (the ability to set state on specific features on a map). The appearances layer property contains one or more appearance objects defining layout properties like icon-image and the conditions on which to apply them. For example, the following symbol layer defaults to using the image hotel, but includes appearances for 3 additional icons for 3 distinct feature states (currentlySelected, hover, and hasBeenClicked).

{
  id: 'points',
  type: 'symbol',
  source: 'points',
  layout: {
    'icon-allow-overlap': true,
    'icon-image': 'hotel',
    'icon-size': 0.75
  },
  appearances: [
    {
      name: 'clicked',
      condition: [
        'boolean',
        ['feature-state', 'currentlySelected'],
        false
      ],
      properties: {
        'icon-image': 'hotel-active'
      }
    },
    {
      name: 'hovered',
      condition: [
        'boolean', 
        ['feature-state', 'hover'], 
        false
      ],
      properties: {
        'icon-image': 'hotel-hover'
      }
    },
    {
      name: 'has-been-clicked',
      condition: [
        'boolean',
        ['feature-state', 'hasBeenClicked'],
        false
      ],
      properties: {
        'icon-image': 'hotel-clicked'
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Appearances make the display logic for different states concise and easy to understand. The map style responds in a predictable way to feature state changes, so your frontend code can focus on managing state, not directly updating the map.

To make use of the appearances defined in the symbol layer above, you just need to set feature state as the user interacts with each marker.

For example, when a feature is hovered, set its hover feature state to true using an interaction:

map.addInteraction('hover-in', {
    type: 'mouseenter',
    target: { layerId: 'points' },
    handler: (e) => {
        map.setFeatureState(e.feature, { hover: true });
        map.getCanvas().style.cursor = 'pointer';
    }
});
Enter fullscreen mode Exit fullscreen mode

On click, the process is similar. The currentlySelected feature state for the clicked feature is set to true:

map.addInteraction('click', {
    type: 'click',
    target: { layerId: 'points' },
    handler: (e) => {
        // deselect the currently selected feature
        if (selectedFeature) {
            map.setFeatureState(selectedFeature, {
                currentlySelected: false
            });
        }
        // set selectedFeature and add to list of clickedFeatures
        selectedFeature = e.feature;
        clickedFeatures.push(e.feature);
        map.setFeatureState(e.feature, {
            currentlySelected: true,
            hasBeenClicked: true
        });
    }
});
Enter fullscreen mode Exit fullscreen mode

You can see the full example outlined above and try it for yourself in the Mapbox docs.

The above example shows frontend implementation for the web using Mapbox GL JS, but the same patterns apply to the Maps SDKs for iOS and Android. See the iOS and Android code examples linked at the bottom of this post.

Zoom-dependent expressions

In addition to using feature state to determine which icon to show, you can also use zoom expressions. The following layer definition uses appearances the icon-size property based on the current zoom:

{
    'id': 'points',
    'type': 'symbol',
    'source': 'points',
    'layout': {
        'icon-allow-overlap': true,
        'icon-image': 'hotel',
        'icon-size': 0.5
    },
    'appearances': [
        {
            'name': 'zoomed-in',
            'condition': ['>=', ['zoom'], 16],
            'properties': {
                'icon-size': 1.2
            }
        },
        {
            'name': 'zoomed-mid',
            'condition': ['>=', ['zoom'], 14],
            'properties': {
                'icon-size': 0.8
            }
        }
    ]
}

Enter fullscreen mode Exit fullscreen mode

Screenshot showing zoom-dependent icon size with appearances

The Old Way: Updating feature properties

Before appearances, the only way to achieve per-feature icon changes was to store the icon name in each feature's properties and update the entire GeoJSON source every time a state changed. This approach is verbose and requires a lot of client-side code to track state, update properties, and re-set the data source:

{
    'id': 'points',
    'type': 'symbol',
    'source': 'points',
    'layout': {
        'icon-allow-overlap': true,
        'icon-image': ['get', 'icon'],
        'icon-size': 0.75
    }
}
Enter fullscreen mode Exit fullscreen mode
function setFeatureIcon(id, icon) {
  const source = map.getSource('points');
  const data = getSourceData();
  if (!data) return;
  const feature = data.features.find(f => f.id === id);
  if (feature) {
    feature.properties.icon = icon;
    source.setData(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

This method is not only cumbersome, but it also does not work with vector tiles, since you cannot update the data on a vector tile source. This limitation made dynamic icon styling impossible for symbol layers visualizing a vector source.

Why Appearances Are a Game Changer

  • Declarative: Define all your icon state logic in one place, right in the layer definition.
  • No data changes required: No more manual property management or data source updates.
  • Works with vector tiles: Since appearances are based on feature-state, you can now have dynamic icons on vector tile sources, unlocking new possibilities for large-scale, performant maps.
  • Cleaner code: Less boilerplate, fewer bugs, and easier to maintain.

Try It Out and Share Feedback!

We invite you to test out the new appearances feature in your projects to make symbol layers that respond to feature state or zoom changes.

Have feedback or ideas? Let us know—your input helps us make Mapbox maps even better!

Additional resources

You can also explore full working examples in the official documentation:

Top comments (0)