Skip to content
This site is a preview of the main branch. The published documentation is at maplibre.org.

Handle gestures and clicks

Users can pan, zoom, rotate, and pitch the map by default. MapInteractions sets which of these camera movements users can make and what your app does when they click. MapUiOptions sets which gesture, scroll, or key does what.

Pan

Drag with one finger.

Zoom and rotate

Pinch or spread; twist to rotate.

Pitch

Drag two fingers up or down.

Quick zoom

Double-tap, then drag up or down.

For quick zoom, dragging down zooms in. On iOS, dragging up zooms in.

Turn off a camera movement when your map must stay a certain way, such as north-up and flat. This map can be panned and zoomed, but not rotated or pitched:

App.kt
MaplibreMap(
interactions =
MapInteractions {
camera {
rotate { enabled = false }
pitch { enabled = false }
}
}
)

Settings that you leave out keep their defaults. To turn off every camera movement, pass MapInteractions.None; clicks still reach the layers’ onClick handlers. To make the map ignore all input, including clicks, pass MapUiOptions.None as uiOptions. To keep the camera inside an area or a zoom range instead, see Limit where the camera can go.

Some app behavior should stop when the user moves the map, such as following the user’s location. Pass a function to onStart, and the map calls it each time the user starts to pan:

App.kt
@Composable
fun TrackingMap(onUserPanned: () -> Unit) {
MaplibreMap(interactions = MapInteractions { camera { pan { onStart(onUserPanned) } } })
}

zoom, rotate, and pitch have onStart too.

Use callbacks.click to get the geographic position that the user clicked, for example to drop a pin there:

App.kt
@Composable
fun ClickableMap(onLocationSelected: (Position) -> Unit) {
MaplibreMap(
interactions =
MapInteractions {
callbacks {
click {
onEvent { event ->
event.position?.let(onLocationSelected)
ClickResult.Consume
}
}
}
}
)
}

The callback runs before any layer receives the click. Return ClickResult.Consume to stop there, or ClickResult.Pass to let layers receive the click too. For a touch long press or a right click, use longClick in the same way.

Give a layer an onClick handler to let users select its features. This map reports the earthquake that the user taps, and reports null when they tap anywhere else:

App.kt
@Composable
fun SelectableEarthquakes(onEarthquakeSelected: (JsonObject?) -> Unit) {
val state = rememberMapState {
val earthquakes =
rememberGeoJsonSource(
GeoJsonData.Uri("https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson")
)
CircleLayer(
id = "earthquakes",
source = earthquakes,
hitPadding = 12.dp,
onClick = { features ->
onEarthquakeSelected(features.first().properties)
ClickResult.Consume
},
)
}
MaplibreMap(
state = state,
interactions =
MapInteractions {
callbacks {
click {
onUnhandled {
onEarthquakeSelected(null)
ClickResult.Pass
}
}
}
},
)
}

onClick receives the layer’s features at the click, topmost first. Its receiver is the ClickEvent, so the handler can also read position and screenOffset. hitPadding widens the area around the click that counts as a hit, which makes small circles and thin lines easier to tap.

When several layers have features at the click, they receive it from the top layer down until one handler returns ClickResult.Consume. If none does, onUnhandled runs. To change how the selected feature looks, see Query and highlight features.

Scrolling zooms the map by default. This map pans when the user scrolls and zooms when they hold Ctrl and scroll:

App.kt
MaplibreMap(
uiOptions =
MapUiOptions {
bindings {
scroll {
mappings {
on(modifiers = Containing(KeyModifier.Ctrl), action = CameraAction.Zoom)
otherwise(CameraAction.Pan)
}
}
}
}
)

A mappings block replaces the default mappings. The map uses the first on row that matches the input, and otherwise matches any other input. Drags, taps, and keys have their own mappings blocks in bindings.

When a rotation ends within 7° of north, the map turns to exactly north. Use snapping to choose other bearings. This map snaps to north, east, south, and west:

App.kt
MaplibreMap(
interactions =
MapInteractions {
camera {
rotate {
snapping {
targets = BearingTargets.evenlySpaced(count = 4)
tolerance = 7.0
}
}
}
}
)

To turn snapping off, set enabled = false in the snapping block.

On Android, iOS, and macOS, the device gives haptic feedback when the user rotates the map past north. Use haptics to choose the bearings and how strong the feedback is. This map gives subtle feedback every 15°, standard feedback at east, south, and west, and emphasized feedback at north:

App.kt
MaplibreMap(
interactions =
MapInteractions {
camera {
rotate {
haptics {
notch(BearingTargets.evenlySpaced(24), HapticEmphasis.Subtle)
notch(BearingTargets.evenlySpaced(4), HapticEmphasis.Standard)
notch(BearingTargets.at(0.0), HapticEmphasis.Emphasized)
}
}
}
}
)

A haptics block replaces the default north notch. Where notches overlap, the strongest emphasis wins.

If your app recognizes gestures itself, for example in a host that is not Compose UI, pass them to the map with MapState.panBy, MapState.scaleBy, MapState.fling, and MapState.click. They follow the same MapInteractions settings and click callbacks as the built-in gestures. See Present outside Compose UI for hosts.