# 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 ``` Available on JSR as `@buddy/leaflet-components`. ## Usage Import once to register all custom elements, then use them declaratively in HTML. ```html Hello! Hover me I am a circle ``` ## `leaflet-map` The root component. All other components must be children of ``. ```html ``` ### 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.map; // 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. ### CSS Leaflet's stylesheet is loaded from a CDN via a `` 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 ### `` | 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 | ### `` | 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 | ### `` | 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 | ### `` All [path style](#path-style) attributes apply. | Attribute | Default | Description | |---|---|---| | `lat` | `0` | Center latitude | | `lng` | `0` | Center longitude | | `radius` | `1000` | Radius in meters | ### `` All [path style](#path-style) attributes apply. | Attribute | Default | Description | |---|---|---| | `lat` | `0` | Center latitude | | `lng` | `0` | Center longitude | | `radius` | `10` | Radius in pixels | ### `` Coordinates are taken from child `` elements, not from attributes. All [path style](#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 | ### `` Coordinates are taken from child `` elements, not from attributes. All [path style](#path-style) attributes apply. ### `` All [path style](#path-style) attributes apply. | Attribute | Default | Description | |---|---|---| | `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` | ### `` Vertex helper — not rendered directly. Used as a child of `` or ``. | Attribute | Default | Description | |---|---|---| | `lat` | `0` | Latitude | | `lng` | `0` | Longitude | ### `` | 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 | ### `` | 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) | ### `` Content comes from an inline `` 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 | ### `` Passthrough container. No attributes. Accepts layers, popups, and tooltips as children. ### `` Passthrough container with Leaflet's `FeatureGroup` (supports `getBounds()`, style propagation, etc.). No attributes. Accepts layers, popups, and tooltips as children. ### `` All [path style](#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 | ### `` 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 | ### `` | 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 | ### `` | Attribute | Default | Description | |---|---|---| | `position` | `'bottomright'` | Corner position | | `prefix` | `''` | Text before the attribution | ### `` | 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 ``, ``, ``, ``, ``, and ``. | 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 `` children, not from attributes. ## Development ```bash npm run build # type-check + emit .d.ts via tsc, then bundle to dist/index.js via esbuild npm run lint # ESLint npm run format # Prettier # serve the project root with any static-file server and open index.html ```