← Back to the list of blog posts
Maplair 4.0 — a fork of Leaflet
Today we’re releasing Maplair 4.0.0, the first stable release of Maplair: a fork of Leaflet, the open-source JavaScript library for interactive maps that Volodymyr Agafonkin created in 2010. It brings many features which Leaflet apps used plugins or workarounds for — map rotation, fullscreen, search, heatmaps, a minimap, legends and more — all of them dropped by bundlers unless you use them.
Maplair builds on the work that went into Leaflet 2.0 — the move to ES modules, standard JavaScript classes and Pointer Events — and continues it: some changes that were proposed for Leaflet, but didn’t land there, are part of Maplair. Many thanks to Volodymyr Agafonkin and to the hundreds of Leaflet contributors, whose work Maplair is based on.
Why 4.0?
Maplair’s first version is 4.0, so that it can’t be confused with Leaflet 2.x or 3.x. If you’re coming from Leaflet 1.9.4, read on for everything that changed in between; if you already use the Leaflet 2.0 alpha, most of it will look familiar.
What’s new
-
Translations of the built-in texts (zoom buttons, popups, scale, attribution …) with the new
I18nAPI. German, Spanish, French, Czech, Slovak, Hungarian, Slovenian, Italian and Romansh are included; help translate on Transifex.import {I18n} from 'maplair'; import de from 'maplair/i18n/de.json' with {type: 'json'}; I18n.registerLocale('de', de); I18n.setLocale('de'); - Better performance with many markers: the new map option
markerCullingtakes markers far outside of the view off the page, and the new marker optionshadow: falsedrops the marker shadows. Popups open with one layout pass instead of two, adding many layers no longer slows down quadratically, and unchanged positions aren’t written to the page again. - Tree-shaking: bundlers include only what you use — e.g. a map with just a tile layer is about 34 KB of gzipped JS, about half the size of all of Maplair (63 KB).
- A new look: a maple leaf logo and marker 🍁.
- Standard JavaScript classes: classes use
constructor()like any other JavaScript class, andnew Point(point)creates a copy like any other constructor. - Type declarations and a new API reference: the doc comments are JSDoc with TypeScript types now. The package ships type declarations (
dist/types/) for Maplair and its optional modules, and the API reference is generated from them, with inherited options, events and methods.
New controls
FullscreenControl(map optionfullscreenControl) withtoggleFullscreen(),isFullscreen()and the eventsenterfullscreen,exitfullscreenandfullscreenchange. Without the Fullscreen API, e.g. on iPhones, the map fills the window instead.-
SearchControl: a search field with results to pick with the pointer or the keyboard, finding coordinates and geo URIs by itself (with the newLatLng.parse()). Bring your owngeocoderfunction, or useSearchControl.nominatim()with your own Nominatim server — or with the public one, if your use complies with the Nominatim usage policy, which you confirm withacceptUsagePolicy: true:new SearchControl({ geocoder: SearchControl.nominatim({acceptUsagePolicy: true}) }).addTo(map); ButtonControlandButtonBarControlfor buttons of your own, styled like the zoom buttons, with states, disabling and access keys;TextControlfor a title or a status line.MiniMapControl: an overview map showing the view of the map, to drag or click.SideBySideControl: a draggable divider to compare two layers.-
LegendControlandClassification: classify features by value or by range (like the categorized and graduated renderers of QGIS), style aGeoJSONlayer with it, and show the classes in a legend, optionally with the feature count:const temperature = new Classification([ {upper: 0, symbol: {fillColor: '#5bc0de'}, legend: '< 0 °C'}, {lower: 0, upper: 10, symbol: {fillColor: '#33ee33'}, legend: '0–10 °C'}, {lower: 10, symbol: {fillColor: '#f0a030'}, legend: '≥ 10 °C'} ], {name: 'Air temperature', value: 'temperature'}); new GeoJSON(stations, { pointToLayer: (feature, latlng) => new CircleMarker(latlng), style: temperature.style }).addTo(map); ContextMenufor menus on right click or long tap, on the map or on layers.- The new control option
ordersorts the controls of a corner.
Map and interaction
- Rotation: the map option
bearingwithsetBearing(), rotating with two fingers (pinchRotate) and a compass (RotateControl);MarkeroptionsrotationAngleandrotationOrigin, e.g. for the heading of a vehicle. - Smoother zooming:
continuousZoom: truezooms through fractional zoom levels, also with the scroll wheel. Pinching on a touchpad zooms, and withscrollWheelZoom: 'trackpad', scrolling on a touchpad pans. Tap once, then touch again and drag up or down to zoom (tapDragZoom, enabled by default). Hashkeeps the view (and optionally the layers) in the URL, as#zoom/lat/lng, as geo URI or in the query string;MapSynckeeps the views of several maps in sync.setCrs()changes the CRS of the map at runtime.- The new
idleevent is fired when the view isn’t changing and all layers have loaded, and view changes caused by the user have the DOM event asoriginalEvent. - The map follows changes of the
devicePixelRatio, e.g. when moving the window to another screen, and a hidden map (e.g. in an inactive tab) keeps its view. - Accessibility: the map has an accessible name (option
ariaLabel), ? shows the keyboard shortcuts, the controls use<button>elements, and popups opened with the keyboard get the focus. - Popups and tooltips can extend beyond the map with the
overflowoption, and tooltips withdirection: 'auto'open on the side where they fit.
Layers and markers
-
SvgIcondraws the default marker in any color, with a character, emoji or SVG path as glyph:new Marker(latlng, {icon: new SvgIcon({color: 'green', glyph: '🚲'})}).addTo(map); HeatLayerdraws heatmaps of tens of thousands of points.DisplaceHandlermoves overlapping circle markers apart (opt in withMaplairMap.addInitHook('addHandler', 'displace', DisplaceHandler)and the optiondisplace: true).- Tile layers:
fetchOptionsloads tiles withfetch(), e.g. with anAuthorizationheader, and aborts requests of tiles no longer needed;setUrl()swaps the tiles without flickering;placeholdersets a background while tiles load;WMSTileLayer#getLegendUrl()returns the URL of the legend. pixelated: truekeeps the pixels of tiles and image overlays crisp when zoomed.setInteractive()changes whether a layer is interactive, andsetAttribution()changes its attribution, sorted by the new optionattributionOrder.
Optional modules
These features extend existing classes, so they’re separate modules: import them to use them, e.g. import 'maplair/zoom-styles';. The bundles maplair.js and maplair-global.js contain all of them.
maplair/map-rotationandmaplair/marker-rotation: the rotation of the map and of markers.maplair/marker-movement:Marker#moveTo()andmoveAlong()move a marker smoothly along a route.maplair/dash-animation: the path optiondashSpeedanimates dashes along a path (“marching ants”).maplair/path-decorations: the path optionsarrowheadsandtext, e.g. for directions and street names along lines.maplair/geojson-loading:GeoJSON#load()and the optionurlload GeoJSON from a URL.maplair/zoom-styles: a pathweightand a circle markerradiusdepending on the zoom, as a function or as zoom stops.
Theming
The colors, borders, shadows, fonts and spacing are CSS variables now, so that you can theme maps by overriding them:
:root {
--maplair-background-color-base: #202122;
--maplair-color-base: #eaecf0;
}
Migrating from Leaflet 1.9.4
Maplair targets browsers which support the web features that are Baseline Widely available (supported by all major browsers for at least 30 months); Internet Explorer isn’t supported anymore.
Package, files and loading
-
Install the
maplairpackage (npm install maplair), and import frommaplair. Maplair is published as ES modules with named exports only — there’s no default export and no globalL:import {MaplairMap, TileLayer, Marker} from 'maplair'; const map = new MaplairMap('map').setView([51.505, -0.09], 13); new TileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>' }).addTo(map); new Marker([51.5, -0.09]).addTo(map);Without a bundler, use an import map, e.g. pointing
maplairtohttps://cdn.jsdelivr.net/npm/maplair@4.0.0/dist/maplair.js, and<script type="module">. - For old-style scripts,
dist/maplair-global.jsstill provides a globalL(e.g.new L.MaplairMap('map')), includingL.noConflict(). - The files were renamed:
leaflet.js→maplair.js,leaflet-src.js→maplair-src.js,leaflet.css→maplair.css(also importable asmaplair/styles.css). - All CSS classes were renamed from
leaflet-*tomaplair-*, e.g..leaflet-container→.maplair-container,.leaflet-popup→.maplair-popup. Update your stylesheets and any code selecting these classes. - The images are SVG files:
marker-icon.svg,marker-shadow.svgandlayers.svgreplacemarker-icon.png,marker-icon-2x.png,marker-shadow.png,layers.pngandlayers-2x.png.
Creating objects
- The factory functions were removed — use constructors instead:
L.marker(latlng)→new Marker(latlng),L.map('map')→new MaplairMap('map'), etc. L.Mapis nowMaplairMap(Mapis still exported as an alias, but shadows JavaScript’s built-inMapwhen imported).-
Classes that were attached to other classes are separate exports now:
Leaflet 1.9.4 Maplair 4.0 L.Control.Zoom,L.Control.Attribution,L.Control.Layers,L.Control.ScaleZoomControl,AttributionControl,LayersControl,ScaleControlL.Map.Drag,L.Map.BoxZoom,L.Map.DoubleClickZoom,L.Map.Keyboard,L.Map.ScrollWheelZoom,L.Map.TapHold,L.Map.TouchZoomDragHandler,BoxZoomHandler,DoubleClickZoomHandler,KeyboardHandler,ScrollWheelZoomHandler,TapHoldHandler,PinchZoomHandlerL.TileLayer.WMSWMSTileLayerL.Icon.DefaultDefaultIconL.CRS.EPSG3857,L.CRS.EPSG4326,L.CRS.EPSG3395,L.CRS.EPSG900913,L.CRS.Earth,L.CRS.SimpleEPSG3857,EPSG4326,EPSG3395,EPSG900913,EarthCRS,SimpleCRS - The touch zoom handler was renamed: the map option
touchZoomand the propertymap.touchZoomstill work, but are deprecated in favour ofpinchZoom. -
new LatLng(latlng),new Point(point),new LatLngBounds(bounds)andnew Bounds(bounds)now always create a new object (Leaflet’s factories returned the given object). To convert without copying, useLatLng.from(),Point.from(),LatLngBounds.from()andBounds.from(), which return instances as they are, e.g.L.latLng(latlng)→LatLng.from(latlng).LatLng#clone()andPoint#clone()were removed — use the constructors instead. Layers likeImageOverlaykeep their own copy of the coordinates passed to them.Constructor Factory Copy Leaflet 1.9.4 new L.LatLng(lat, lng, alt): numbers onlyL.latLng(x): also arrays and objects, returnsLatLnginstances as they arelatlng.clone()Leaflet 2.0 alpha new LatLng(x): also arrays and objects, but returnsLatLnginstances as they areremoved latlng.clone()Maplair 4.0 new LatLng(x): also arrays and objects, always a new objectLatLng.from(x): also arrays and objects, returnsLatLnginstances as they arenew LatLng(latlng)Point,LatLngBoundsandBoundsfollow the same pattern (in Leaflet, the constructors of the bounds took corner points too, and the bounds had noclone()).
Classes and plugins
Class.extend()was removed; use JavaScript classes:class MyLayer extends Layer { … }.initialize()was replaced by the standardconstructor(). Callsuper(…)instead ofParentClass.prototype.initialize.call(this, …).includeandstaticsin class definitions are gone; useMyClass.include({…})andstaticmembers, and configure default options withsetDefaultOptions()in a static block.- Hooks added with
addInitHook()run at the end of the constructor of the class they were added to, which callsthis.callInitHooks()— so a hook added toPolylinesees a fully constructed polyline. All Maplair classes support hooks; your own classes do, if they callthis.callInitHooks()at the end of their constructor. Hooks of a parent class run before the constructor of a subclass continues. See the tutorial on extending classes. - Leaflet plugins import from
leafletand expectleaflet-*CSS classes and theLglobal, so most won’t work with Maplair unchanged.
Events
- Maplair uses Pointer Events instead of mouse and touch events. Listen to
pointerdown,pointermove,pointerup,pointerover,pointeroutetc. instead ofmousedown,mousemove,mouseup,mouseover,mouseout,touchstartetc. Maplair logs an error when listening to a removedmouse*event. - The aliases
addEventListener,removeEventListener,addOneTimeEventListener,fireEventandhasEventListenersofEvented, andaddListenerandremoveListenerofDomEvent, were removed — useon,off,once,fireandlistens. DomEvent.getMousePosition()was renamed toDomEvent.getPointerPosition().- The map listens to window resizes with a
ResizeObserver, so it updates its size when its container changes size for any reason.
Utilities and options
- Helpers that are now part of JavaScript or the DOM were removed — use the standard APIs instead:
Util:bind(),isArray(),create(),trim(),indexOf(),extend(),getParamString(),requestAnimFrame()/cancelAnimFrame(),emptyImageUrl— useFunction#bind(),Array.isArray(),Object.create(),String#trim(),Array#indexOf(),Object.assign()or{...},URLSearchParams,requestAnimationFrame()/cancelAnimationFrame().DomUtil:hasClass(),addClass(),removeClass(),setClass(),getClass(),remove(),empty(),setOpacity(),getStyle(),testProp(),TRANSFORM,TRANSITION,TRANSITION_END— useclassList,Element#remove(),Element#replaceChildren(),style/getComputedStyle().
- Most
Browserdetection flags were removed (e.g.ie,edge,webkit,android,gecko,opera,any3d,canvas,svg,vml,passiveEvents), as wereL_NO_TOUCH, the VML renderer andLayerGroup#invoke()(useeachLayer()). - The zoom control options
zoomInTitle/zoomOutTitleand the popup optioncloseButtonLabelwere removed; translate these texts withI18ninstead. - The default attribution prefix links to maplair.org with a 🍁 instead of the Ukrainian flag (CSS class
maplair-attribution-leafinstead ofmaplair-attribution-flag), and the OpenStreetMap attribution added for OpenStreetMap tiles now links the whole text “OpenStreetMap contributors”. - Tile layers default to the
strict-origin-when-cross-originreferrer policy. - The layers control uses
<fieldset>instead of<section>. - The zoom, layers and fullscreen controls and the popup close button are
<button>elements instead of<a href="#">, and the map container getsrole="region"and anaria-labelunless it has them already. - The styles use CSS variables, which changes some details slightly: e.g. the controls are 8px instead of 10px away from the edge of the map, and font sizes are in
rem. - Tile layers with
detectRetinano longer request tiles beyondmaxNativeZoom.
Feedback
Please report problems and share feedback on GitHub — and have fun mapping with Maplair! 🍁
