Place styling

The Places module draws places in its own look, set by the marker type, colour, icon and label settings below. Each plain-valued setting is a knob in placesKnobCatalogue. Two settings from outside the module also reach these layers. The map’s accent colours markers you give no colour, and the knobs that restyle the style’s POIs restyle the marker types that borrow their look.

Marker type

markerType sets the marker every place is drawn with, at creation or at runtime:

  • pin (default) - Teardrop pins.
  • circle-icon - Circles with the category icon centred.
  • base-map - The style’s own POI look, through main, selected and micro layers, so places blend in with the map’s POIs.
    • For dense results, hide main to draw only the small micro dots.
  • pin-clustered - Pins that merge into a counted cluster when zoomed out (Clusters).
const placesModule = await PlacesModule.create(map, { markerType: 'base-map' });
placesModule.updateConfig({ markerType: 'circle-icon' });
// Micro dots only: `base-map` with a hidden `main` layer
const microPlaces = await PlacesModule.create(map, {
markerType: 'base-map',
layers: { main: { layout: { visibility: 'none' } } },
});

base-map, circle-icon and pin-clustered borrow the look of the style’s POIs. Borrowed looks lists which POI knobs restyle each marker type.

Clusters

With the pin-clustered marker type, cluster sets how places cluster and how the count badge looks. Other marker types ignore it. badge styles the badge of both single-category and mixed-category clusters, and source takes MapLibre’s cluster options:

const placesModule = await PlacesModule.create(map, {
markerType: 'pin-clustered',
// The per-place value the cluster aggregates
extraFeatureProps: {
connectorCount: (place) =>
place.properties.chargingPark?.connectors.reduce((total, { count }) => total + count, 0) ?? 0,
},
cluster: {
badge: { color: '#3f9cd9', radius: { single: 13, mixed: 18 } },
source: {
radius: 100, // default 60
maxZoom: 14, // default 17
// Total charging points across each cluster
properties: { connectors: ['+', ['get', 'connectorCount']] },
},
},
// An aggregated value lands on the cluster feature's properties
layers: { clusterCount: { layout: { 'text-field': ['get', 'connectors'] } } },
});
  • A layers override of clusterBadge, clusterCount, clusterMixedBadge or clusterMixedCount wins over badge for the paint and layout properties it names, and badge sets the rest.
  • badge.label sets the count’s color and size.
  • source takes MapLibre’s clusterRadius, clusterMaxZoom, clusterMinPoints and clusterProperties as radius, maxZoom, minPoints and properties.
  • Changing source at runtime recreates the places source and shows its places again.
  • The marker type switches clustering on, so source can’t. The clusterBaseMapIconIDs key of properties is reserved for the module’s own aggregator.

Colour

color colours the module’s own drawing: the default pin fill, the label of a hovered or clicked place, connection lines and entry points. Connection labels take a darker shade of it on a light map and a lighter one on a dark map. Entry points take the darker one on a light map.

const placesModule = await PlacesModule.create(map, { color: '#2A9D8F' });
  • A more specific setting wins for its element, such as icon.default.style.fillColor for the pin, and label.color or a layers.* paint override for a label or layer.
  • If you don’t set it, it is the map’s accent, and the markers repaint when the accent changes. Once you set it, accent changes leave them as they are.
  • Cluster badges don’t use it. cluster.badge.color sets them.

Label styling

label sets the content and look of the labels next to the markers:

