Interactivity
Interactivity in maplibre-yaml is primarily configured through:
- Map Controls - Navigation, scale, geolocation, fullscreen
- Legends - Interactive layer information
- Layer Interactions - Click, hover, and pointer events (configured at layer level)
Map Controls
Section titled “Map Controls”Map controls provide standard UI elements for map interaction. See Map Configuration for detailed control configuration.
Available Controls
Section titled “Available Controls”controls: navigation: true # Zoom and rotation controls geolocate: true # User geolocation button scale: true # Distance scale fullscreen: true # Fullscreen toggle attribution: true # Attribution textControl Positioning
Section titled “Control Positioning”Each control can be positioned independently:
controls: navigation: enabled: true position: top-left scale: enabled: true position: bottom-rightAvailable positions:
top-lefttop-rightbottom-leftbottom-right
Attribution
Section titled “Attribution”attribution renders a real attribution control and additionally accepts
MapLibre’s own options:
controls: attribution: position: bottom-right # default compact: true # collapse behind an (i) button customAttribution: "© Example Data"Configuring it suppresses MapLibre’s built-in attribution so only one control renders. Leave it unset and the built-in one still appears — attribution is a licensing obligation for most tile providers, so it is never removed silently.
Interactive Legends
Section titled “Interactive Legends”Legends display layer information and can be positioned anywhere on the map.
Basic Legend
Section titled “Basic Legend”legend: title: "Map Features" position: top-right collapsed: falseCustom Legend Items
Section titled “Custom Legend Items”legend: title: "Data Categories" position: top-left items: - color: "#e74c3c" label: "High Priority" shape: circle - color: "#f39c12" label: "Medium Priority" shape: circle - color: "#2ecc71" label: "Low Priority" shape: circleLegend Item Shapes
Section titled “Legend Item Shapes”circle- Circular symbolsquare- Square symbolline- Line symbolicon- Custom icon (requiresiconproperty)
legend: title: "Transportation" items: - color: "#3b82f6" label: "Highway" shape: line - color: "#10b981" label: "Local Road" shape: line - color: "#000000" label: "Restaurant" shape: icon icon: "restaurant"Map Interaction Settings
Section titled “Map Interaction Settings”Control which user interactions are enabled.
Disable All Interactions
Section titled “Disable All Interactions”Create a static presentation map:
config: center: [-74.006, 40.7128] zoom: 12 interactive: false mapStyle: "..."Selective Interactions
Section titled “Selective Interactions”Enable specific interactions:
config: center: [-74.006, 40.7128] zoom: 12 scrollZoom: true dragPan: true dragRotate: false # Disable rotation touchPitch: false # Disable pitch on mobile doubleClickZoom: false # Disable double-click zoom mapStyle: "..."Interaction Properties
Section titled “Interaction Properties”| Property | Type | Default | Description |
|---|---|---|---|
interactive | boolean | true | Enable all interactions |
scrollZoom | boolean | true | Scroll to zoom |
boxZoom | boolean | true | Shift+drag box zoom |
dragRotate | boolean | true | Right-drag to rotate |
dragPan | boolean | true | Drag to pan |
keyboard | boolean | true | Keyboard shortcuts |
doubleClickZoom | boolean | true | Double-click zoom |
touchZoomRotate | boolean | true | Touch zoom/rotate |
touchPitch | boolean | true | Two-finger pitch |
Layer Interactivity
Section titled “Layer Interactivity”Layers declare interactivity through an interactive: block. These are real
schema fields handled by the renderer — not something you wire up yourself.
- id: stations type: circle source: type: geojson url: "https://example.com/stations.geojson" generateId: true paint: circle-radius: 8 circle-color: "#3b82f6" interactive: hover: cursor: pointer highlight: true click: popup: - h3: [{ property: name }] - p: [{ property: description }] flyTo: zoom: 14 duration: 600| Property | Type | Description |
|---|---|---|
cursor | string | CSS cursor while the pointer is over a feature |
highlight | boolean | Recolour the hovered feature |
highlight: true wraps the layer’s primary colour in a feature-state expression
for you, so no paint authoring is required. Two things follow from how
feature-state works:
- Features need ids. Set
generateId: trueon the source, orpromoteIdto use a property as the id. If neither is set,highlightenablesgenerateIdand warns — but generated ids are not stable across data refreshes, sopromoteIdis the durable choice. - Already using an expression for that colour? Then
highlightleaves your paint alone and warns, rather than overwriting your data-driven styling. Reference["feature-state", "hover"]yourself, as in Custom highlight styling below.
Because feature-state is per source, two layers sharing one source share highlight state.
| Property | Type | Description |
|---|---|---|
popup | array | Popup content blocks (see Popups) |
flyTo | object | Animate the camera to the clicked feature |
emit | object | Dispatch a named host event with a payload projected from the feature |
flyTo accepts center, zoom, and duration. center defaults to the
clicked point, and zoom/duration fall through to MapLibre’s defaults when
omitted:
interactive: click: flyTo: zoom: 14 # optional duration: 600 # optional, millisecondsWhen a layer configures both, the popup opens first and then travels with the camera.
click.emit dispatches a named host event carrying a declarative payload
projected from the clicked feature. Unlike popup and flyTo, which act on the
map itself, emit hands data to code the host wrote — so it is gated on trust
(see the caveat below).
| Property | Type | Description |
|---|---|---|
event | string | The event name, resolved against the host’s handler map |
payload | object | Named keys, each a declarative projection from the feature |
Each payload key is projected independently:
str— a static literal value.property— read a feature property, with an optionalelsefallback.
version: 1type: mapid: parcels-mapconfig: center: [-73.98, 40.75] zoom: 12 mapStyle: "https://demotiles.maplibre.org/style.json"layers: - id: parcels type: circle source: type: geojson url: "https://example.com/parcels.geojson" generateId: true paint: circle-radius: 6 circle-color: "#3b82f6" interactive: click: emit: event: select payload: id: property: id label: property: name else: "Untitled" kind: str: parcelA missing or null property yields a defined-absent value: the key stays in
the payload carrying its else value, or null when no else is declared. It
is never omitted and never throws, so a handler sees a stable payload shape
across features that carry different properties. Payload fields are data only —
str, property, and else — with no formatting, markup, or executable
fields; unlike popup, an unrecognised field is stripped as an unknown key
rather than passed through.
emit needs a trusted host — it is inert under <ml-map> today. It is a
host-event interaction: it fires only when the map is wired up with
attachInteractions(map, projection, options) and both of these hold:
options.policydeclarestrust: "trusted". The host hook is default-deny; an untrusted (or absent) policy shuts the gate and the interaction does nothing.options.hostHandlershas a matching entry for the event name. Resolution is closed-world — an unregistered name is denied, never dispatched.
Both are required: a trusted policy with no handler does nothing, and a registered handler under an untrusted policy does nothing.
The projection you pass in must carry emit in the first place: it fires only
when projectInteractions(model, policy) also ran under a trusted policy. An
untrusted (or omitted) projection policy drops the emit block at projection
time — fail-closed — so attachInteractions never sees it, no matter how it is
configured. The Interactions & Eject guide shows the
end-to-end wiring (projectInteractions(model, { trust: "trusted" }) →
attachInteractions).
Because of this, emit is a silent no-op under the <ml-map> web component,
which defaults to an untrusted policy and registers no host handlers
(fail-closed). Making emit live under <ml-map> is tracked as follow-up work
(bead ml-1lz).
Deprecated: action
Section titled “Deprecated: action”click.action, mouseenter.action, and mouseleave.action are accepted by the
schema but have never been dispatched at runtime. They emit a deprecation
warning and will be removed in v2. Listen for the corresponding event
instead:
# Deprecated — never firedinteractive: click: action: openDetails// Use the event insteadmap.addEventListener("ml-map:layer-click", (e) => { const { layerId, feature } = e.detail;});Deprecation warnings are exempt from mlym validate’s CI strict mode for one
minor release, so upgrading will not fail your build. Opt in early with
mlym validate --strict-deprecations.
Custom highlight styling
Section titled “Custom highlight styling”To control the highlight colour yourself, author the paint expression directly
and leave highlight off:
- id: interactive-fill type: fill source: type: geojson url: "https://example.com/regions.geojson" generateId: true paint: fill-color: - case - ["boolean", ["feature-state", "hover"], false] - "#3b82f6" # Blue when hovered - "#e5e7eb" # Gray otherwise fill-opacity: - case - ["boolean", ["feature-state", "hover"], false] - 0.8 - 0.5 interactive: hover: cursor: pointerKeyboard Shortcuts
Section titled “Keyboard Shortcuts”When keyboard interaction is enabled (keyboard: true), the following shortcuts are available:
| Keys | Action |
|---|---|
+ / = | Zoom in |
- | Zoom out |
| Arrow keys | Pan map |
| Shift + Arrow keys | Rotate map |
Shift + + / - | Increase/decrease pitch |
Disable Keyboard
Section titled “Disable Keyboard”config: center: [0, 0] zoom: 2 keyboard: false mapStyle: "..."Touch Interactions
Section titled “Touch Interactions”Control touch gestures on mobile devices.
Enable All Touch
Section titled “Enable All Touch”config: center: [0, 0] zoom: 2 touchZoomRotate: true touchPitch: true mapStyle: "..."Disable Pitch on Touch
Section titled “Disable Pitch on Touch”Prevent accidental camera tilt:
config: center: [0, 0] zoom: 2 touchZoomRotate: true touchPitch: false mapStyle: "..."URL Hash Syncing
Section titled “URL Hash Syncing”Sync map state (center, zoom, bearing, pitch) with the URL hash for bookmarkable views.
config: center: [-74.006, 40.7128] zoom: 12 hash: true # Enable URL hash syncing mapStyle: "..."When enabled, the URL will update as users interact with the map:
https://example.com/map#12/40.7128/-74.006Cursor Styling
Section titled “Cursor Styling”The map automatically changes the cursor to indicate interactivity:
- Default cursor over the map
- Pointer cursor over interactive features
- Grab cursor when dragging
Complete Examples
Section titled “Complete Examples”Fully Interactive Map
Section titled “Fully Interactive Map”- type: map id: interactive-map config: center: [-74.006, 40.7128] zoom: 12 hash: true mapStyle: "https://demotiles.maplibre.org/style.json" layers: - id: buildings type: fill source: type: geojson url: "https://example.com/buildings.geojson" paint: fill-color: - case - ["boolean", ["feature-state", "hover"], false] - "#3b82f6" - "#cbd5e1" fill-opacity: 0.7 controls: navigation: true geolocate: true scale: true fullscreen: true legend: title: "Buildings" position: top-right items: - color: "#cbd5e1" label: "Building" shape: square - color: "#3b82f6" label: "Hovered" shape: squareLimited Interaction Map
Section titled “Limited Interaction Map”- type: map id: limited-map config: center: [0, 20] zoom: 2 scrollZoom: true dragPan: true dragRotate: false touchPitch: false doubleClickZoom: false keyboard: false minZoom: 1 maxZoom: 8 mapStyle: "https://demotiles.maplibre.org/style.json" layers: - id: data-layer type: circle source: type: geojson url: "https://example.com/data.geojson" paint: circle-radius: 6 circle-color: "#3b82f6" controls: navigation: true scale: true legend: title: "Data Points" position: top-leftStatic Presentation Map
Section titled “Static Presentation Map”- type: map id: static-map style: "height: 500px;" config: center: [-74.006, 40.7128] zoom: 12 pitch: 45 bearing: -30 interactive: false attributionControl: false mapStyle: "https://demotiles.maplibre.org/style.json" layers: - id: buildings type: fill-extrusion source: type: vector url: "https://demotiles.maplibre.org/tiles/tiles.json" source-layer: building paint: fill-extrusion-color: "#aaa" fill-extrusion-height: ["get", "height"] fill-extrusion-opacity: 0.8 legend: title: "3D Buildings" position: bottom-right items: - color: "#aaa" label: "Building" shape: squareTouch-Optimized Mobile Map
Section titled “Touch-Optimized Mobile Map”- type: map id: mobile-map config: center: [-74.006, 40.7128] zoom: 12 touchZoomRotate: true touchPitch: false # Disable pitch to prevent accidental tilts dragRotate: false # Disable rotation keyboard: false # Not needed on mobile mapStyle: "https://demotiles.maplibre.org/style.json" layers: - id: points type: circle source: type: geojson url: "https://example.com/points.geojson" paint: circle-radius: 12 # Larger for touch targets circle-color: "#3b82f6" circle-stroke-width: 2 circle-stroke-color: "#ffffff" controls: navigation: enabled: true position: top-right geolocate: enabled: true position: top-right scale: enabled: true position: bottom-left legend: title: "Locations" position: top-left collapsed: true # Start collapsed on mobileBest Practices
Section titled “Best Practices”Performance
Section titled “Performance”- Limit interactive features: Too many interactive features can impact performance
- Use feature-state: Prefer feature-state for hover/select styling over re-rendering
- Throttle updates: Limit update frequency for real-time data sources
User Experience
Section titled “User Experience”- Provide visual feedback: Use hover states to indicate clickable features
- Clear controls: Position controls logically and consistently
- Mobile-first: Disable complex gestures like pitch on mobile
- Accessibility: Include keyboard navigation and clear visual indicators
Map Constraints
Section titled “Map Constraints”- Set zoom limits: Use
minZoom/maxZoomto keep users in relevant area - Geographic bounds: Use
maxBoundsto restrict panning - Disable unnecessary interactions: Remove interactions that don’t serve your use case
TypeScript Types
Section titled “TypeScript Types”import { type ControlsConfig, type LegendConfig, type MapConfig} from '@maplibre-yaml/core/schemas';
const controls: ControlsConfig = { navigation: { enabled: true, position: "top-right" }, scale: true, geolocate: true};
const legend: LegendConfig = { title: "Features", position: "top-left", collapsed: false, items: [ { color: "#ff0000", label: "High", shape: "circle" }, { color: "#00ff00", label: "Low", shape: "circle" } ]};
const interactiveConfig: MapConfig = { center: [-74.006, 40.7128], zoom: 12, mapStyle: "https://demotiles.maplibre.org/style.json", interactive: true, scrollZoom: true, dragPan: true, hash: true};