You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
Buddy 255d10499b docs: bring README and CLAUDE.md up to date
README.md:
- Import options: drop CJS/UMD/minified-bundle mentions, none exist since
  the build simplified to ESM-only.
- New Events + TypeScript sections documenting the leaflet:<type> event
  forwarding mechanism and the HTMLElementTagNameMap / per-component
  addEventListener typing added this session.
- leaflet-tile-layer: 6 -> 23 attributes (full tileLayerProps), fixed
  z-index default (0 -> 1).
- leaflet-tile-layer-wms: now documents that it inherits all tile-layer
  attributes (previously listed none, matching the bug fixed earlier) +
  the new crs attribute.
- leaflet-video-overlay: added the 4 attributes added this session.
- leaflet-polyline: fixed a wrong "excludes fill*" claim and added
  smooth-factor/no-clip.
- leaflet-geojson: was documenting a stale hand-picked subset of path
  attributes (with stroke mis-described as a color); now says all
  path-style attributes apply, matching the dedup against pathProps.
- Path style table: added stroke/interactive/bubbling-mouse-events/
  class-name/pane, fixed fill's default.
- Added leaflet-icon/leaflet-div-icon sections (existed but were
  undocumented).
- Nesting rules and Development section brought in line with reality.

CLAUDE.md: the WithProps mixin description referenced an API from before
this session that no longer exists (WithProps(Base, PROPS), definePropAccessors,
initOptions(), updateLeafletObject()) and claimed LeafletMap extends
HTMLElement directly when it extends WithProps(...) like everything else.
Rewrote to match the real internals, fixed the child-component-pattern
steps, and documented how to type a new component's events.

Also added '*.md' to the format script's glob -- root markdown files
weren't covered, so this formatting could have silently drifted again.
4 weeks ago
demos feat: split leaflet-icon into image Icon and DivIcon, event-only marker↔icon handshake 3 months ago
src feat: type leaflet-* components and their leaflet: events 4 weeks ago
test feat: type leaflet-* components and their leaflet: events 4 weeks ago
.gitignore feat: Initial implementation of leaflet-components 4 months ago
CLAUDE.md docs: bring README and CLAUDE.md up to date 4 weeks ago
README.md docs: bring README and CLAUDE.md up to date 4 weeks ago
index.html feat: split leaflet-icon into image Icon and DivIcon, event-only marker↔icon handshake 3 months ago
jsr.json feat: Initial implementation of leaflet-components 4 months ago
oxlint.config.ts test: add a Vitest + jsdom test suite 4 weeks ago
package-lock.json test: add a Vitest + jsdom test suite 4 weeks ago
package.json docs: bring README and CLAUDE.md up to date 4 weeks ago
prettier.config.js core: rename prettier config and run prettier 3 months ago
tsconfig.json refactor: use .ts extension for relative imports in source 3 months ago
tsconfig.test.json test: add a Vitest + jsdom test suite 4 weeks ago
vitest.config.ts test: add a Vitest + jsdom test suite 4 weeks ago

README.md

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 is ESM-only — tsc emits dist/ as individual modules, one per component, with no bundling step. Deep imports work for registering only what you use.

// Registers all components
import 'leaflet-components';

// Deep import — register only one component
import 'leaflet-components/dist/components/leaflet-marker.js';

leaflet is always an external dependency — you must install it yourself. There is no CommonJS or UMD build.

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></leaflet-map>

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 (01)

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.

Events

Every event the underlying Leaflet object fires is re-emitted on the element as a leaflet:<type> DOM event — zoomend becomes leaflet:zoomend, popupopen becomes leaflet:popupopen, and so on. This is generic and automatic for every component; nothing needs to be configured per event.

const map = document.querySelector('leaflet-map');
map.addEventListener('leaflet:moveend', (e) => {
  console.log(e.detail.target.getCenter());
});

const marker = document.querySelector('leaflet-marker');
marker.addEventListener('leaflet:dragend', (e) => {
  console.log('dragged', e.detail.distance, 'px');
});

