leaflet-components
Leaflet.js as native Web Components (Custom Elements). Each HTML element maps 1:1 to a Leaflet object with reactive attribute binding.
Installation
npm install leaflet-components
Import options
The package ships multiple bundle formats and supports deep imports for tree-shaking.
// Default ESM bundle (recommended) — registers all components
import 'leaflet-components';
// Minified ESM bundle
import 'leaflet-components/dist/index.min.js';
// CommonJS
const lc = require('leaflet-components');
// Deep import — register only one component
import 'leaflet-components/dist/components/leaflet-marker.js';
// UMD (for script tags, expects Leaflet as window.L)
// <script src="node_modules/leaflet-components/dist/index.umd.js"></script>
leaflet is always an external dependency — you must install it yourself.
Usage
Import once to register all custom elements, then use them declaratively in HTML.
<script type="module">
import 'leaflet-components';
</script>
<leaflet-map lat="51.505" lng="-0.09" zoom="13" style="height:400px">
<leaflet-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution="© OpenStreetMap contributors"
></leaflet-tile-layer>
<leaflet-marker lat="51.505" lng="-0.09">
<leaflet-popup><b>Hello!</b></leaflet-popup>
<leaflet-tooltip>Hover me</leaflet-tooltip>
</leaflet-marker>
<leaflet-circle lat="51.508" lng="-0.11" radius="500" color="red" fill-color="#f03" fill-opacity="0.5">
<leaflet-popup>I am a circle</leaflet-popup>
</leaflet-circle>
<leaflet-polygon color="blue">
<leaflet-line lat="51.509" lng="-0.08"></leaflet-line>
<leaflet-line lat="51.503" lng="-0.06"></leaflet-line>
<leaflet-line lat="51.510" lng="-0.047"></leaflet-line>
</leaflet-polygon>
<leaflet-control-scale position="bottomleft"></leaflet-control-scale>
</leaflet-map>
leaflet-map
The root component. All other components must be children of <leaflet-map>.
<leaflet-map lat="51.505" lng="-0.09" zoom="13" disable-scroll-wheel-zoom>
View state
These attributes stay in sync with the map as the user interacts with it — panning updates lat/lng, zooming updates zoom.
| Attribute |
Default |
Description |
lat |
0 |
Center latitude |
lng |
0 |
Center longitude |
zoom |
2 |
Zoom level |
min-zoom |
0 |
Minimum zoom level |
max-zoom |
— |
Maximum zoom level. When unset, Leaflet uses the tile layer's own max zoom. |
Interaction
Boolean options that default to true are controlled by the presence of a disable-* attribute. Handler-based options can be toggled at any time; the rest only apply at construction.
| Attribute |
Controls |
Live? |
disable-dragging |
Mouse/touch panning |
✓ |
disable-scroll-wheel-zoom |
Scroll-wheel zoom |
✓ |
disable-double-click-zoom |
Double-click zoom |
✓ |
disable-touch-zoom |
Pinch-to-zoom |
✓ |
disable-box-zoom |
Shift-drag zoom box |
✓ |
disable-keyboard |
Keyboard pan/zoom |
✓ |
disable-zoom-control |
Built-in zoom control |
— |
disable-attribution-control |
Built-in attribution |
— |
disable-close-popup-on-click |
Close popup on map click |
— |
disable-track-resize |
Auto-resize on window resize |
— |
disable-bounce-at-zoom-limits |
Bounce animation at min/max zoom |
— |
disable-tap-hold |
Long-press context menu (mobile) |
— |
Boolean options that default to false are enabled by adding the attribute:
| Attribute |
Description |
prefer-canvas |
Render vector layers on Canvas instead of SVG |
world-copy-jump |
Pan to the original world copy when crossing the antimeridian |
Animation
| Attribute |
Default |
Description |
disable-zoom-animation |
— |
Disable CSS zoom animation |
disable-fade-animation |
— |
Disable tile fade-in |
disable-marker-zoom-animation |
— |
Disable marker zoom animation |
zoom-animation-threshold |
4 |
Max zoom delta for animated zoom |
Inertia & panning
| Attribute |
Default |
Description |
disable-inertia |
— |
Disable inertial panning |
inertia-deceleration |
3000 |
Deceleration rate (px/s²) |
inertia-max-speed |
Infinity |
Maximum inertia speed (px/s) |
ease-linearity |
0.2 |
Pan easing linearity |
Zoom behaviour
| Attribute |
Default |
Description |
zoom-snap |
1 |
Zoom snapping interval; 0 for continuous zoom |
zoom-delta |
1 |
Zoom step per keyboard/button press |
max-bounds-viscosity |
0 |
How much the bounds resist panning past them (0–1) |
Scroll wheel
| Attribute |
Default |
Description |
wheel-debounce-time |
40 |
Debounce delay for wheel events (ms) |
wheel-px-per-zoom-level |
60 |
Pixels of scroll per zoom level |
Keyboard & touch
| Attribute |
Default |
Description |
keyboard-pan-delta |
80 |
Pan distance per key press (px) |
tap-tolerance |
15 |
Max touch movement to trigger a tap (px) |
Rendering
| Attribute |
Default |
Description |
transform-3d-limit |
8388608 |
Max CSS translate3d component value before a layer reset |
Accessing the underlying map
const el = document.querySelector('leaflet-map');
el.leafletObject; // L.Map instance (undefined before connected)
el.zoom; // current zoom (reads live from map, falls back to attribute)
el.scrollWheelZoom = false; // disable at runtime
All leaflet-map properties are two-way: reading returns the live map value; writing updates both the attribute and the map.
Escape hatch
Every component exposes a leafletObject getter that returns the underlying Leaflet instance. This is useful when you need to call Leaflet API methods directly:
const marker = document.querySelector('leaflet-marker');
marker.leafletObject?.setLatLng([51.5, -0.09]);
const map = document.querySelector('leaflet-map');
map.leafletObject?.flyTo([48.86, 2.35], 13);
Returns undefined while the element isn't connected to the DOM.
CSS
Leaflet's stylesheet is loaded from a CDN via a <link> in the shadow DOM. Marker icon paths are derived from the CSS URL automatically.
| Attribute |
Default |
Description |
css-url |
https://unpkg.com/leaflet@1.9.4/dist/leaflet.css |
URL for Leaflet CSS |
css-integrity |
sha256-... |
SRI hash (required with default URL; when css-url is custom, no default integrity is used unless explicitly set) |
css-crossorigin |
(auto) |
"anonymous" when integrity is active, otherwise absent. Set explicitly to override. |
Resize
A ResizeObserver on the host element automatically calls map.invalidateSize() whenever the element's dimensions change — from CSS classes, inline styles, attribute changes, or parent layout.
Components
<leaflet-tile-layer>
| Attribute |
Default |
Description |
url |
'' |
Tile URL template (https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png) |
attribution |
'' |
Attribution text |
min-zoom |
0 |
Minimum zoom level |
max-zoom |
18 |
Maximum zoom level |
opacity |
1.0 |
Tile layer opacity |
z-index |
0 |
Z-index |
<leaflet-tile-layer-wms>
| Attribute |
Default |
Description |
url |
'' |
WMS service URL |
layers |
'' |
Comma-separated layer names |
styles |
'' |
Comma-separated style names |
format |
'image/jpeg' |
Image format |
transparent |
— |
Request transparent tiles |
version |
'1.1.1' |
WMS version |
uppercase |
— |
Use uppercase WMS parameter names |
<leaflet-marker>
| Attribute |
Default |
Description |
lat |
0 |
Latitude |
lng |
0 |
Longitude |
title |
'' |
Tooltip text on hover |
alt |
'' |
Alt text for the marker image |
draggable |
— |
Allow dragging the marker |
opacity |
1.0 |
Marker opacity |
z-index-offset |
0 |
Z-index offset |
<leaflet-circle>
All path style attributes apply.
| Attribute |
Default |
Description |
lat |
0 |
Center latitude |
lng |
0 |
Center longitude |
radius |
1000 |
Radius in meters |
<leaflet-circle-marker>
All path style attributes apply.
| Attribute |
Default |
Description |
lat |
0 |
Center latitude |
lng |
0 |
Center longitude |
radius |
10 |
Radius in pixels |
<leaflet-polyline>
Coordinates are taken from child <leaflet-line> elements, not from attributes. All path style attributes apply (excluding fill* — polylines don't fill).
| Attribute |
Default |
Description |
color |
'#3388ff' |
Stroke color |
weight |
3 |
Stroke width (px) |
opacity |
1.0 |
Stroke opacity |
<leaflet-polygon>
Coordinates are taken from child <leaflet-line> elements, not from attributes. All path style attributes apply.
<leaflet-rectangle>
All path style attributes apply.
| Attribute |
Default |
Description |
bounds |
'' |
Bounding box as JSON: [[south,west],[north,east]] |
<leaflet-line>
Vertex helper — not rendered directly. Used as a child of <leaflet-polyline> or <leaflet-polygon>.
| Attribute |
Default |
Description |
lat |
0 |
Latitude |
lng |
0 |
Longitude |
<leaflet-image-overlay>
| Attribute |
Default |
Description |
url |
'' |
Image URL |
bounds |
'' |
Bounding box as JSON: [[south,west],[north,east]] |
opacity |
1.0 |
Overlay opacity |
alt |
'' |
Alt text |
interactive |
— |
Receive mouse/touch events |
cross-origin |
'' |
CORS setting for the image |
error-overlay-url |
'' |
Fallback image on load error |
z-index |
0 |
Z-index |
class-name |
'' |
CSS class for the image element |
<leaflet-video-overlay>
| Attribute |
Default |
Description |
url |
'' |
Video URL |
bounds |
'' |
Bounding box as JSON: [[south,west],[north,east]] |
opacity |
1.0 |
Overlay opacity |
alt |
'' |
Alt text |
interactive |
— |
Receive mouse/touch events |
cross-origin |
'' |
CORS setting for the video |
loop |
— |
Loop playback |
autoplay |
— |
Start playing automatically |
muted |
— |
Mute audio |
playsinline |
— |
Play inline (mobile) |
<leaflet-svg-overlay>
Content comes from an inline <svg> child element. No url attribute.
| Attribute |
Default |
Description |
bounds |
'' |
Bounding box as JSON: [[south,west],[north,east]] |
opacity |
1.0 |
Overlay opacity |
interactive |
— |
Receive mouse/touch events |
cross-origin |
'' |
CORS setting |
z-index |
0 |
Z-index |
class-name |
'' |
CSS class for the SVG element |
<leaflet-layer-group>
Passthrough container. No attributes. Accepts layers, popups, and tooltips as children.
<leaflet-feature-group>
Passthrough container with Leaflet's FeatureGroup (supports getBounds(), style propagation, etc.). No attributes. Accepts layers, popups, and tooltips as children.
<leaflet-geojson>
All path style attributes apply.
| Attribute |
Default |
Description |
data |
'' |
GeoJSON string |
stroke |
'' |
Stroke color (overrides color) |
line-cap |
'round' |
Line cap style |
line-join |
'round' |
Line join style |
dash-array |
'' |
Dash pattern |
dash-offset |
'' |
Dash offset |
fill-rule |
'evenodd' |
Fill rule |
Content comes from innerHTML, not an attribute. Mutations to innerHTML sync automatically via a MutationObserver.
| Attribute |
Default |
Description |
lat |
0 |
Latitude (omit to auto-attach to the parent layer's position) |
lng |
0 |
Longitude |
max-width |
300 |
Maximum width (px) |
min-width |
50 |
Minimum width (px) |
max-height |
0 |
Maximum height (px; 0 = unlimited) |
auto-pan |
— |
Automatically pan the map to keep the popup visible |
close-button |
— |
Show a close button |
auto-close |
— |
Close the popup when another popup is opened |
<leaflet-tooltip>
Content comes from innerHTML, not an attribute.
| Attribute |
Default |
Description |
lat |
0 |
Latitude (omit to auto-attach to the parent layer's position) |
lng |
0 |
Longitude |
pane |
— |
Map pane name |
offset |
— |
Pixel offset as JSON: [x, y] |
direction |
'auto' |
Direction: 'right', 'left', 'top', 'bottom', 'center', 'auto' |
permanent |
— |
Always visible (no hover required) |
sticky |
— |
Follow the mouse |
opacity |
1.0 |
Tooltip opacity |
<leaflet-control-zoom>
| Attribute |
Default |
Description |
position |
'topleft' |
Corner position |
zoom-in-text |
'+' |
Zoom-in button label |
zoom-in-title |
'Zoom in' |
Zoom-in button tooltip |
zoom-out-text |
'-' |
Zoom-out button label |
zoom-out-title |
'Zoom out' |
Zoom-out button tooltip |
<leaflet-control-attribution>
| Attribute |
Default |
Description |
position |
'bottomright' |
Corner position |
prefix |
'' |
Text before the attribution |
<leaflet-control-scale>
| Attribute |
Default |
Description |
position |
'bottomleft' |
Corner position |
max-width |
100 |
Maximum width of the scale (px) |
metric |
— |
Show metric scale (m/km) |
imperial |
— |
Show imperial scale (mi/ft) |
update-when-idle |
— |
Update only when the map stops moving |
Path style
The following attributes apply to <leaflet-circle>, <leaflet-circle-marker>, <leaflet-polyline>, <leaflet-polygon>, <leaflet-rectangle>, and <leaflet-geojson>.
| Attribute |
Default |
Description |
color |
'#3388ff' |
Stroke color |
weight |
3 |
Stroke width (px) |
opacity |
1.0 |
Stroke opacity |
line-cap |
'round' |
Line cap: 'butt', 'round', 'square' |
line-join |
'round' |
Line join: 'miter', 'round', 'bevel' |
dash-array |
'' |
Dash pattern, e.g. '5, 10' |
dash-offset |
'' |
Dash offset |
fill |
— |
Enable fill |
fill-color |
'#3388ff' |
Fill color |
fill-opacity |
0.2 |
Fill opacity |
fill-rule |
'evenodd' |
Fill rule: 'nonzero', 'evenodd' |
Nesting rules
- Popup / tooltip as child of a layer → bound via
bindPopup / bindTooltip.
- Layer as child of a group → added via
addLayer.
- Any layer as child of
leaflet-map → added to the map directly.
leaflet-polygon / leaflet-polyline take their coordinates from <leaflet-line> children, not from attributes.
Development
npm run build # tsc emits individual ESM modules; Rollup bundles ESM, CJS + UMD (minified/unminified)
npm run typecheck # tsc --noEmit
npm run lint # ESLint
npm run format # Prettier
# serve the project root with any static-file server and open index.html