Map modules
Map Modules are self-contained extensions that add specialized functionality to a TomTomMap instance.
Each module provides focused capabilities for different mapping needs — from displaying traffic information to handling user-generated content — while following a consistent pattern for initialization, configuration, and lifecycle management that makes them easy to integrate and use together.
Key features
- Composition: Modules can be combined to work together seamlessly. Whether a given module type can be instantiated more than once on the same map depends on who owns the layers it controls — see Types of modules below.
- Data management: Modules handle their own data sources and rendering
- Style integration: Modules automatically integrate with map styles and themes
- Statefulness: Modules maintain their own state and configuration independently. Modules restore their state when the map style changes.
- One configuration surface: every module takes its configuration at creation and through
applyConfig(config)(a whole configuration, replacing the current one),updateConfig(partial)(changes the given properties and keeps the rest, the same on every module) andresetConfig();getConfig()reads it back andevents.on('config-change', …)reports every change. See Changing a setting. - Smart event handling: Modules provide their own event systems for user interactions with seamless mapping to the original typed data.
- Service integration: Seamless integration with TomTom services for added data such as places, routes and geometries
- Lazy loading: Modules are loaded only when needed for optimal performance
Types of modules
There are two types of modules in the TomTom Maps SDK, depending on who owns the sources and layers the module controls. Ownership decides both the factory you call and how many instances make sense on one map.
Style-owned modules — get(map, config?)
These modules do not display data of their own. They control sources and layers that the map style already provides — the base map, POI symbols, the traffic tile layers, the terrain elevation — under source and layer IDs that are fixed and global to the style.
A style-owned module is therefore a shared controller, not a piece of content. get() is a lookup, and quite literally so: the SDK keeps one instance per map, so calling it twice returns the same module. Two components can each ask for the terrain without coordinating, and neither has to hoist a shared reference.
Calling get(map) with no configuration is a plain accessor — it changes nothing. Calling it with a configuration applies that configuration to the instance that already exists.
To handle events for only part of what a style-owned module covers, scope the events rather than creating a second module — see Scoped events.
Data-owned modules — create(map, config?)
These modules bring their own data. They add the GeoJSON sources, layers and sprite images they need and own them for their whole lifetime, with every identifier suffixed per instance.
Instances are therefore fully independent: create() builds a new module every time, and stacking several of them on one map — one set of places per search, one route per plan — is a supported thing to do. Nothing is shared or reused between them.
Per-module reference
| Module | Kind | Factory | Instances per map | Why |
|---|---|---|---|---|
| BaseMapModule | Style-owned | get() |
One per map | One global vector-tile source. To act on part of the map, scope its events with events.where({ layerGroups }) rather than creating a second module. |
| POIsModule | Style-owned | get() |
One per map | Fixed POI source and layer IDs; visibility and category filters are global to the map. |
| TrafficFlowModule | Style-owned | get() |
One per map | Fixed traffic-flow source; visibility and filters are global to the map. |
| TrafficIncidentsModule | Style-owned | get() |
One per map | Fixed traffic-incident source; visibility and filters are global to the map. |
| TerrainModule | Style-owned | get() |
One per map | Fixed elevation source; hillshade visibility and the 3D surface are global to the map. |
| PlacesModule | Data-owned | create() |
Multiple | Owns its own place source, layers and pin images, suffixed per instance. |
| RoutingModule | Data-owned | create() |
Multiple | Owns its own route, waypoint and section sources and layers, suffixed per instance. |
| GeometriesModule | Data-owned | create() |
Multiple | Owns its own geometry and label sources, suffixed per instance. |
| ReachableRangesModule | Data-owned | create() |
Multiple | Owns its own range and label sources, suffixed per instance. |
| CustomGeoJSONModule | Data-owned | create() |
Multiple | Owns the sources and layers declared in its own sources config, suffixed per instance. |
| TrafficIncidentDetailsModule | Data-owned | create() |
Multiple | Owns the GeoJSON source and layers for the incidents you fetched yourself. |
| TrafficAreaAnalyticsModule | Data-owned | create() |
Multiple | Owns the grid/heatmap source and layers it renders your analytics into. |
Who owns what
Everything visible on the map has exactly one owner, and the owner follows from who owns the layers:
| You want to change… | Owner | How |
|---|---|---|
| Whether something the loaded style draws is shown — a base-map layer group, the traffic tiles, a POI category, the hillshade, the 3D surface | The style-owned module for that part | setVisible, updateConfig({ filters }), setHillshadeVisible, setElevationEnabled |
| How the style draws it — label and icon sizes, road widths, the ten map colours, traffic colours, hillshade lighting, projection and sky | The Styling module | set(knob, value), applyPreset, setMapColors |
| Anything a data-owned module draws itself — routes, places, geometries, your GeoJSON, fetched traffic incidents, area analytics | That module, through its own configuration | create(map, config), applyConfig / updateConfig, setKnob and setVisible |
| The map's colour identity — the accent routes, places and geometries default to | The Styling module publishes it; each data-owned module reads it | setMapColors, set('colors.accent', …); a colour set on the module always wins |
| A look a data-owned module borrows from the style, such as POI-looking places or the colours of fetched traffic incidents | The Styling module sets it on the style; the module reads it and redraws its own layers | The knobs that restyle what it borrows; a setting given to the module always wins |
Two consequences:
- The rows do not cross. Styling knobs never write a data-owned module's layers, and a data-owned module never restyles the base map under it: recolouring a route is
RoutingModuleconfiguration, recolouring the roads it runs on iscolors.roadMajor. The accent and the borrowed looks are read, not written. - The first two rows meet at three points:
- Whole groups. A
basemap.<group>toggle knob (buildings.footprintsandbuildings.3dfor the two building groups,roads.shieldsforroadShields) writes throughBaseMapModule, andstyling.getreads the layers back, so it agrees withbaseMap.isVisible(…). The later call wins, across asetStyletoo:baseMap.setVisibleover a group removes that group's knob fromstyling.getConfig(), and a knob set afterwards lands on top of the base map's record. - Layers inside a module's layers.
pois.microMarkersamong the POIs, androads.arrows,roads.restricted,roads.underConstructionandroads.exitNumbersamong the base-map roads, can only hide what their module shows.falsehides those layers;truenever shows them whilePOIsModuleorBaseMapModulehides them; the module showing its layers again keeps a knob'sfalse; and unsetting the knob leaves the layers as the module has them. - POI filters.
pois.zoomShiftrewrites the POI layer filters that thePOIsModulefilters.categoriessetting narrows, and the two compose in either order.
- Whole groups. A
Each module guide opens with a What this module owns line that applies this rule to that module.
Changing a setting
Every setting changes through three calls each module has: applyConfig(config) replaces the whole configuration, updateConfig(partial) changes the top-level properties it is given, and setKnob sets one setting by its catalogue id.
updateConfigmerges one level deep. A nested object it is given replaces the one there, so to change one nested field, spread the current part or set its knob:placesModule.updateConfig({ label: { ...placesModule.getConfig()?.label, color: '#c2185b' } }); setKnob(placesModule, placesKnobCatalogue, 'label.color', '#c2185b');Named setters are for visibility:
setVisible/isVisibleand the parts that hide on their own (see Visibility under Appearance vocabulary). The only others areTrafficAreaAnalyticsModule's per-metricsetPalette,setColorStops,setHeightandsetFilters. A named setter is alwaysset<Setting>.
Saving and replaying a configuration
getConfig() is the export and applyConfig() the import: a module's configuration that went through JSON, given back to applyConfig, holds the same configuration and draws the same layers. Keep one to restore a user's map, attach it to a support report, or replay it in a test.
localStorage.setItem('places', JSON.stringify(placesModule.getConfig()));
// Later, on the same module or a new one
placesModule.applyConfig(JSON.parse(localStorage.getItem('places')));
Most modules' configurations are JSON whole, styling's knobs included. Five modules also take functions or image elements, which JSON cannot carry. Each has a type for the part that survives, which its reference page details; a replay adds the rest back in code:
| Module | Serializable part | Left out |
|---|---|---|
PlacesModule |
PlacesModuleSerializableConfig |
label.title as a function, connections.label, evAvailability.formatText, icon.mapping, extraFeatureProps functions, image elements in icon |
RoutingModule |
RoutingModuleSerializableConfig |
a custom chargingStops.icon.mapping, image elements in chargingStops.icon.customIcons |
GeometriesModule |
GeometriesModuleSerializableConfig |
transformFeaturesForDisplay |
ReachableRangesModule |
ReachableRangesModuleSerializableConfig |
title |
CustomGeoJSONModule |
CustomGeoJSONModuleSerializableConfig |
images |
JSON.stringify drops a function without a trace, but turns an image element into {}, which does not replay. Give icon images as URLs, data URIs or SVG text, and a place title as a MapLibre expression, to keep the whole configuration serializable.
const state = JSON.parse(saved); // PlacesModuleSerializableConfig
placesModule.applyConfig({ ...state, connections: { ...state.connections, label: (connection) => connection.name } });
The configuration is what a module draws with, not the data it shows: replay show() with the data too.
Appearance vocabulary
A concept has one name on every module that has it. The table lists the shared appearance terms, the rule each follows, and the exact properties that carry it; a module missing from a row does not have that concept.
| Concept | The rule | Modules and properties |
|---|---|---|
| The module's colour | A top-level color is the colour of what the module draws. Unset, it is the map's accent (colors.accent, published as map.mapColors.accent), then the module's own default. A part-level colour overrides it. |
RoutingModule: color, under layers.* paint overrides · GeometriesModule: color, under fill.palette, fill.color, line.color and a feature's color property (order) · PlacesModule: color, under icon.default.style.fillColor, label.color and layers.* |
| Labels | One label shape, LabelConfig: size, color, haloColor, haloWidth, opacity, font. Each module adds the label's content and its own offset. |
PlacesModule: label (PlaceLabelConfig, adds title, and offset as one number of ems away from the icon) · GeometriesModule: label (GeometryLabelConfig, adds text, a string or expression that defaults to the feature's title), and lineLabel (GeometryLineLabelConfig, the same text plus minZoom, symbolSpacing, and offset as [x, y] ems) · RoutingModule: chargingStops.label (ChargingStopLabelConfig: visible and a title expression, without the LabelConfig styling) |
| Multipliers | A *Factor multiplies a default and is clamped to a published range. Where a size also has presets ('s', 'm', 'l'), the preset picks the base and the factor multiplies it. |
RoutingModule: width and widthFactor (ROUTE_WIDTH_FACTOR_RANGE, 0.5 to 2), waypointSize and waypointSizeFactor (ROUTE_WAYPOINT_SIZE_FACTOR_RANGE, 0.5 to 2), sections.<type>.width and sections.<type>.widthFactor, sections.<type>.icon.sizeFactor (SECTION_ICON_SIZE_FACTOR_RANGE, 0.25 to 2), highlight.outlineWidthFactor (ROUTE_HIGHLIGHT_OUTLINE_WIDTH_FACTOR_RANGE, 1 to 1.25) · GeometriesModule: highlight.fillOpacityFactor (1 to 4) and highlight.lineWidthFactor (1 to 3) · TrafficIncidentDetailsModule: focus.widthFactor (INCIDENT_FOCUS_WIDTH_FACTOR_RANGE, 1 to 1.8) · Styling knobs: labels.sizeFactor, symbols.sizeFactor, pois.sizeFactor, roads.widthFactor, traffic.flow.widthFactor, traffic.incidents.widthFactor |
| Hovered and clicked look | A feature a handler covers, or one putEventState puts a state on, draws one look whether hovered or clicked. highlight holds the factors that look multiplies by; a factor of 1 draws the feature as any other. A MapLibre layer you give in place of the default draws as given. |
RoutingModule: highlight.outlineWidthFactor · GeometriesModule: highlight.fillOpacityFactor, highlight.lineWidthFactor · TrafficIncidentDetailsModule: its focus config, the treatment setFocus draws, focus: false to turn it off · PlacesModule: layers.selected |
| Absolute units | A value that mirrors a MapLibre property keeps MapLibre's name and is in pixels. Any other unit is in the name. Opacities run from 0 to 1, label offsets are in ems. | Pixels: LabelConfig size and haloWidth, GeometriesModule line.width and lineLabel.symbolSpacing, PlacesModule cluster.badge.strokeWidth and cluster.badge.radius · Metres: TrafficAreaAnalyticsModule maxHeightMeters, minHeightMeters and, per unit of the metric, metersPerUnit · Two exaggerations: TerrainModule elevationExaggeration scales the raised surface, the hillshade.exaggeration knob sets the shading strength |
| Marker type, fill style, map theme | Theme means only the map-wide look. How one module draws its own features has a name of its own. | PlacesModule: markerType (PlacesMarkerType; placesMarkerTypes lists pin, circle-icon, base-map, pin-clustered) · GeometriesModule: fillStyle (GeometryFillStyle; geometryFillStyles lists filled, outline, inverted), per feature properties.fillStyle · ReachableRangesModule: fillStyle, the same values · Map theme: StylingModule.setMapColors, fed by the Map Theme plugin's deriveMapColors, deriveMapColorsFromCss, deriveMapColorsFromImage and mapColorsLightDark |
| Fill colour or palette | A colour field takes a colour or a MapLibre expression, never a palette name. A palette is its own field: on geometries and reachable ranges a list of your own CSS colours, which the features or bands take one after another; on area analytics a name, ramped over the metric's value. | GeometriesModule: fill.color, fill.palette, unset drawing one colour, the accent · ReachableRangesModule: palette, unset ramping from the accent toward the map's land colour · TrafficAreaAnalyticsModule: metricConfig.<metric>.palette (AreaAnalyticsPalette; areaAnalyticsPalettes lists the names), and metricConfig.<metric>.colorStops, which wins over it |
| Zoom thresholds | minZoom and maxZoom, as on the styling knobs. |
PlacesModule: entryPoints.minZoom · GeometriesModule: lineLabel.minZoom · RoutingModule: sections.<type>.sign.minZoom, countryCrossings.minZoom · Styling knobs: pois.minZoom, hillshade.maxZoom |
| Borrowed looks | A part drawn to look like the style follows the knobs that restyle what it borrows. A look set on the module wins. | PlacesModule with markerType base-map, circle-icon or pin-clustered · RoutingModule sections.traffic · TrafficIncidentDetailsModule line colours. The knobs each one follows: Borrowed looks |
| Visibility | visible in the config, setVisible(visible) to change it, isVisible() to read it back. isVisible() returns the setting, the same value as the module's visible knob. On a data-owned module, hiding keeps the data, show() and clear() leave it hidden, and getShown() returns what is shown. On a style-owned module that nothing has set, isVisible() reads what the style draws, and the traffic modules are hidden until shown. A part that hides on its own has its own visible. |
All three on PlacesModule, RoutingModule, GeometriesModule, CustomGeoJSONModule, TrafficIncidentDetailsModule, TrafficAreaAnalyticsModule, POIsModule, TrafficFlowModule and TrafficIncidentsModule (plus icons.visible with setIconsVisible / isIconsVisible) · BaseMapModule: visible, groups.<group>.visible, setVisible and isVisible with { layerGroups } · RoutingModule per part: chargingStops.visible, chargingStops.label.visible, summaryBubbles.visible, countryCrossings.visible, sections.<type>.visible · TerrainModule: hillshade with setHillshadeVisible / isHillshadeVisible, elevation with setElevationEnabled / isElevationEnabled |
| Layer position | beforeLayerConfig takes 'top' or a named style layer such as 'lowestLabel' (BeforeLayerConfig) for all of a module's layers. A module drawing several parts also takes a record keyed by its part names, all covering the parts it leaves out (PartsBeforeLayerConfig). |
GeometriesModule: GeometryBeforeLayerConfig, parts fill, line · RoutingModule: RoutingBeforeLayerConfig, parts mainLines, instructionLines, icons · PlacesModule: PlacesBeforeLayerConfig, parts places, connections · TrafficAreaAnalyticsModule: AreaAnalyticsBeforeLayerConfig, one part per display mode · TrafficIncidentDetailsModule: BeforeLayerConfig alone · CustomGeoJSONModule: MapLibre's beforeID on each layer |
Every setting as data
A module's display settings are also published as a knob catalogue: static data, readable with no map loaded, that a control panel builds its controls from and an agent checks a value against. An entry's id is the setting's path in the module's configuration, where a number indexes a list (filters.any.0), and its kind says which value it takes and which fields come with it. The entry type is KnobEntry, and the kinds are KnobKind:
| Kind | Value | Fields |
|---|---|---|
toggle |
true or false |
|
factor |
a multiplier, 1 keeping what the style does |
range |
number |
a number | range |
offset |
an [x, y] displacement, x to the right and y down, in the unit the description names |
range, for x and y each |
color |
a CSS colour | |
enum |
one of the options | options |
enums |
a list of the options; the description says when their order matters | options |
colors |
a list of CSS colours or of the names in options |
options, where there are names |
image |
the id of an image in the style's sprite | |
text |
any string |
rangeis the span a control offers, with thestepa slider uses. Itsboundssay which ends a value must keep to:boundsA value must be For example hardbetween minandmaxan opacity, a *Factorhard-minat least min; abovemaxit still appliesa height in metres softany number an offset defaultis present where one value holds everywhere. Without it, the description says what decides: the style, the accent, the global configuration.A setting that takes a MapLibre expression, a callback or a structured object stays in the configuration. The plain-value alternatives of a setting that also takes an expression are in its entry, and they are what the module's
…KnobValueOf<ID>type names.The styling catalogue and the map effects plugin's use the same kinds.
Setting a knob by its id
setKnob sets one setting by its catalogue id and keeps every other one; a value its entry refuses changes nothing. validateKnobValue is the check alone, for a value from a form or a language model. Both throw a RangeError naming the knob and what it expects.
import { setKnob, trafficFlowKnobCatalogue, validateKnobValue } from '@tomtom-org/maps-sdk/map';
setKnob(trafficFlow, trafficFlowKnobCatalogue, 'filters.any.0.roadCategories.values', ['motorway', 'trunk']);
const knob = trafficFlowKnobCatalogue.find(({ id }) => id === 'filters.any.0.roadCategories.values');
validateKnobValue(knob, ['motorway', 'cart track']); // RangeError: … 'cart track' is not
The Styling Module's own set runs the same check. The traffic config playground sets every flow and incidents knob this way, from controls built off the two catalogues.
List of modules
Base map module
A BaseMapModule provides visibility and events for layer groups of the base map.
import { BaseMapModule } from '@tomtom-org/maps-sdk/map';
const baseMapModule = await BaseMapModule.get(map);
// Toggle visibility, for the whole base map or for specific layer groups
baseMapModule.setVisible(true);
baseMapModule.setVisible(false, { layerGroups: { mode: 'include', names: ['borders', 'buildings2D'] } });
Learn more about Base Map Module →
Places module
A PlacesModule handles search results visualization and place information display.
import { PlacesModule } from '@tomtom-org/maps-sdk/map';
import { discoverPlaces } from '@tomtom-org/maps-sdk/services';
// Initialize places module
const placesModule = await PlacesModule.create(map);
// Search and display results
const results = await discoverPlaces({ query: 'restaurants near me' });
await placesModule.show(results);
// Handle place interactions — `events.places` is the pins alone, typed as Place
placesModule.events.places.on('click', (place) => {
console.log('Selected place:', place.properties.title);
});
Learn more about Places Module →
POIs module
A POIsModule controls the display and events for Points of Interest with category-based filtering and styling.
import { POIsModule } from '@tomtom-org/maps-sdk/map';
// Display specific POI categories
const poisModule = await POIsModule.get(map, {
filters: {
categories: { show: 'all-except', values: ['CHARGING_LOCATION'] },
},
});
Learn more about POIs Module →
Routing module
A RoutingModule controls the display of routes, waypoints, and navigation instructions, and user events on such parts.
import { RoutingModule } from '@tomtom-org/maps-sdk/map';
import { calculateRoute } from '@tomtom-org/maps-sdk/services';
// Initialize routing module
const routingModule = await RoutingModule.create(map);
// Calculate and display route
const route = await calculateRoute({
locations: [
[4.9041, 52.3676], // Amsterdam
[4.4777, 51.9244] // Rotterdam
],
});
await routingModule.showRoutes(route);
Learn more about Routing Module →
Traffic flow module
The TrafficFlowModule displays real-time traffic flow information with color-coded speed indicators.
import { TrafficFlowModule } from '@tomtom-org/maps-sdk/map';
// Add traffic flow visualization
const trafficFlowModule = await TrafficFlowModule.get(map);
// Control traffic display
trafficFlowModule.setVisible(true);
// Show road closures only
trafficFlowModule.updateConfig({
filters: { any: [{ showRoadClosures: 'only' }] },
});
Learn more about Traffic Flow Module →
Traffic incidents module
The TrafficIncidentsModule shows traffic incidents like accidents, road closures, and construction.
import { TrafficIncidentsModule } from '@tomtom-org/maps-sdk/map';
// Display traffic incidents
const trafficIncidentsModule = await TrafficIncidentsModule.get(map, {
icons: { visible: false },
filters: { any: [{ magnitudes: { show: 'all-except', values: ['minor'] } }] },
});
Learn more about Traffic Incidents Module →
Geometries module
The GeometriesModule displays custom geometric data like polygons, lines, and markers.
import { GeometriesModule } from '@tomtom-org/maps-sdk/map';
// Initialize geometries module
const geometryModule = await GeometriesModule.create(map);
const geometryToSearch = await geometryData({ geometries: location });
await geometryModule.show(geometryToSearch);
Learn more about Geometries Module →
Reachable ranges module
The ReachableRangesModule displays what calculateReachableRanges returns: each band titled from its budget, each origin's bands nested, coloured from the map.
import { ReachableRangesModule } from '@tomtom-org/maps-sdk/map';
const rangesModule = await ReachableRangesModule.create(map, { fillStyle: 'outline' });
await rangesModule.show(await calculateReachableRanges([10, 20].map((value) => ({ origin, budget: { type: 'timeMinutes', value } }))));
Learn more about displaying reachable ranges →
Custom GeoJSON module
The CustomGeoJSONModule renders your own GeoJSON data with typed sources, clustering, custom images, and style-change restoration.
import { CustomGeoJSONModule } from '@tomtom-org/maps-sdk/map';
const customModule = await CustomGeoJSONModule.create(map, {
sources: {
heatmap: { layers: [{ type: 'heatmap' }] }
}
});
await customModule.show(heatmapData, 'heatmap');
Learn more about Custom GeoJSON Module →
Traffic incident details module
The TrafficIncidentDetailsModule renders a specific set of incidents you fetched yourself, rather than the live vector-tile layer.
import { TrafficIncidentDetailsModule } from '@tomtom-org/maps-sdk/map';
import { trafficIncidentDetails } from '@tomtom-org/maps-sdk/services';
const incidentDetailsModule = await TrafficIncidentDetailsModule.create(map);
const result = await trafficIncidentDetails({ bbox });
await incidentDetailsModule.show(result);
Learn more about Traffic Incident Details Module →
Traffic area analytics module
The TrafficAreaAnalyticsModule visualizes aggregated traffic statistics for an area as a hexgrid, square grid, or heatmap.
import { TrafficAreaAnalyticsModule } from '@tomtom-org/maps-sdk/map';
const analyticsModule = await TrafficAreaAnalyticsModule.create(map, {
displayMode: 'hexgrid-3d',
activeMetric: 'congestionLevel',
});
await analyticsModule.show(analyticsResult);
analyticsModule.updateConfig({ activeMetric: 'speed' });
Learn more about Traffic Area Analytics Module →
Terrain module
The TerrainModule draws the relief of the elevation data: hillshade shading, and the map surface raised in 3D.
import { TerrainModule } from '@tomtom-org/maps-sdk/map';
const terrain = await TerrainModule.get(map, { hillshade: true });
// Raise the surface in 3D, for a pitched camera
terrain.setElevationEnabled(true);