⚡ Modernization of Leaflet
After two and a half years of hard work, we’re thrilled to announce the first alpha release of Leaflet 2.0!
This release marks a major modernization of the Leaflet codebase. We've dropped support for Internet Explorer, removed legacy methods and polyfills, adopted modern standards like Pointer Events, and now publish Leaflet as an ESM module. The global L is no longer part of the core package (though it’s still available in the bundled version leaflet-global.js for backward compatibility).
For more information checkout the blog post: https://leafletjs.com/2025/05/18/leaflet-2.0.0-alpha.html
What's New in Leaflet 2.0
- Targeting only evergreen browsers
- Switched from Mouse and Touch events to PointerEvents
- ESM support and tree shaking
- Rewritten using standardized ES6 classes
Migration
- Replace all factory methods with constructor calls: L.marker(latlng) ➜ new Marker(latlng)
- Change the <script> tag to module: <script type='module'>
- Replace usage of L with explicit imports from the Leaflet package: import { Marker } from 'leaflet'
- if you are not using a package manager like npm, you can use a importmap: https://leafletjs.com/examples/quick-start/
- To have global access to variables of a module-script, assign them to the window object (Not recommended): window.map = map
- Consider Leaflet V1 Polyfill if you are using outdated plugins
Old implementation
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script>
const map = L.map('map').setView([51.505, -0.09], 13);
const tiles = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
</script>
New implementation
Module
<script type="importmap">
{
"imports": {
"leaflet": "https://unpkg.com/[email protected]/dist/leaflet.js"
}
}
</script>
<script type="module">
import L, {Map, TileLayer, Marker, Circle, Polygon, Popup} from 'leaflet';
const map = new Map('map').setView([51.505, -0.09], 13);
const tiles = new TileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
</script>
Global Script
<script src="https://unpkg.com/[email protected]/dist/leaflet-global.js"></script>
<script>
const map = new L.Map('map').setView([51.505, -0.09], 13);
const tiles = new L.TileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
</script>
Need Legacy Support?
Check out this polyfill package to help ease the transition for legacy apps: Leaflet V1 Polyfill
Changes
❇️ New Features
- Automatically attributes OSM if OSM tiles are used and no attribution is provided by @Jouwee in #9489
- Added decoding option to ImageOverlay by @IvanSanchez in #8650
- Added option for setting value of aria-label for popup close button by @ShivangMishra in #8590
- Implement BlanketOverlay as Renderer superclass by @IvanSanchez in #8611
- Support SSR by @Falke-Design in #9385
- Implemented trackResize for L.Popup by @IvanSanchez in #9605
- VideoOverlay: add controls option by @simon04 in #9666
- Add aria-keyshortcuts to map container by @chcederquist in #9688
- Expand layers control on Enter keydown by @larsgw in #8556
✨ Refactorings (⚠️ Breaking Changes)
- Change <section> to <fieldset> in the layers control by @tmiaa in #7912
- Use ResizeObserver for map resizes by @IvanSanchez in #8612
- Remove usage of global L and safeguard against it by @mourner in #8536
- Replace Util.bind() with Function.bind() by @jonkoops in #8484
- Replace Util.isArray() with Array.isArray() by @jonkoops in #8683
- Replace Util.create() with Object.create() by @jonkoops in #8681
- Replace Util.trim() with String.prototype.trim() by @jonkoops in #8682
- Prefer Object.hasOwn() over Object.prototype.hasOwnProperty() by @jonkoops in #8684
- Use the classList API for class manipulation methods in DomUtil by @jonkoops in #8685
- Replace DomUtil.hasClass() with classList.contains() by @jonkoops in #8727
- Replace DomUtil.getClass() with classList.value by @jonkoops in #8728
- Remove DomUtil.setClass() method by @jonkoops in #8729
- Replace DomUtil.addClass() with classList.add() by @jonkoops in #8731
- Replace DomUtil.removeClass() with classList.remove() by @jonkoops in #8732
- Replace DomUtil.remove() with Element.remove() by @jonkoops in #8735
- Replace DomUtil.empty() with Element.replaceChildren() by @jonkoops in #8736
- Hold element positions in WeakMap by @jonkoops in #8749
- Destructure statics and includes from class properties by @jonkoops in #8823
- Enforce .js file extension for module imports by @jessetane in #8837
- Move callInitHooks() to prototype of Class by @jonkoops in #8825
- Use Object.setPrototypeOf() to extend Class by @jonkoops in #8824
- Use Pointer Events for Map.Keyboard by @Falke-Design in #8758
- Convert Class to a JavaScript class by @jonkoops in #8871
- Drop __super__ property from Class by @jonkoops in #8800
- Move initialization logic for sub-classes to Class constructor by @jonkoops in #8872
- Use Pointer Events for Map.BoxZoom by @jonkoops in #8757
- Use Pointer Events for Control.Layers by @Falke-Design in #8759
- Drop UMD and make ESM the default entrypoint by @jonkoops in #8826
- Replace animation frame polyfill with native API by @jonkoops in #8810
- Rename TouchZoom handler to PinchZoom (keep TouchZoom as alias for backward compatibility) by @Mahendra-006 in #9642
- Use nullish coalescing and optional chaining by @simon04 in #9661
- Use arrow callback and shorthand methods by @simon04 in #9659
- Replace Util.getParamString with URL.searchParams by @simon04 in #9654
- Convert Bounds, LatLng, LatLngBounds, Point, Transformation to ES6 classes by @simon04 in #9655
- Use for...of by @simon04 in #9653
- Replace Util.extend with ES6 Object.assign or object spread by @simon04 in #9652
- Refactor to PointerEvents by @Falke-Design in #9620
- Convert CRS to ES6 classes by @simon04 in #9669
- Use SVG icon for layer control by @simon04 in #9665
- Optimize PNG files by @dilyanpalauzov in #8661
- Add shadow-icon as svg icon and add white circle to marker-icon by @Falke-Design in #8768
❌ Removed Features (⚠️ Breaking Changes)
- Drop legacy VML code & official support for IE7–8 by @mourner in #8196
- Remove the long-deprecated L.Mixin.Events and _flat by @mourner in #8537
- Remove Internet Explorer from browser detection by @jonkoops in #8559
- Remove legacy CSS hacks and unused prefixes by @mourner in #8600
- Remove Microsoft-specific Pointer Events from browser detection by @jonkoops in #8603
- Remove Android from browser detection by @jonkoops in #8607
- Remove Edge from browser detection by @jonkoops in #8606
- Remove Opera from browser detection by @jonkoops in #8621
- Remove PhantomJS from browser detection by @jonkoops in #8622
- Remove indexOf() function from Util by @jonkoops in #8623
- Stop inheriting filter CSS property on tiles by @jonkoops in #8651
- Remove DomUtil.setOpacity by @mourner in #8730
- Remove DomUtil.TRANSFORM constant by @jonkoops in #8733
- Remove TRANSITION and TRANSITION_END constants from DomUtil by @jonkoops in #8734
- Remove vendor prefixes when setting userSelect style by @jonkoops in #8738
- Remove DomUtil.testProp() by @jonkoops in #8739
- Remove DomUtil.getStyle() by @jonkoops in #8747
- Remove Browser.any3d by @jonkoops in #8748
- Remove SVG feature detection code by @jonkoops in #8755
- Remove noConflict() by @jonkoops in #8753
- Remove browser specific detection code for CSS transforms by @jonkoops in #8751
- Remove L_NO_TOUCH global switch by @jonkoops in #8752
- Remove Browser.canvas feature detection by @jonkoops in #8754
- Remove Windows platform detection by @jonkoops in #8769
- Remove Gecko-based browser detection by @jonkoops in #8770
- Remove references to PhantomJS by @jonkoops in #8771
- Remove passive event detection by @jonkoops in #8772
- Remove WebKit-based browser detection by @jonkoops in #8773
- Remove deprecated which and change button to main-button by @Falke-Design in #8796
- Remove jitter debug page by @jonkoops in #8829
- Remove legacy timer fallbacks for requestAnimFrame by @mourner in #9236
- Get rif of prefixed requestAnimationFrame leftovers by @mourner in #9241
- Removes mozEvent warning by @Falke-Design in #9650
- Remove deprecated methods / options by @Falke-Design in #9622
- Removes factory functions by @Falke-Design in #9626
- Bring back global L as new bundle leaflet-global.js by @Falke-Design in #9686
🐞 Bugfixes
- Fix Events.listens for nested propagations by @Falke-Design in #8457
- Fix marker popup location by @raychanks in #8523
- Fix intermittent wobble when setMaxBounds(map.getBounds()) by @rjackson in #8534
- Alternate fix for PopUp keepInView recursion and speed up associated test by @rjackson in #8520
- Support sticky maps by @tmiaa in #8550
- Align the scale control's alpha transparency with the attribution control by @Malvoz in #8547
- Allow the scale control's text to overflow the container by @Malvoz in #8548
- Fixes event target of popupopen event and adds test by @Belair34 in #8571
- Fix worldCopyJump with Keyboard by @Falke-Design in #8562
- Fix CSS syntax error in leaflet.css by @Malvoz in #8628
- Fix closed coord's reference in latLngsToCoords by @marlo22 in #7344
- Rename 'closed' parameter of 'latLngsToCoords' to 'close' to avoid confusion by @ronaldhoek in #8678
- toGeoJSON() should still work if no values in coords array by @Falke-Design in #8737
- Fix implementation for disabling & enabling text selection by @jonkoops in #8741
- fix vector drifts when zoomAnimation is false and zooming via flyTo or pinch by @plainheart in #8794
- Update PolyUtil.js by @davidmgvaz in #8840
- Revamp synthetic dblclick event instantiation by @IvanSanchez in #8632
- Alleviate tile gaps in chromium by using mix-blend-mode CSS by @IvanSanchez in #8891
- Apply mix-blend-mode only on tile images by @Falke-Design in #8907
- Set outlineStyle instead of outline when preventing outline by @jonkoops in #8917
- Prevent adding layer while expanding Layer-Control on mobile by @Falke-Design in #8910
- Fix tooltip focus listener if getElement is no function by @Falke-Design in #8890
- Fix issue whereby tooltips loaded dynamically while moving the map were never shown. by @theGOTOguy in #8672
- Fix noMoveStart option for fitBounds method by @AbdullahSohail-SE in #8911
- Mapbox tiles not loading 8960 by @Dele-Oyelese in #8968
- Update the height of the container after resizing the window by @Falke-Design in #9019
- Use _limitZoom in flyTo, like we do in resetView by @yohanboniface in #9025
- Do not propagate click when element has been removed from dom by @yohanboniface in #9052
- Fixes showing tooltip while panning the map by @Falke-Design in #9154
- Fix hover underline in flag + Leaflet attribution prefix by @rkaravia in #9280
- Prevent showing outline-box on Chromium when clicking on a vector with a tooltip by @Falke-Design in #9393
- Clear timeouts on remove (#9575) by @Mark-Falconbridge-i2 in #9577
- Fix Leaflet attribution link by @florian-h05 in #9471
- Solution to issue 9067 - Inverting x axis of L.CRS.Simple causes L.Circle to have no radius by @bakp22 in #9414
- Unbind tooltip remove focus listeners by @hollowM1ke in #9232
- Refocus map after using layers control (#9004) by @quarl in #9005
- GridLayer construct url with integer {z} for fractional zoom by @raychanks in #8613
- Fix adding Icon.Default shadow icon to the map if option is falsy by @Falke-Design in #9607
- Clean up DOM event listeners when destroying Map's animation proxy by @samclaus in #9048
- Fix Canvas rendering with setting _redrawRequest to null for requestAnimFrame by @Falke-Design in #9608
- Add dashOffset to Canvas by @Falke-Design in #9649
- Add cancelable check before preventDefault while dragging by @Falke-Design in #9639
- Fix video control / seek bar usage in safari by @Falke-Design in #9641
- GeoJSON: fix object spread regression by @simon04 in #9678
- Use arrow callback by @simon04 in #9684
- TileLayer.WMS: fix wmsParams parameter regression by @simon04 in #9683
📝 Docs
- Update website & docs for v1.9.0 by @jonkoops in #8453
- Fix the resource integrity hashes by @jonkoops in #8456
- README: update JS size for 1.9.1 by @simon04 in #8468
- Deprecate old versions' docs by @Malvoz in #8294
- Use the topmost browsing context for links in tutorial frames by @Malvoz in #8466
- Update the Leaflet Editor's description and map id by @Malvoz in #8476
- Quick-start: fix link in code block. by @Sjlver in #8415
- fixing typo lon->lng in refence.html file by @shashwat010 in #8497
- chore: replaced substr with substring by @k-rajat19 in #8517
- Update documentation for v1.9.2 by @jonkoops in #8527
- Update changelog with latest revisions by @jonkoops in #8528
- Adding documentation for the support of lon in the latLng function, resolves 8509 by @brianferry in #8524
- Fixed some grammers in readme by @Saran-pariyar in #8539
- Fix 2 links without URLs in docs by @Malvoz in #8542
- Fix markdown link of ImageOverlay.decoding by @plainheart in #8660
- Update Changelog 1.9.3 - main by @Falke-Design in #8662
- Minor typo in code example by @Lalaluka in #8710
- Added missing curly bracket in docs by @i-sukhanov in #8767
- Update docs language for HTML and CSS. by @alope107 in #8934
- CHANGELOG.md - Add 1.9.4 by @mtmail in #8967
- Update stackoverflow image #9023 by @Beast-Hunter in #9030
- Updated Twitter Logo and Name by @aialok in #9102
- Updated Multiple Logos by @UmerrAli in #9136
- Update License to 2024 by @arnabsen in #9219
- Changed Wording by @hollowM1ke in #9229
- remove dead links in #9305
- Add citation.cff to repo by @nakajimayoshi in #9341
- Docstrings for L.Marker getElement() by @yuri-karelics in #9180
- Add Stadia Maps to FAQ by @ianthetechie in #9340
- Removed unused variable from the choropleth example doc by @Cinderella-Man in #9182
- Add scroll up button to website by @simondriesen in #9186
- Add scrollbar to container if the text can't wrapped on mobile devices by @Falke-Design in #9384
- Update CONTRIBUTING.md by @cherylhughey in #9435
- Update FAQ.md by @cherylhughey in #9436
- Update License to 2025 by @PixlePixle in #9597
- Evergreen language updates and removal by @Kxiru in #9528
- Changed slogan with evergreen wording #8477 by @JoT8ng in #9491
- Updated Evergreen language in the documentation #8477 {please label me for Hacktoberfest-24} by @Dhairya-A-Mehra in #9488
- Update browser support list by @jonkoops in #8455
- Create SECURITY.md by @Ahlam-Banu in #9619
- Refactor docs for ESM by @Falke-Design in #9624
- Improve documentation about renderer and pane by @Falke-Design in #9651
- Replace unpkg with jsDelivr in documentation (fixes #9628) by @eshanair in #9663
- Remove dead link(Placekitten.com) for Cataas(fixes #9691) by @MelvinManni in #9695
- Updated titles to be descriptive, aligned tutorial titles with overvi… by @chcederquist in #9692
- Add position doc string to controls by @Falke-Design in #8570
📜 Formatting
- Enforce quotes ESLint rule for the spec directory by @jonkoops in #8686
- Provide file extensions when running ESLint by @jonkoops in #8831
- Lint files in debug directory by @jonkoops in #8925
- Upgrade to ESLint 9+ and flat config by @mourner in #9410
- Enable prefer-exponentiation-operator linting rule and fix issues by @simon04 in #9660
- Guard for-in loops and enable guard-for-in lint rule. by @alope107 in #8879
- Split ESLint config and clean it up by @jonkoops in #8563
- Upgrade ESLint config to latest version by @jonkoops in #8583
- Enable arrow-spacing linting rule and fix issues by @jonkoops in #8584
- Enable func-name-matching linting rule and fix issues by @jonkoops in #8585
- Enable no-duplicate-imports linting rule and fix issues by @jonkoops in #8586
- Enable prefer-template linting rule and fix issues by @jonkoops in #8587
- Enable prefer-rest-params linting rule and fix issues by @jonkoops in #8593
- Enable object-shorthand linting rule and fix issues by @jonkoops in #8592
- Enable prefer-arrow-callback linting rule and fix issues by @jonkoops in #8594
- Enable no-var linting rule and fix issues by @jonkoops in #8602
- Remove unused test globals from ESLint config by @jonkoops in #9285
🔧 Workflow
- Improve integrity generation for releases by @mourner in #8459
- Remove the release check to manually modify the version number in reference.html by @Malvoz in #8475
- Use different cache keys by @jonkoops in #8487
- Add build-docs job to main github actions file by @exequiel09 in #8504
- Add step for release assets by @IvanSanchez in #8494
- Add Dependabot config for Bundler dependencies by @jonkoops in #8516
- tests workflow for mac and win by @adrianaris in #8540
- Upgrade Rollup to version 3 by @jonkoops in #8582
- Convert build scripts into ESM by @jonkoops in #8610
- Use Node.js version 18 for CI by @jonkoops in #8640
- Add a note to release docs to ensure green CI by @mourner in #8668
- Run CI on Ubuntu 20.04 by @jonkoops in #8670
- Simplify workflows on CI by @mourner in #8671
- Upgrade Bundlemon to v2 by @jonkoops in #8676
- Lock actions/cache to version 3.2.0 by @jonkoops in #8742
- Run CI on latest version of Ubuntu by @jonkoops in #8744
- Run CI on latest version of Windows by @jonkoops in #8743
- Use OS name as part of cache key for jobs by @jonkoops in #8750
- Always build ESM version in watch mode by @jonkoops in #8844
- Add dev dependency http-server for debugging ESM by @jessetane in #8848
- Regenerate lockfile in version 3 format by @jonkoops in #8919
- Remove Rollup pre-proccesor from Karma runner by @jonkoops in #8935
- Add possibility to create coverage reports by @Falke-Design in #9029
- Include prosthetic-hand from GitHub by @jonkoops in #9033
- Upgrade and cleanup dev dependencies by @mourner in #9157
- Set versioning-strategy for NPM to increase by @jonkoops in #9165
- Speed up Karma runner by narrowing down files Karma serves by @mourner in #9231
- Upgrade Husky to latest version by @jonkoops in #9264
- Run test with the source files by @Falke-Design in #9609
- Add missing "./dist/leaflet.css" specifier in "leaflet" package by @simon04 in #9658
- Address npm audit issues by @Falke-Design in #9714
- Replace version when npm version is called by @Falke-Design in #9717
- Remove Karma launcher for Microsoft Edge by @jonkoops in #8604
- Remove Internet Explorer from test framework by @jonkoops in #8264
🧪 Tests
- Disable zoom animation for Line/PolyUtil tests by @mourner in #8478
- Fix test on mac changes the option 'wheelPxPerZoomLevel' by @Falke-Design in #8481
- Make test runner output cleaner by @mourner in #8480
- Add slow test stats and make some tests faster by @mourner in #8486
- Added tests for panInsideBounds by @mikelowe5919 in #8429
- Added testing for mouseEventLatLng by @spatterss135 in #8403
- Update tests index.html and add missing RectangleSpec by @Falke-Design in #8499
- Add test for map stopLocate (#8371) by @raychanks in #8505
- Added test cases for Map:addHandler method by @precious-void in #8503
- Added test cases for Map:mouseEventToContainerPoint method. by @kreloaded in #8406
- Cover Map Locate with Unit Tests by @stephenspol in #8424
- Add getWheelPxFactor and fix test changes the option 'wheelPxPerZoomLevel' for mac by @Falke-Design in #8512
- Speed up PathSpec's "Add layer inside move handler" test: 611ms to 5ms (122x faster) by @rjackson in #8518
- Speed up tests Map.ScrollWheelZoomSpec and Map.DoubleClickZoomSpec by @rjackson in #8519
- Add panBy test by @adrianaris in #8420
- Speed up tests relating to focusing on Marker by @rjackson in #8545
- Speed up TileLayer.setUrl test (251ms to 13ms) by @rjackson in #8546
- Speed up tests relating to containerPoint / layerPoint methods by @rjackson in #8544
- Adding tests for 'layerPointToLatLng' method #8375 by @ANaphade in #8435
- Refactor Event handling and happen.js by @Falke-Design in #8760
- Added two more test cases for the unproject map method by @snehalvibhute in #8637
- Replace expect.js with Chai by @jonkoops in #8952
- Import Leaflet in tests using JavaScript modules by @jonkoops in #8975
- Update ui-event-simulator and import as JavaScript module by @jonkoops in #8977
- Add tests for BoxZoom by @Falke-Design in #9032
- Fix CI tests to not depend on big Chrome window size by @mourner in #9235
- Use explicit imports chai and sinon in the test suite by @jonkoops in #9284
- Set Chrome window size to fix failing test in ubuntu by @Falke-Design in #9604
- add test for project method by @AshwinNema in #9303
- Add demo for all GeoJSON types by @simon04 in #9679
- Vector Drift Test Pinch Zoom Fix by @stephenspol in #8644
- Convert control layers debug page to ESM by @jonkoops in #8832
- Convert canvas debug page to ESM by @jonkoops in #8830
- Convert geolocation debug page to ESM by @jonkoops in #8835
- Convert controls debug page to ESM by @jonkoops in #8834
- Convert map debug pages to ESM by @jonkoops in #8838
- Convert test debug pages to ESM by @jonkoops in #8921
- Convert vector debug pages to ESM by @jonkoops in #8924
- Use import maps to load Leaflet in the debug directory by @jonkoops in #8926
- flyToBounds tests added by @shreya024 in #9112
- Add test for map stop (#8369) by @victorpatru in #8581
Full Changelog: v1.9.4...v2.0.0-alpha