event.detail is the same object a native Leaflet .on() listener receives — it always carries type, target, and sourceTarget in addition to whatever fields that particular event adds (latlng for mouse events, popup for popupopen/popupclose, distance for dragend, etc.). These events don't bubble: Leaflet already propagates layer events up to the map internally, so a bubbling DOM event would make <leaflet-map> see each one twice.

Which events a component can fire depends on the Leaflet class it wraps — see Leaflet's own event reference for the full list per class (Map, Layer, Marker, Path, Popup, TileLayer, etc.).

TypeScript

leaflet-* elements are typed. document.createElement/querySelector infer the right component class for every tag:

const map = document.querySelector('leaflet-map'); // LeafletMap | null

addEventListener is typed per component against the real Leaflet event payload for events that component actually fires — ordinary DOM events (click, etc.) still work normally, and an event name the component doesn't fire is a compile error:

marker.addEventListener('leaflet:dragend', (e) => {
  e.detail.distance; // number — DragEndEvent
});

// Type error: baselayerchange is only fired by <leaflet-map>, not <leaflet-marker>.
marker.addEventListener('leaflet:baselayerchange', () => {});

There's no generic fallback overload for leaflet:* names — that's deliberate, so a typo or a wrong event name doesn't silently type-check. A genuinely dynamic (non-literal) event name string needs a cast.


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 1 Z-index
subdomains 'abc' Subdomains for {s} in the URL template
tms Use TMS tile coordinate scheme
zoom-offset 0 Offset added to the zoom level when requesting tiles
zoom-reverse Reverse the zoom level when requesting tiles
detect-retina Request higher-resolution tiles on retina displays
cross-origin '' CORS setting for tile images
referrer-policy referrerPolicy for tile image requests
error-tile-url '' Fallback tile image on load error
tile-size 256 Tile size in pixels
no-wrap Don't wrap tiles horizontally across the antimeridian
bounds '' Restrict tile loading to this bounding box, as JSON: [[south,west],[north,east]]
class-name '' CSS class for tile images
min-native-zoom Lowest zoom level the tile source natively supports
max-native-zoom Highest zoom level the tile source natively supports
keep-buffer 2 Extra rows/columns of tiles to keep loaded outside the viewport
update-when-idle Update tiles only when the map stops moving
update-when-zooming true (toggle with update-when-zooming="false") Update tiles continuously while zooming
update-interval 200 Throttle between tile updates while panning (ms)
pane 'tilePane' Map pane name

Most of these apply only at construction — Leaflet exposes no setter for them, so changing the attribute afterward has no effect. min-zoom, max-zoom, opacity, and z-index are live.

<leaflet-tile-layer-wms>

All <leaflet-tile-layer> attributes above apply (a WMS layer is a tile layer), plus:

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
crs Coordinate reference system by name: 'EPSG3857', 'EPSG4326', 'EPSG3395', or 'Simple'

<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

A <leaflet-icon> or <leaflet-div-icon> child sets the marker's icon (see below); without one, Leaflet's default icon is used.

<leaflet-icon>

