Theming

Every piece of chrome ApexMaps draws itself, the legend, tooltip, breadcrumb and attribution, reads its colors from CSS custom properties on the map's container. A host dashboard restyles the map by setting those properties, with no options tree involved at all.

#dashboard-map {
  --apexmaps-surface: #1e1b4b;
  --apexmaps-focus: #f472b6;
}

Dark mode

theme.mode is 'light', 'dark', or 'auto':

theme: { mode: 'dark' }

Dark mode is applied as a class (apexmaps--dark) on the container rather than only through a prefers-color-scheme media query, so a dashboard can force it independently of the operating system. Unset, the map renders light, or dark when the apexmaps/ignite-ui add-on is loaded and the page's Ignite UI theme is a dark one. What changes between light and dark is the chrome, the no-data color and the text; the data palette itself (scale.palette) is unchanged, since a blues choropleth should read as the same blues choropleth in either mode.

From 1.3.0, 'auto' follows the OS color scheme for as long as the map is open, and switches when the OS does. Before 1.3.0 it resolved prefers-color-scheme at render only, so a map already on screen kept the mode it opened with until you re-applied it. On those versions, subscribe once:

// Only needed before 1.3.0
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () =>
  map.updateOptions({ theme: { mode: 'auto' } }),
)

theme.palette sets a default PaletteName for series that don't name their own, useful for keeping every series on a map consistent without repeating scale: { palette: 'x' } on each one.

Design tokens and the design system

From 1.3.0 a map also reads the --apx-* tokens the Apex products share, below its own --apexmaps-* tokens: --apx-series-1 to --apx-series-N supply the default categorical palette (and row 1 is the single color of a bubble, marker or line series with none of its own), and --apx-font-family the font, below chart.fontFamily. Naming a theme.palette keeps the built-in categorical defaults, so --apx-series-N no longer applies. With the apexmaps/ignite-ui add-on loaded, an Ignite UI theme on the page ranks below all of these and above the built-in defaults. Tokens are read at each render and updateOptions() call.

Two switches turn these layers off:

// Ignore the page's design system (Ignite UI): its colors, font and dark variant
theme: { designSystem: false }

// Ignore every --apx-* token, in the stylesheet and in JS, and the design system with them.
// Your --apexmaps-* tokens still apply.
theme: { tokens: false }

Both default to true. theme.designSystem has an effect only while the apexmaps/ignite-ui add-on is loaded. See Using with Ignite UI.

The --apexmaps-* custom property surface

These are the variables the library itself reads. Override any of them on the map's container or on any ancestor, globally on :root included, and every component that uses that variable follows. That reaches through a shadow root too: a value set on the host of a web component styles every map inside it. Before 1.1.1 only a value set on the map's own element applied.

The defaults in the table are fallbacks inside the library's own rules, not declared values, so page CSS that reads a variable it never set, such as var(--apexmaps-focus), gets nothing back (from 1.1.1). Give it a fallback of your own, for example var(--apexmaps-focus, #2563eb).

VariableUsed forLight defaultDark default
--apexmaps-fgPrimary text (legend, breadcrumb, tooltip title)#1f2933#e5e7eb
--apexmaps-fg-mutedSecondary text (tooltip labels, breadcrumb items)#6b7280#9ca3af
--apexmaps-bgContainer backgroundtransparenttransparent
--apexmaps-surfaceTooltip and card backgrounds#ffffff#1f2937
--apexmaps-borderBorders, legend item hover, swatch outlinesrgba(0,0,0,0.12)rgba(255,255,255,0.16)
--apexmaps-shadowTooltip drop shadow0 4px 16px rgba(15,23,42,0.14)0 4px 16px rgba(0,0,0,0.5)
--apexmaps-focusFocus ring, selection box outline, active legend accents#2563eb#60a5fa
--apexmaps-radiusTooltip and legend swatch corner radius6px6px
--apexmaps-font-sizeBase font size for legend, tooltip, breadcrumb12px12px

The library's stylesheet is injected when the first map mounts, into the root the map renders in: the start of the page <head>, or a web component's shadow root (from 1.1.1, where a map in a shadow root used to render unstyled). Placed first, it loses to your own rules at equal specificity.

--apexmaps-focus doubles as the selection box color (.apexmaps-select-box strokes and fills with it), so a rebrand that changes the accent color updates the box-selection rectangle for free.

#branded {
  --apexmaps-fg: #f8fafc;
  --apexmaps-fg-muted: #cbd5f5;
  --apexmaps-surface: #1e1b4b;
  --apexmaps-border: rgba(255, 255, 255, 0.24);
  --apexmaps-focus: #f472b6;
  --apexmaps-radius: 2px;
  --apexmaps-font-size: 13px;
}
// No theme option set at all: the container's CSS alone drives the legend,
// tooltip and attribution.
new ApexMaps(document.getElementById('branded'), {
  chart: { height: 320, background: 'transparent' },
  geo: { map: 'eu/nuts0@20m' },
  series: [{ name: 'Adoption', joinBy: ['nuts_id', 'key'], data, scale: { palette: 'magma' } }],
})

Two variables outside this table are written by the engine rather than meant for you to set directly: --apexmaps-anim and --apexmaps-anim-geom pace the fill and geometry transitions from chart.animations.speed, and zero out automatically under prefers-reduced-motion.

Colors the map paints itself

From 1.3.0, four more tokens back colors the map paints from JS rather than through the stylesheet. Each sits below the option that sets the same thing, so a value you pass in the options still wins:

VariablePaintsLight defaultDark defaultOption that wins
--apexmaps-basemapThe land drawn when no feature series is configured#dfe4ea#374151geo.fill
--apexmaps-no-dataFeatures with no value, and the legend's no-data swatch#eeeeee#374151scale.nullColor
--apexmaps-outlineDefault region and symbol outlines#ffffff#ffffffa series' stroke.color
--apexmaps-selectedThe selected feature's outline#111111#111111states.active.stroke

The defaults are the colors earlier releases painted, so a map that sets none of these looks the same. They are read at each render and updateOptions() call, so after changing one on a live page, call updateOptions({}).

#dashboard-map {
  --apexmaps-basemap: #e7e5e4;
  --apexmaps-no-data: #f5f5f4;
  --apexmaps-selected: #0f766e;
}

Responsive rules

responsive swaps in a different slice of options once the map's measured container width crosses a breakpoint, the same shape as ApexCharts' own responsive array:

responsive: [
  { breakpoint: 560, options: { legend: { position: 'bottom' }, dataLabels: { enabled: false } } },
  { breakpoint: 900, options: { legend: { position: 'right' } } },
]

Rules match against measured width rather than viewport width, and the narrowest matching breakpoint wins, so they can be declared in any order. This is what a map should do at that width, not a media query in disguise: a map that is half of a two-column dashboard row is "narrow" long before the browser viewport is, and responsive reacts to the box the map is actually in.

map.on('resized', () => {
  console.log(map.viewport.width, map.config.legend.position)
})

An explicit width on the container (chart.width: '100%' on a sized parent, not an unconstrained flex or grid item) matters here: a resizable box that reports its intrinsic max-content width will size the map to that instead of to the page, and no breakpoint will ever fire.

See also

  • Using with Ignite UI to follow an Ignite UI theme on the page with an add-on that needs no licence key of its own, from ApexMaps 1.3.0.
  • Legends, Labels & Tooltips for the components most of this surface styles.
  • Palettes for the data-color ramps, which theming intentionally leaves alone.
  • Zoom, Pan & Camera for prefers-reduced-motion handling on camera moves.