Map theme plugin

The Map Theme plugin themes a TomTomMap from any source of a look, such as a brand’s swatches, your page’s CSS, a logo, a photo, a mood or an artist. It turns the source into the ten map colours for land, water, roads and labels, plus the accent that routes, geometries and pins take. setMapColors applies them to the whole map.

What it is for

Every source comes down to a handful of colours, and one call assigns them their roles:

The look comes fromCall
A brand guide, a palette, colours a user pickedderiveMapColors(colors)
Your own page’s CSSderiveMapColors over its custom properties
A stylesheet: a website’s, a design system’s tokensderiveMapColorsFromCss(cssText)
A logo, a photo, a posterderiveMapColorsFromImage(image)
A mood, an artista coding agent or the Agent Toolkit writes the colours, then deriveMapColors

Installation

npm install @tomtom-org/maps-sdk maplibre-gl culori @tomtom-org/maps-sdk-plugin-map-theme

@tomtom-org/maps-sdk and culori are peer dependencies. Every function is pure. Deriving needs no backend and no map.

From colours

import { StylingFoundationsModule } from '@tomtom-org/maps-sdk/map';
import { deriveMapColors } from '@tomtom-org/maps-sdk-plugin-map-theme';
const styling = await StylingFoundationsModule.get(map);
const colors = deriveMapColors(['#1d3f8a', '#e63312', '#f5d000', '#111111', '#f4f1ea']);
styling.setMapColors(colors);
  • Every role comes back, including the accent, so a UI can show what became what. setKnob(styling, stylingFoundationsKnobCatalogue, 'colors.water', …) corrects one.
  • An applied theme lives in the colors.* knobs of the Styling Foundations module. It survives setStyle and serialises with getConfig().
  • To theme the base map alone, leave accent out of the object before setMapColors.

How colours get their roles

The assignment is deterministic. Order doesn’t matter, and near-duplicates count once.

RoleGets
landThe lightest low-saturation colour (the darkest on a dark theme), toned down to a neutral
roadMajorThe most colourful (highest OKLCH chroma) colour, preferring one that isn’t blue or green
water, vegetationA blue or cyan, and a green
labelThe colour contrasting most with land, if it reaches 4.5:1
accentThe most colourful colour left that reads 3:1 on land and sits 30° of hue or more from roadMajor. Otherwise, roadMajor’s complement
  • Roles no colour fits are derived from land. Roads and outlines become shades of it, and water and vegetation become muted hues. One to eight colours always give a complete palette, and picked colours come back as given.
  • mode is 'auto' (the default), 'light' or 'dark', listed in mapThemeModes. 'auto' makes a dark theme when the colours are mostly dark. 'light' and 'dark' skip that check, but land still comes from your colours.
  • Low-saturation means an HSL saturation of 40% or less. A saturated colour becomes land only when none of your colours is low-saturation. So if white is your only low-saturation colour, mode: 'dark' keeps a white land and mapColorsLightDark returns 'light'. To keep a dark map, add a dark grey such as #141414.

Light and dark styles

setMapColors re-derives each style’s own shades at that style’s offsets, so a dark theme over streetLight pushes them to black. mapColorsLightDark(colors) tells a dark theme (labels lighter than land) from a light one, whether 'auto' or the source decided it. Use it to switch the map first:

import { mapColorsLightDark } from '@tomtom-org/maps-sdk-plugin-map-theme';
const lightDark = mapColorsLightDark(colors);
if (lightDark !== map.styleLightDarkTheme) {
await map.setStyle(lightDark === 'dark' ? 'streetDark' : 'streetLight');
}
styling.setMapColors(colors);

From your page’s CSS

To give a map your app’s brand, read the page’s custom properties and its theme-color, and theme from those.

const css = getComputedStyle(document.documentElement);
const brand = ['--color-primary', '--color-accent', '--color-surface', '--color-text']
.map((name) => css.getPropertyValue(name).trim())
.concat(document.querySelector('meta[name="theme-color"]')?.content ?? [])
.filter(Boolean);
styling.setMapColors(deriveMapColors(brand));