import { PlacesModule, type PlaceLabelConfig } from '@tomtom-org/maps-sdk/map';
// POI names red, other places blue
const label: PlaceLabelConfig = {
title: (place) => place.properties.poi?.name ?? place.properties.address.freeformAddress,
color: ['match', ['get', 'type'], 'POI', '#D32F2F', '#1976D2'],
haloWidth: 2,
};
const placesModule = await PlacesModule.create(map, { label });
// `label` replaces the whole label: spread the current one to change a single field
placesModule.updateConfig({ label: { ...placesModule.getConfig()?.label, opacity: 0.8 } });
  • PlaceLabelConfig takes the label settings shared across modules (size, color, haloColor, haloWidth, opacity and font), plus the place’s own title and distance, how far the text sits from the icon, in ems.
  • By default, labels follow the marker type. Under base-map, they are the style’s POI labels. Under the others, they are dark text on a light halo, reversed on a dark map. A hovered or clicked place’s label takes color.
  • Set labels here, not with MapLibre’s setPaintProperty on the module’s layers. A style change, or a switch to or from pin-clustered, rebuilds those layers from the configuration and drops what was written on them. Another marker type change rewrites the properties it changes. A title computed from each place has no MapLibre equivalent.
  • A layers.* override, such as layers.main.layout['text-size'], wins over label for the properties it names. Changing a setting covers how updateConfig merges.

Icon styling

icon.categoryIcons draws a category’s places with your own image, under every marker type. Places in other categories keep the marker type’s icon.

import { PlacesModule, type PlaceIconConfig } from '@tomtom-org/maps-sdk/map';
const icon: PlaceIconConfig = {
categoryIcons: [
{ id: 'RESTAURANT', image: '/icons/food.png', pixelRatio: 2 },
// A pin whose tip sits 10 pixels below its centre
{ id: 'HOTEL_OR_MOTEL', image: '/icons/hotel.png', pixelRatio: 2, offsetY: -10 },
],
};
const placesModule = await PlacesModule.create(map, { markerType: 'pin', icon });
  • A place takes the entry for its own category, otherwise the first entry for the map category its own falls in. For example, RESTAURANT reaches an ITALIAN_RESTAURANT a search returns, and CAFE a CAFE_PUB.
    • In between, a plain sub-category such as SCHOOL_GENERIC takes the entry for its parent, SCHOOL, with or without an image.
    • The map category match takes only entries with an image, like the POIs module, so with one list, a place it reaches draws the icon of the map’s own POIs.
  • offsetX and offsetY shift the image from its coordinate, in pixels. An entry that names a sprite icon by id alone ignores them.
  • icon.default is the pin drawn for places without a matched category. style recolours it, and image replaces it. With an image, neither style nor the image’s offsets apply.
  • icon.mapping picks the icon from your own data (Your own data).

icon.opacity (0 to 1, 1 by default) fades the icons of every marker type, the cluster pin of pin-clustered included. The labels take label.opacity. The cluster badges and a highlighted or selected place stay opaque, so a selected place still stands out. Set it by its knob to keep the rest of icon:

import { placesKnobCatalogue, setKnob } from '@tomtom-org/maps-sdk/map';
setKnob(placesModule, placesKnobCatalogue, 'icon.opacity', 0.6);

Extra properties

extraFeatureProps adds properties to every place feature, for layers expressions to read with ['get', '<name>'] and for event handlers:

placesModule.updateConfig({
extraFeatureProps: {
firstCategory: (place) => place.properties.poi?.categories?.[0],
brand: (place) => place.properties.poi?.brands?.[0] ?? '',
source: 'search',
},
});

An entry is a fixed JSON value or a callback that receives a Place and returns one. If the places you show have a narrower shape, such as search results with their relevance score, name it on the config, or as the type parameter of applyConfig / updateConfig. The callbacks, label.title and icon.mapping are then typed for it:

import type { SearchPlaceProps } from '@tomtom-org/maps-sdk/core';
import type { PlacesModuleConfig } from '@tomtom-org/maps-sdk/map';
const config: PlacesModuleConfig<SearchPlaceProps> = {
extraFeatureProps: {
relevance: (place) => place.properties.score,
},
};

Layer positioning

By default, the markers sit above the map’s layers, and the connection lines below its labels. beforeLayerConfig (PlacesBeforeLayerConfig) moves them. It takes either one target for everything the module draws, 'top' or a key of mapStyleLayerIDs, or a record keyed by part, plus all for the parts it leaves out. The parts are named after the event scopes:

  • places - The markers and their labels, selected places, clusters with their badges and counts, and entry points.
    • By default, the small POI dots of the base-map and pin-clustered marker types sit below the map’s place labels. A target puts them right under the markers.
  • connections - The lines drawn by showConnections and their labels.
