Smooth Transitions for MapLibre GL JS

Animate any data-driven paint property, per feature, with easing, delays and multi-stop breakpoints. One requestAnimationFrame for the whole map — thousands of features, one write each per frame.

Interactive Examples

6
fill-extrusion

Rising City

Five thousand real Vancouver buildings grow from their footprints to full height in 3D. Per-building staggered delay, two channels each — height and color — and a camera that orbits the skyline it just built.

paint: { 'fill-extrusion-height': [null, h] }
the call

Playground

Every knob on the API, wired to a live map, printing the exact paint object it is about to run. Nine easings, side by side.

map.transition(f, { duration, ease, delay, paint })
breakpoints

Color & Breakpoints

An editor whose UI is the value you pass. Add, remove and reorder stops; three or more make a piecewise ramp.

'circle-color': ['#26547c', '#e9c46a', '#f4703a']
scheduler

Stress

Eight thousand points, three channels each — 24,000 animating properties on one rAF. Turn up the stagger and watch setFeatureState calls per frame fall roughly fivefold, because a delayed transition costs nothing until it starts.

map.transition(f, { delay: i * 0.4 })
pointer

Hover Effects

delay as a dwell threshold: leaving supersedes the pending transition, so it never fires. No timers, no saved values.

map.transition(f, { delay: dwellMs })
sequence

Chained Transitions

Sequences built purely on onComplete. Stop and re-trigger cancel by supersession — a superseded step never re-arms.

map.transition(f, { onComplete: next })

Getting Started

Installation

npm install maplibre-transition

Quick Example

import maplibregl from 'maplibre-gl';
import MaplibreTransition from 'maplibre-transition';

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://demotiles.maplibre.org/style.json',
  center: [-123.12, 49.28],
  zoom: 11
});

// Teach the map to transition. Adds map.transition().
MaplibreTransition.init(map);

map.on('click', 'cities-layer', (e) => {
  map.transition(e.features[0], {
    duration: 900,
    ease: 'cubic',
    delay: 0,
    paint: {
      // null = "start from wherever this feature is right now"
      'circle-radius': [null, 28],
      'circle-color': [null, '#f4703a']
    },
    onComplete: () => console.log('landed')
  });
});
Paint ownership. Once you have transitioned a (layer, paint-property) pair, the plugin rewrites it to ["coalesce", ["feature-state", …], fallback] and owns it. Animate it with map.transition() from then on — never map.setPaintProperty().