Use your design system’s own property names. The plugin reads colours in hex, rgb(), hsl() or a CSS name, and skips anything else.

From a stylesheet

A whole stylesheet works too, such as a website’s CSS, a design system’s tokens or a file a user uploads. deriveMapColorsFromCss(cssText, { mode, colorCount }) reads the colours it paints with and assigns them roles. It takes the CSS as text, so it needs no DOM.

import { deriveMapColorsFromCss } from '@tomtom-org/maps-sdk-plugin-map-theme';
// A File from an <input type="file" accept=".css">, or CSS a server fetched
styling.setMapColors(deriveMapColorsFromCss(await fileInput.files[0].text()));
  • The most used colour wins. A colour counts every time a rule fills with it. Borders and shadows count for less. Translucent colours, a colour’s own shades (a button’s hover) and words in url() paths don’t count.
  • var() resolves as the page does, against the rule’s own custom properties or the page root’s, not one set on another selector (.inverse { --bg: … }).
  • Greys don’t crowd out the brand. While colourful ones remain, the six colours (colorCount changes that) hold at most two greys, a background and a text colour.
  • The theme is light or dark to match the background on :root, html or body. Otherwise, it matches the grey most rules fill with. It is light when neither resolves. The plugin reads rules for one colour scheme (prefers-color-scheme, a .dark or .light class, data-theme="dark") only with that mode, and they then override the defaults. 'auto' reads the page without them.
  • The plugin doesn’t follow @imports, so concatenate the files first.

cssColors(cssText) returns the colours, most used first, without assigning roles, so the user can edit them first.

From an image

import { deriveMapColorsFromImage } from '@tomtom-org/maps-sdk-plugin-map-theme';
// A File from an <input type="file">, a Blob, an <img>, an ImageBitmap or a CORS-readable URL
styling.setMapColors(await deriveMapColorsFromImage(fileInput.files[0], { mode: 'dark' }));
  • The plugin draws the image into a 64×64 canvas in the browser, which is cheap, and its six dominant colours (colorCount changes that) get the roles. A small but striking colour can go missing.
  • A URL must be served with Access-Control-Allow-Origin. Otherwise, pass the File or Blob.
  • Without a DOM (a worker, a test, a server), decode the image yourself and pass { data, width, height } (ImagePixels, shaped like ImageData). Or hand dominantColors(pixels) to deriveMapColors.
  • Busy photos average into muted colours. Raise colorCount, or pick from dominantColors by hand.

From a mood, an artist or a website you cannot read

A mood has no colours to read. A page can’t fetch another origin’s CSS either, though a server can fetch it and hand it to deriveMapColorsFromCss. So these start with a model, not a function:

  • A coding agent reads the site’s theme-color, :root custom properties and logo, or turns “warm earthy tones” or “like a Miró” into three to six hex colours. It then writes the deriveMapColors call. The /tomtom-maps-sdk-js skill has the recipes.
  • In a conversation, the Agent Toolkit’s setMapStyling({ theme: { colors } }) does the same at run time, and theme: { imageUrl } themes from an image. It switches to the light or dark variant of the standard style the theme needs.

Limits

  • The assignment is a heuristic. It makes calls a designer wouldn’t, which is why every role comes back for correction.
  • Only the label contrast is guaranteed. A palette of near-equal lightness gives roads barely separable from land. A label derived for a mid-lightness land takes whichever side reads, even against the mode.
  • If they find no colour that parses, or none that is opaque, deriveMapColors and deriveMapColorsFromCss throw a RangeError, and deriveMapColorsFromImage rejects with one. An image that can’t be read (not CORS-readable, no DOM to decode it) rejects too. Catch the error to keep the current colours.
  • A theme recolours a TomTom style. It sets the ten map colours and the accent, and leaves single layers as the style draws them. Editing style layers covers those.

API reference

deriveMapColors · mapColorsLightDark · deriveMapColorsFromCss · cssColors · deriveMapColorsFromImage · dominantColors · mapThemeModes