// The markers under the map's labels; the connections stay where they are
placesModule.updateConfig({ beforeLayerConfig: { places: 'lowestLabel' } });
  • Each part keeps its own layers in order wherever it goes. With the same target, the connections stay under the markers.
  • applyConfig or updateConfig restacks them at runtime, and resetConfig() puts them back.
  • A target the style lacks, such as lowestRoadLine on the satellite style, puts the part on top.

Every setting as data

placesKnobCatalogue lists every plain-valued display setting of the module with its kind, its range or options and, if one value holds everywhere, its default. Each id is the setting’s path in the config. The catalogue reads with no map loaded, so a UI can build its controls from it:

import { placesKnobCatalogue } from '@tomtom-org/maps-sdk/map';
const badgeKnobs = placesKnobCatalogue.filter(({ id }) => id.startsWith('cluster.badge.'));
// [{ id: 'cluster.badge.color', kind: 'color', description: '…', default: '#000000' }, …,
// { id: 'cluster.badge.radius.single', kind: 'number', description: '…', range: { min: 4, max: 32, step: 1, bounds: 'hard-min' }, default: 11 }, …]
KnobKindDefault
markerTypeenum'pin'
colorcolorthe map’s accent
visibletoggletrue
icon.default.style.fillColorcolorcolor
icon.default.style.outlineColorcolor
icon.default.style.outlineOpacitynumber
icon.opacitynumber, 0 to 11
label.sizenumber14 to 16 pixels by zoom
label.colorcolorby the map’s light/dark theme
label.haloColorcolorby the map’s light/dark theme
label.haloWidthnumber1 to 1.5 pixels by zoom
label.opacitynumber
label.distancenumberfrom the icon’s size
label.fontenumsNoto-Bold, or the style’s under base-map
label.typefaceenumthe map’s typeface
cluster.badge.colorcolor'#000000'
cluster.badge.outlineColorcolor'#FFFFFF'
cluster.badge.outlineWidthnumber1.5
cluster.badge.label.colorcolor'#FFFFFF'
cluster.badge.label.sizenumber12
cluster.badge.radius.singlenumber11
cluster.badge.radius.mixednumber15
cluster.badge.offsetoffset[12, -14]
cluster.source.radiusnumber60
cluster.source.maxZoomnumber17
cluster.source.minPointsnumber2
entryPoints.showForenum'selected'
entryPoints.minZoomnumber15
connections.colorcolorthe module’s color, otherwise the map’s accent, otherwise by the map’s light/dark theme
connections.label.colorcolora shade of the line’s colour for the map’s light/dark theme
connections.label.haloColorcolorby the map’s light/dark theme
  • A default in words is not in the entry, because the accent, the theme, the zoom or the marker type decides it.
  • setKnob, getKnob and resetKnob set, read and reset one by its id (Setting a knob by its id).
  • label.font offers mapFonts. Order matters, because the first font the map has wins.
  • label.typeface offers mapTypefaces, and sets the labels apart from the map’s typeface. A label.font wins over it.
  • cluster.badge.offset shifts a single-category cluster’s badge, in pixels. Mixed-category badges stay centred.
  • placesKnobIds lists the ids in catalogue order, PlacesKnobId is their union, and PlacesKnobValueOf<ID> is the plain value one takes. The MapLibre expression a label or badge setting also accepts is not a knob value.
  • The catalogue leaves out the label’s title and customTypeface, the cluster source’s properties, the icon images and mapping, the connections’ label, layers, beforeLayerConfig, extraFeatureProps and events, because they take callbacks, images, font files, structured or MapLibre values.
  • Entry points draw nothing until you set entryPoints, and cluster applies only under pin-clustered.
  • Its entries share their shape, KnobEntry, with every module’s catalogue.