Skip to content

ExpressiveCSS

Browse documentation

Map

An interactive map with markers, popups, routes, arcs, clusters and controls, built on MapLibre.

View as Markdown

A .map is an interactive map built on MapLibre, which the page supplies. Markers, popups and controls are HTML. Routes, arcs and clusters are data added from script.

Loop store

Open until 9 p.m.

Lincoln Park store

Open until 8 p.m.

<div class="map" role="region" aria-label="Chicago stores">
  <button type="button" class="map-marker" data-lng-lat="-87.6278,41.8820"
          aria-label="Loop store" aria-controls="loop">
    <span class="map-marker-label">Loop</span>
  </button>
  <div class="map-popup" id="loop">
    <h3>Loop store</h3>
    <p>Open until 9 p.m.</p>
  </div>
  <div class="map-controls">
    <div class="map-control-group">
      <button type="button" class="icon-button" data-action="zoom-in" aria-label="Zoom in">
        <span class="material-symbols" aria-hidden="true">add</span>
      </button>
      <button type="button" class="icon-button" data-action="zoom-out" aria-label="Zoom out">
        <span class="material-symbols" aria-hidden="true">remove</span>
      </button>
    </div>
  </div>
</div>

Setup

ExpressiveCSS does not bundle MapLibre. To set it up:

  1. Install MapLibre.
  2. Load its stylesheet before ExpressiveCSS.
  3. Pass the module to MapView.init().

AutoInit() does not start the map. The class is MapView so importing it does not hide JavaScript's Map.

npm install maplibre-gl
import 'maplibre-gl/dist/maplibre-gl.css';
import * as maplibregl from 'maplibre-gl';
import { MapView } from '@expressivecss/expressive';

const map = MapView.init(document.querySelector('.map'), {
  maplibregl,
  center: [-87.65, 41.89],
  zoom: 11
});

Without a bundler, import maplibre-gl.mjs from a CDN. MapLibre loads its worker from the same folder, so a page with a Content Security Policy needs worker-src blob: and the CDN in script-src.

Styles

By default the map uses OpenFreeMap's Positron style in the light theme and its Dark style in the dark theme. Neither needs an API key. The map changes style when the theme attribute or the system color scheme changes. To keep one style, pass style as a style URL or object. To choose both, pass { light, dark }. Any MapLibre style works, such as MapTiler, Stadia Maps or Protomaps. Follow your provider's terms and keep its attribution visible.

MapView.init(el, {
  maplibregl,
  style: {
    light: 'https://tiles.openfreemap.org/styles/liberty',
    dark: 'https://tiles.openfreemap.org/styles/dark'
  }
});

Markers and popups

Set data-lng-lat on a .map-marker to place it, written as longitude then latitude. The script skips a value that is not two numbers with a latitude between -90 and 90, and logs a console warning. A marker with no content of its own is a primary dot. Put an avatar, icon or image inside to replace the dot. A .map-marker-label shows below it, or above with .top. A child .tooltip shows on hover and focus.

Make a marker a <button> with aria-controls naming a .map-popup. The marker then opens that popup and sets aria-expanded. A click on the map or Escape closes it, and focus returns to the marker. A .map-popup with its own data-lng-lat and no marker opens when the map starts.

Add data-draggable to let the reader drag a marker, and make the marker a named <button>. A drag is not the only way to move it. A tap picks the marker up and sets aria-pressed="true". The next tap on the map drops it there. Escape or a second tap on the marker puts it back. The arrow keys nudge a focused marker. Each move leaves the new position in its data-lng-lat and fires change.

<button type="button" class="map-marker" data-lng-lat="-87.62,41.88" data-draggable aria-label="Drop-off point">
  <span class="map-marker-label top">Drop-off</span>
</button>
<div class="map-popup" data-lng-lat="-87.70,41.85">Depot</div>

<script>
document.querySelector('.map').addEventListener('change', (event) => {
  console.log(event.target.dataset.lngLat);
});
</script>

Routes and arcs

addRoute(id, coordinates, options) draws a line through [longitude, latitude] points in the primary color. addArcs(id, arcs, options) draws a curve for each { from, to } pair in the tertiary color. Both take color, width, opacity and dashArray. The map does not plan routes, so get the points from a routing service such as OSRM or Valhalla.

const instance = MapView.init(el, { maplibregl });
instance.addRoute('delivery', [[-87.70, 41.85], [-87.65, 41.87], [-87.62, 41.88]]);
instance.addArcs('flights', [
  { from: [-87.90, 41.97], to: [-73.78, 40.64] },
  { from: [-87.90, 41.97], to: [-118.41, 33.94] }
], { dashArray: [2, 2] });

Clusters

addClusters(id, data, options) draws GeoJSON points, or the URL of a GeoJSON file, and groups those close together. A cluster shows its count when the style has fonts. A click on a cluster zooms in until it splits. Its layers are ${id}-clusters, ${id}-count and ${id}-points. To handle a click on a point, use instance.map.on('click', `${id}-points`, …).

instance.addClusters('stores', 'https://example.com/stores.geojson', { radius: 50, maxZoom: 14 });
instance.map.on('click', 'stores-points', (event) => {
  console.log(event.features[0].properties);
});

Lines and clusters take their colors from the theme tokens where the map is. When the style changes, the map redraws them in the new theme's colors. removeOverlay(id) removes one.

Controls

Put icon buttons in a .map-controls to stack them in the top right corner. Add .top-left, .bottom-left or .bottom-right for another corner. A .map-control-group joins buttons on one surface. Each button's data-action sets what it does:

  • zoom-in and zoom-out zoom the map. They are disabled at the zoom limits.
  • compass turns the map back to north. Its icon points north as the map rotates.
  • locate centers the map on the reader's location and marks it. It is disabled if the reader refuses.
  • fullscreen shows the map full screen and sets aria-pressed. It is hidden where full screen is unavailable.
  • pan-up, pan-down, pan-left and pan-right move the view a third of the map's size, so it pans without a drag.

Accessibility

Name the map with aria-label and role="region". MapLibre makes the canvas focusable. On it, the arrow keys pan, + and - zoom, and Shift with the arrows rotates. A marker that does something should be a named <button>. A decorative one can be a <span>. Put what matters, such as store addresses, in text on the page as well, because a map is hard to use without sight.

Options

Name Type Default Description
maplibregl Module null The maplibre-gl module. Required.
style String, Object OpenFreeMap Positron and Dark A style URL or object, or { light, dark } to follow the theme.
center Array [0, 20] Starting [longitude, latitude].
zoom Number 1 Starting zoom level.
mapOptions Object {} More MapLibre Map options, such as maxZoom or bearing.

Methods

const instance = Expressive.MapView.getInstance(document.querySelector('.map'));
instance.map; // the MapLibre map, for anything else
instance.markers; // the MapLibre markers, in markup order
instance.addRoute(id, coordinates, options);
instance.addArcs(id, arcs, options);
instance.addClusters(id, data, options);
instance.removeOverlay(id);
instance.destroy(); // removes the map and puts the markup back

Tokens

Override these on the .map.

Token Default
--md-comp-map-height400px
--md-comp-map-shape16px
--md-comp-map-marker-colorprimary
--md-comp-map-marker-ring-colorsurface
--md-comp-map-route-colorprimary
--md-comp-map-arc-colortertiary
--md-comp-map-cluster-colorprimary
--md-comp-map-cluster-label-coloron-primary
--md-comp-map-popup-max-width280px
--md-comp-map-control-offset12px

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow