Map styling
The Styling Module restyles the base map, its POIs and the traffic overlays through semantic knobs: bigger labels, thicker roads, no exit numbers, orange for slow traffic. It sits between hiding whole layer groups with the Base Map Module and editing individual MapLibre layers by hand — and unlike either, it is designed to be read by machines: the whole catalogue of knobs is available as data.
Initialization
import { TomTomMap, StylingModule } from '@tomtom-org/maps-sdk/map';
const map = new TomTomMap({ mapLibre: { container: 'map' } });
const styling = await StylingModule.get(map);
The module is shared per map: a second get() returns the same instance. Pass settings to apply them right away, for instance restored from storage:
const styling = await StylingModule.get(map, { 'labels.sizeFactor': 1.2, 'buildings.3d': true });
Knobs
Every adjustable thing is a knob with a stable id. Set one with set, read it with get, restore the style's own value with reset:
styling.set('labels.sizeFactor', 1.3); // every base-map label 30% bigger
styling.set('roads.widthFactor', 0.8); // thinner roads, hierarchy kept
styling.set('roads.exitNumbers', false); // no exit number badges
styling.set('buildings.3d', true); // extruded buildings
styling.set('pois.minZoom', 10); // POIs appear from zoom 10
styling.set('traffic.flow.slowColor', '#f59e0b');
styling.get('roads.widthFactor'); // 0.8
styling.reset('roads.widthFactor'); // back to the style's width
styling.reset(); // everything back to the style as shipped
Values are validated before anything is touched: a factor outside its range or a string that is not a colour throws a RangeError that names the knob and its range.
| Group | Knobs | Kind |
|---|---|---|
| Labels & symbols | labels.sizeFactor, symbols.sizeFactor |
factor 0.5–1.5 |
| Roads | roads.widthFactor |
factor 0.5–1.5 |
roads.exitNumbers, roads.shields, roads.arrows, roads.restricted, roads.underConstruction |
toggle | |
| Buildings | buildings.footprints, buildings.3d |
toggle |
| POIs | pois.sizeFactor |
factor 0.5–1.5 |
pois.minZoom (3–18), pois.zoomShift (−3–3) |
number | |
pois.labelColor, pois.labelOutlineColor |
colour | |
pois.microMarkers |
toggle | |
| Traffic flow | traffic.flow.freeColor, slowColor, queueingColor, stationaryColor, closedColor |
colour |
traffic.flow.widthFactor |
factor 0.5–2 | |
| Traffic incidents | traffic.incidents.minorColor, moderateColor, majorColor, closedColor |
colour |
traffic.incidents.widthFactor |
factor 0.5–2 | |
| View | view.projection (mercator | globe) |
enum |
view.sky, view.terrain |
toggle | |
view.skyColor, view.horizonColor, view.spaceColor |
colour | |
view.terrainExaggeration (0.5–3) |
number |
Colour knobs re-derive the shades the style built from that colour: recolouring slow traffic recolours its darker outline by the same offset, so the tube keeps reading as designed. Ranges follow TomTom Map Maker's validated grammar, so a value that looks right in the designer's tool looks right here.
Globe, sky and 3D terrain
The view knobs own the map-level MapLibre state that a style switch would otherwise reset — projection, sky and terrain — so a globe stays a globe across setStyle:
styling.set('view.projection', 'globe');
styling.set('view.sky', true); // MapLibre's default sky is fully transparent: a globe with no atmosphere
styling.set('view.terrain', true); // needs the hillshade style part (included by default) and a tilted camera
styling.set('view.terrainExaggeration', 1.5);
The sky follows the style's light/dark theme: a daylit blue with a white horizon over a light style, a night blue with a dim horizon over a dark one. view.skyColor and view.horizonColor override either, and a style that declares its own sky wins over both.
view.spaceColor is the colour behind the map — what a globe leaves visible around the planet. The map canvas is transparent there, so something has to fill it. Where your page already gives the map container a background, that colour is the knob's default and what a reset returns to, so the SDK never paints over your own styling. Where it does not, the knob follows the style theme as the sky does, and is re-applied after a setStyle, so switching light to dark carries the surround with it:
styling.set('view.spaceColor', '#05070d'); // or leave it to follow the style theme
Three things worth knowing, measured rather than assumed: the globe flattens to Mercator between zoom 11 and 12 by design; without view.sky a globe has no atmosphere; and MapLibre's terrain fog starts at pitch 60, which is also its default maxPitch, so raise maxPitch on the map if you want to see it. See the Globe and 3D terrain example and the 3D terrain playground for a tilted camera over relief that outlives a style switch. The map effects plugin adds a depth of field over a tilted view.
Presets
A preset is a named bundle of knob settings for a common intent — TomTom's cartographic opinions, in code. describe().presets lists them with the settings each one applies:
| Preset | Intent |
|---|---|
data-viz |
A quiet base for your own data on top: fewer labels and markers, thinner roads, no badges |
night-driving |
Bigger labels and shields, wider roads and traffic tubes, POIs out of the way until close up |
minimal |
The bare map: land, water, roads and the main place names |
globe |
Globe projection with an atmosphere |
terrain |
3D terrain with a sky, for tilted views |
styling.applyPreset('data-viz'); // replaces the current settings
styling.set('labels.sizeFactor', 1.1); // then adjust
styling.applyPreset('globe', { merge: true }); // lay a preset over what is there
A preset is exactly as durable as the knobs it sets, and re-applying one on a later SDK version keeps working for as long as those knob ids do. For the visual half of the data-viz intent — dimming and desaturating what remains — see the Map effects plugin.
Map colours: setMapColors()
The most common styling ask — "our map in our brand colours" — is one call over Map Maker's ten Foundations colours. Partial objects are fine.
styling.setMapColors({ land: '#f3f5f7', water: '#accbe2', roadMajor: '#a1b8ce', label: '#364659' });
The ten are Map Maker's own, under its names and in its order, so a colour you set here is the one you set on the same control in Map Maker. What each reaches:
| Name | Map Maker | Reaches |
|---|---|---|
land |
Land (base) | Map background and other bare-land features, influences admin borders/labels. Also used to derive all map colours if undefined. |
water |
Water | Waterbodies, waterways and other water features. Also influences glaciers, water labels and ferry lines. |
vegetation |
Vegetation | Forests and other land cover such as grass and shrubland. Also influences earth cover vegetation at smaller scales. |
park |
Park & Recreation | City parks and sport/recreation grounds. Also influences National Parks and other protected areas at smaller scales. |
artificial |
Artificial | Built-up-areas and man-made features such as landuse grounds and buildings. Also influences railways. |
roadMajor |
Major Road | Most important road classes, from motorways & trunks to primary roads. Also influences outline and tunnel colours. |
road |
Road | Less important road classes, from secondary/tertiary to streets and service roads. Also influences tunnel colours. |
roadOutline |
Road Outline | Road outlines for secondary/tertiary roads and minor classes. Also influences road labels and all other outline colours. |
label |
Label | Labels for capitals and other populated places/admin areas. Also influences border styling and all other label colours. |
labelOutline |
Label Outline | Outlines for populated places and admin area labels. Also influences border outlines and all other label halos. |
One name fans out to every layer and shade the style built from it. roadMajor recolours the motorway line, the trunk line one shade lighter, their outlines darker, the tunnel and bridge variants and the small-scale version — each re-derived from your colour by the same HSL offset the style used, so the result is a coherent recolour with the shade relationships preserved rather than a flat fill. The style is its own manifest: the offsets are measured off the loaded style, not shipped as a table, so the mechanism follows the style to its next version.
The ten colours are also knobs (colors.water, …), so describe() reports the style's own base colour as each default and the summary above as each description, reset('colors.roadMajor') restores one, and getConfig() carries the colours along with the rest of the settings. See the Map colours example.
A palette is a recolour, not an inversion. Because every literal is re-derived by its offset from the style's own colour, a palette that stays near the loaded style's lightness lands cleanly, while one that inverts it — dark land over standardLight — pushes the derived shades to the ends of their range and washes the road hierarchy out. Pair a dark palette with standardDark, whose shades already run dark, and recolour from there:
await map.setStyle('standardDark');
styling.setMapColors({ land: '#161b27', water: '#2c486d', roadMajor: '#b68e54', label: '#dce0e5' });
Fidelity bar: a coherent recolour with preserved shade relationships — visually faithful for reasonable palettes, approximate for extreme ones. Pixel parity with a Map Maker theme is not the goal.
Exporting the style
exportStyle() returns the map's style as currently rendered — the loaded style with every knob applied — as a MapLibre style specification. Save it, diff it, serve it yourself, or load it back as a custom style. Tile URLs carry the API key the map was created with; strip or rotate it before publishing the file.
The catalogue: describe()
describe() returns the same knobs as data — kind, range, default, current value, whether it is overridden and whether it has anything to work on in the loaded style. Build a control panel from it, hand it to an AI agent, or diff two calls:
const { knobs } = styling.describe();
for (const knob of knobs) {
// knob.id, knob.kind ('factor' | 'number' | 'toggle' | 'color' | 'enum'), knob.description,
// knob.range?, knob.options?, knob.default, knob.current, knob.overridden, knob.available, knob.appliesTo
}
The playground above is exactly that: its panel is generated from the catalogue, so it shows whatever the loaded style can do.
The ids are also exported as constants, with no module needed: stylingKnobIds lists every knob and stylingColorKnobIds (typed StylingColorKnobId) the colour ones. Other code names a map colour through them rather than hard-coding it, as the map effects plugin does to scope its bloom.
Two things are worth copying from it when you wire controls up. A slider or a colour picker fires input far faster than a restyle runs, so coalesce the calls into one set per animation frame (last value wins) and apply the final value on change; the readout can follow every event, since updating a label costs nothing. And if you rebuild the panel from the config-change event, skip the rebuild for changes your own controls made — set emits the event, so rebuilding mid-drag replaces the input under the pointer. Rebuild on external changes only: a reset, a setStyle, or an agent.
Settings: persist, share, re-apply
getConfig() returns the overridden knobs as a plain object; applyConfig() takes one back. It serializes cleanly, so styling can live in local storage, a URL or a pull request, and re-applying it on a later SDK version keeps working for as long as the knob ids do:
localStorage.setItem('map-styling', JSON.stringify(styling.getConfig()));
// …later
styling.applyConfig(JSON.parse(localStorage.getItem('map-styling')));
styling.events.on('config-change', (settings) => save(settings));
Style changes
Settings survive map.setStyle(...): the module re-applies them once the new style has loaded, after the other modules have restored themselves, so knobs always land on top. A clean switch (resetState: true) drops them along with the rest of the SDK state. See Map styles.
How durable is this?
Two kinds of knobs, told apart by appliesTo in the catalogue:
any-styleknobs (sizes, widths) wrap whatever the style already does —text-size: Ebecomes["*", 1.3, E]— so they need no knowledge of the style. They work on a custom style of your own too.tomtom-stylesknobs (toggles, colours, POI zoom) reach their layers through a taxonomy the TomTom styles declare, and the SDK re-verifies them against the styles it ships with. On a style that does not declare it, the knob reportsavailable: false, warns once in the console and does nothing, so you can branch on it before offering the knob at all.
That is the contract: one call the SDK re-verifies on every style release, instead of a setPaintProperty block against layer ids that nobody tests again. Which style version the SDK verified against is our business, not yours — available is the answer you need, and it is true of the style actually loaded.
Interplay with other modules
The map modules split along one line: what is shown versus how the style draws it.
BaseMapModule,TrafficFlowModule,TrafficIncidentsModule,POIsModuleandHillshadeModuledecide what is shown from one style part each: layer groups on or off, incident and POI categories, hillshade on or off.StylingModuledecides how the style draws all of it: sizes, widths, colours and zoom thresholds across the base map, the traffic tiles, the POI layers and hillshade, plus the map-level view state. It is not a base-map-only module.- Data modules (
RoutingModule,PlacesModule, geometries, BYOD) draw their own layers and style them through their own configuration. Knobs never touch those layers.
The knobs live in one module rather than on each style-part module because many of them are cross-cutting (labels.sizeFactor, roads.widthFactor, a map colour that reaches land, water, roads and labels at once), because knobs that reach the same paint property compose from one shared record of the style's original values, because they must re-apply in a single pass after every module has restored itself on a style change, and because an agent or a control panel wants one describe() and one settings object for the whole look. The per-part modules keep their small contracts and stay free of style-version coupling.
Where the two sides meet: toggles set layer visibility, as BaseMapModule.setVisible and POIsModule.setVisible do for whole groups, and the later call wins. pois.zoomShift rewrites the POI layer filters, which POIsModule.filterCategories also narrows: the two compose, so either can be set at any time and neither drops the other.
Anything else
For what the knobs do not cover, map.mapLibreMap remains the escape hatch — powerful, and coupled to the layer ids of the style version you tested against. Prefer a knob where one exists.
API reference
For complete documentation see the StylingModule API Reference.
Related guides and examples
- Map styles — switching styles,
resetState, custom styles - Base map — layer group visibility
- Traffic flow and Traffic incidents — the overlays the traffic knobs colour
- AI coding with the SDK — the agent-toolkit exposes the catalogue as
describeMapStyling/setMapStyling