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.
leaflet-components/README.md

581 lines
38 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# leaflet-components
[Leaflet.js](https://leafletjs.com/) as native Web Components (Custom Elements). Each HTML element maps 1:1 to a Leaflet object with reactive attribute binding.
## Installation
```bash
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.
```js
// 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.
```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>`.
```html
<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 (`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
```js
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:
```js
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.
```js
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](https://leafletjs.com/reference.html) 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:
```ts
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:
```ts
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](#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](#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](#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](#path-style) attributes apply.
### `<leaflet-rectangle>`
All [path style](#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](#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.
```html
<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
```bash
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
```