Sets the marker icon to an image. Not rendered itself — swapped into a parent <leaflet-marker> via setIcon() whenever an attribute changes. Produces no icon (Leaflet's default is used instead) until icon-url is set.

Attribute Default Description
icon-url '' Icon image URL (required)
icon-retina-url '' Higher-resolution icon image URL
icon-size Icon size as JSON: [width, height]
icon-anchor Point of the icon aligned to the marker's location, as JSON: [x, y]
popup-anchor Popup anchor point relative to icon-anchor, as JSON: [x, y]
tooltip-anchor Tooltip anchor point relative to icon-anchor, as JSON: [x, y]
shadow-url '' Shadow image URL
shadow-retina-url '' Higher-resolution shadow image URL
shadow-size Shadow size as JSON: [width, height]
shadow-anchor Shadow anchor point, as JSON: [x, y]
class-name '' CSS class for the icon image

<leaflet-div-icon>

Sets the marker icon to an HTML element instead of an image. Content comes from innerHTML (or the html attribute if no markup is given) — either way it's rebuilt on every change, the same as <leaflet-icon>.

Attribute Default Description
icon-size Icon size as JSON: [width, height]
icon-anchor Point of the icon aligned to the marker's location, as JSON: [x, y]
popup-anchor Popup anchor point relative to icon-anchor, as JSON: [x, y]
tooltip-anchor Tooltip anchor point relative to icon-anchor, as JSON: [x, y]
class-name 'leaflet-div-icon' CSS class for the icon element
html '' HTML content, if not using innerHTML
bg-pos Background position offset, as JSON: [x, y]

<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, except fill defaults to unfilled (a polyline isn't a closed shape, but it can still be filled explicitly).

Attribute Default Description
smooth-factor 1.0 Simplification factor applied while panning/zooming — higher is more simplified
no-clip Disable polyline clipping

<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)
z-index 0 Z-index
class-name '' CSS class for the video element
keep-aspect-ratio true (toggle with keep-aspect-ratio="false") Preserve the video's aspect ratio within its bounds
error-overlay-url '' Fallback image on load error

<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 — each feature GeoJSON builds gets them as its style.

Attribute Default Description
data '' GeoJSON string

<leaflet-popup>

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

<leaflet-control-layers>

The built-in Leaflet layer switcher. Children with type="base" appear as radio buttons; children with type="overlay" (or no type) appear as checkboxes.

<leaflet-map>
  <leaflet-control-layers position="topright" collapsed>
    <leaflet-tile-layer type="base" name="Streets" active url="..."></leaflet-tile-layer>
    <leaflet-tile-layer type="base" name="Satellite" url="..."></leaflet-tile-layer>
    <leaflet-marker type="overlay" name="Cities" active lat="51.5" lng="-0.09"></leaflet-marker>
  </leaflet-control-layers>
</leaflet-map>
Attribute Default Description
position 'topright' Corner position
collapsed true (toggle with collapsed="false") Collapse into an icon until hovered
auto-z-index true Assign increasing z-indexes to layers
hide-single-base Hide the base layers section when only one base layer exists
sort-layers Sort layers alphabetically

Each child layer inside <leaflet-control-layers> may carry:

Attribute Description
type="base" Show as a radio button (base layer). Omit or set type="overlay" for a checkbox.
name Human-readable label displayed in the control.
active Add the layer to the map immediately so it starts visible. Internally, the control dispatches a leaflet-add-layer event that the map handles. Layers without active are removed from the map before the control renders, so they correctly appear unchecked.

The control intercepts child leaflet-register events and stops propagation — layers inside <leaflet-control-layers> are managed by the layers control, not added directly to the map.

Path style

The following attributes apply to <leaflet-circle>, <leaflet-circle-marker>, <leaflet-polyline>, <leaflet-polygon>, <leaflet-rectangle>, and <leaflet-geojson>.

Attribute Default Description
stroke true (toggle with stroke="false") Draw the stroke
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 true (toggle with fill="false") Enable fill
fill-color '#3388ff' Fill color
fill-opacity 0.2 Fill opacity
fill-rule 'evenodd' Fill rule: 'nonzero', 'evenodd'
interactive true (toggle with interactive="false") Receive mouse/touch events
bubbling-mouse-events true (toggle with bubbling-mouse-events="false") Let mouse events on this layer also reach the map
class-name '' CSS class for the path element
pane 'overlay' Map pane name

stroke, color, weight, opacity, line-cap, line-join, dash-array, dash-offset, fill, fill-color, fill-opacity, and fill-rule are live — Leaflet has a setter for them (setStyle). interactive, bubbling-mouse-events, class-name, and pane only apply at construction.

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.
  • <leaflet-icon> / <leaflet-div-icon> as child of <leaflet-marker> → swapped in via setIcon().
  • Layer as child of <leaflet-control-layers> → intercepted by the control and registered as a base or overlay entry. The control stops propagation so the layer doesn't reach the map directly. Use active to start the layer visible.

Development

npm run build      # tsc emits individual ESM modules to dist/ (no bundling)
npm run typecheck  # tsc --noEmit, then tsc -p tsconfig.test.json for test/
npm run lint       # oxlint over src/ and test/
npm run format     # Prettier
npm run test       # vitest run
npm run test:watch # vitest, watch mode
# serve the project root with any static-file server and open index.html