Skip to content

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 provide standard UI elements for map interaction. See Map Configuration for detailed control configuration.

controls:
navigation: true # Zoom and rotation controls
geolocate: true # User geolocation button
scale: true # Distance scale
fullscreen: true # Fullscreen toggle
attribution: true # Attribution text

Each control can be positioned independently:

controls:
navigation:
enabled: true
position: top-left
scale:
enabled: true
position: bottom-right

Available positions:

  • top-left
  • top-right
  • bottom-left
  • bottom-right

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.

Legends display layer information and can be positioned anywhere on the map.

legend:
title: "Map Features"
position: top-right
collapsed: false
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: circle
  • circle - Circular symbol
  • square - Square symbol
  • line - Line symbol
  • icon - Custom icon (requires icon property)
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"

Control which user interactions are enabled.

Create a static presentation map:

config:
center: [-74.006, 40.7128]
zoom: 12
interactive: false
mapStyle: "..."

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: "..."
PropertyTypeDefaultDescription
interactivebooleantrueEnable all interactions
scrollZoombooleantrueScroll to zoom
boxZoombooleantrueShift+drag box zoom
dragRotatebooleantrueRight-drag to rotate
dragPanbooleantrueDrag to pan
keyboardbooleantrueKeyboard shortcuts
doubleClickZoombooleantrueDouble-click zoom
touchZoomRotatebooleantrueTouch zoom/rotate
touchPitchbooleantrueTwo-finger pitch

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
PropertyTypeDescription
cursorstringCSS cursor while the pointer is over a feature
highlightbooleanRecolour 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: true on the source, or promoteId to use a property as the id. If neither is set, highlight enables generateId and warns — but generated ids are not stable across data refreshes, so promoteId is the durable choice.
  • Already using an expression for that colour? Then highlight leaves 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.

PropertyTypeDescription
popuparrayPopup content blocks (see Popups)
flyToobjectAnimate the camera to the clicked 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, milliseconds

When a layer configures both, the popup opens first and then travels with the camera.

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 fired
interactive:
click:
action: openDetails
// Use the event instead
map.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.

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: pointer

When keyboard interaction is enabled (keyboard: true), the following shortcuts are available:

KeysAction
+ / =Zoom in
-Zoom out
Arrow keysPan map
Shift + Arrow keysRotate map
Shift + + / -Increase/decrease pitch
config:
center: [0, 0]
zoom: 2
keyboard: false
mapStyle: "..."

Control touch gestures on mobile devices.

config:
center: [0, 0]
zoom: 2
touchZoomRotate: true
touchPitch: true
mapStyle: "..."

Prevent accidental camera tilt:

config:
center: [0, 0]
zoom: 2
touchZoomRotate: true
touchPitch: false
mapStyle: "..."

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.006

The map automatically changes the cursor to indicate interactivity:

  • Default cursor over the map
  • Pointer cursor over interactive features
  • Grab cursor when dragging
- 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: square
- 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-left
- 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: square
- 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 mobile
  1. Limit interactive features: Too many interactive features can impact performance
  2. Use feature-state: Prefer feature-state for hover/select styling over re-rendering
  3. Throttle updates: Limit update frequency for real-time data sources
  1. Provide visual feedback: Use hover states to indicate clickable features
  2. Clear controls: Position controls logically and consistently
  3. Mobile-first: Disable complex gestures like pitch on mobile
  4. Accessibility: Include keyboard navigation and clear visual indicators
  1. Set zoom limits: Use minZoom/maxZoom to keep users in relevant area
  2. Geographic bounds: Use maxBounds to restrict panning
  3. Disable unnecessary interactions: Remove interactions that don’t serve your use case
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
};