Skip to content

Symbol Layer

A symbol layer renders icons, text labels, or both at point locations from a GeoJSON source. It's the style-layer equivalent of addSymbol(), but scales to any number of features and supports data-driven styling.

Ten European cities with custom marker icons and text labels, rendered from a GeoJSON source.

Add sources and layers after the style loads

Every call below needs a loaded style. Run them from onStyleLoadedCallback, not from onMapCreated, and run them there again after a style change: a new style discards every source and layer you added. See Constraints and gotchas.

Basic setup

// 1. Register a custom icon
final bytes = await rootBundle.load('assets/marker.png');
await controller.addImage('my-marker', bytes.buffer.asUint8List());

// 2. Add a GeoJSON source with point features
await controller.addGeoJsonSource('cities', {
  'type': 'FeatureCollection',
  'features': [
    {
      'type': 'Feature',
      'properties': {'name': 'Paris'},
      'geometry': {'type': 'Point', 'coordinates': [2.3522, 48.8566]},
    },
  ],
});

// 3. Add a symbol layer referencing the source
await controller.addSymbolLayer(
  'cities',
  'cities-layer',
  const SymbolLayerProperties(
    iconImage: 'my-marker',
    iconSize: 1.0,
    iconAnchor: 'bottom',
    iconAllowOverlap: true,
    textField: [Expressions.get, 'name'],
    textSize: 13,
    textOffset: [0, 1.2],
    textAnchor: 'top',
    textHaloColor: '#ffffff',
    textHaloWidth: 2,
  ),
);

Data-driven icon and text

SymbolLayerProperties(
  // Different icon per category
  iconImage: [
    Expressions.match,
    [Expressions.get, 'type'],
    'restaurant', 'restaurant-icon',
    'hotel', 'hotel-icon',
    'default-icon', // fallback
  ],

  // Text from a property
  textField: [Expressions.get, 'name'],

  // Text size driven by a rank property
  textSize: [
    Expressions.interpolate, ['linear'],
    [Expressions.get, 'rank'],
    1, 16.0,   // rank 1 -> 16px
    5, 10.0,   // rank 5 -> 10px
  ],
)

Collision behavior

Symbols automatically avoid each other. Control this:

SymbolLayerProperties(
  iconAllowOverlap: true,        // icons always show, even if overlapping
  iconIgnorePlacement: false,    // icons consider others' space
  textAllowOverlap: false,       // labels hide if they overlap
  symbolAvoidEdges: true,        // keep symbols away from map edges
)

Text layout

SymbolLayerProperties(
  textField: [Expressions.get, 'name'],
  textSize: 13,
  textAnchor: 'top',             // relative to the symbol point
  textJustify: 'center',         // left, center, right
  textOffset: [0, 1.5],          // [x, y] in ems
  textRotate: 0,                 // degrees
  textMaxWidth: 8,               // wrap at 8 ems
  textLetterSpacing: 0.05,
)

Raise symbols above the map (web)

On web, symbolHeightOffset places icons and text the given number of meters above their anchor, for labels that float over 3D buildings or terrain:

const SymbolLayerProperties(
  textField: [Expressions.get, 'name'],
  symbolHeightOffset: 30,
)

Android and iOS ignore it.

Update layer properties

await controller.setLayerProperties(
  'cities-layer',
  const SymbolLayerProperties(
    textSize: 16,
    iconSize: 1.5,
  ),
);

Filter by property

await controller.addSymbolLayer(
  'cities', 'major-cities',
  SymbolLayerProperties(...),
  filter: ['>=', ['get', 'population'], 1000000], // only cities with pop >= 1M
);

Symbols that update often

MapLibre fades symbols in as it places them. With a layer you update often, such as live vehicles, this makes updated symbols flicker. Turning placement transitions off places symbols at once, without fading. It applies to every symbol on the map, the style's labels included.

Off for the whole map

Best when your data updates every second or more often.

MapLibreMap(
  styleString: myStyle,
  symbolPlacementTransitionsEnabled: false,
  // ...
)

The base map's labels also stop fading while zooming and panning.

Off only during an update

Keeps the base map's fades.

await controller.setSymbolPlacementTransitionsEnabled(false);
await controller.setGeoJsonSource('vehicles', data);
await controller.setSymbolPlacementTransitionsEnabled(true);

Android

If your style declares no transition, symbols stop fading after the first update. Add "transition": {"duration": 300, "delay": 0} to the style to keep them.

See Live Symbols (Placement Transitions) in the example app.

Key SymbolLayerProperties fields

Property Description
iconImage Icon name (registered via addImage)
iconSize Scale factor (1.0 = original)
iconAnchor Which part of icon touches the point
iconOffset [x, y] pixel offset
iconColor Tint color (SDF icons only)
iconAllowOverlap Show even if overlapping other icons
textField Label text (string or expression)
textSize Font size in pixels
textColor Label color
textHaloColor / textHaloWidth Outline around text
textAnchor Anchor relative to point
textOffset [x, y] offset in ems
symbolPlacement point or line

Key APIs