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

Overlay Compose UI

MaplibreMap draws Compose UI on top of the map in its trailing overlay block. Without a block, it draws MapOverlay.Default: a scale bar and a compass along the top edge, and the MapLibre logo and an attribution button along the bottom edge. The scale bar appears for a moment when the zoom changes. The compass appears while the map is rotated or tilted.

App.kt
MaplibreMap()

The attribution button shows the attribution text of the sources in the style.

include draws a preset inside the overlay block. MapOverlay.Full adds zoom buttons to the default controls, which helps users with a mouse. MapOverlay.AttributionOnly keeps only the logo and the attribution button.

100 m200 ft
Default

Scale bar, compass, logo, and attribution.

100 m200 ft
Full

Default controls plus zoom buttons.

100 m200 ft
AttributionOnly

Logo and attribution.

Scale bars and compasses are shown while active. Attribution is shown collapsed.

App.kt
MaplibreMap { include(MapOverlay.Full) }

An empty block draws nothing. Use it when your app shows the attribution somewhere else. Call attributions on mapState.style to get the attribution text of the style’s sources, and draw it with AttributionLinks.

App.kt
MaplibreMap {}

The overlay block lays out its children like a Box that covers the map. This example puts the controls in a Box with safeDrawingPadding(), so they stay clear of the system bars, and places each one with Modifier.align.

App.kt
MaplibreMap {
Box(Modifier.fillMaxSize().safeDrawingPadding().padding(8.dp)) {
MaplibreLogo(Modifier.align(Alignment.BottomStart))
ExpandingAttributionButton(
modifier = Modifier.align(Alignment.TopEnd),
contentAlignment = Alignment.TopEnd,
)
}
}

The library’s controls, such as ZoomButtons and CompassButton, are composables you can place the same way.

The base controls use fixed colors that are readable on light and dark maps. The maplibre-compose-material3 module draws the same controls with the colors and typography of your Material 3 theme. Add the dependency:

libs.versions.toml
[libraries]
maplibre-composeMaterial3 = { module = "org.maplibre.compose:maplibre-compose-material3", version = "0.20.0" }
build.gradle.kts
commonMain.dependencies {
implementation(libs.maplibre.composeMaterial3)
}

Then include MapOverlay.Material3. It places the same controls as MapOverlay.Default. MapOverlay.Material3Full and MapOverlay.Material3AttributionOnly are the Material 3 versions of MapOverlay.Full and MapOverlay.AttributionOnly.

App.kt
MaplibreMap { include(MapOverlay.Material3) }

The module has a Material 3 version of each control except the MapLibre logo, in the org.maplibre.compose.material3 package. Use them in your own layout:

App.kt
MaplibreMap {
Box(Modifier.fillMaxSize().safeDrawingPadding().padding(8.dp)) {
CompassButton(Modifier.align(Alignment.TopEnd))
ZoomButtons(Modifier.align(Alignment.CenterEnd))
MaplibreLogo(Modifier.align(Alignment.BottomStart))
ExpandingAttributionButton(Modifier.align(Alignment.BottomEnd))
}
}

A control can read the map it is drawn on through LocalMapState. You do not need to pass the map state to it. This label shows the current zoom level and updates as the user zooms:

ZoomLabel.kt
@Composable
fun ZoomLabel(modifier: Modifier = Modifier) {
val mapState = checkNotNull(LocalMapState.current) { "ZoomLabel must be inside a map overlay" }
Text("Zoom ${mapState.cameraPosition.zoom.roundToInt()}", modifier)
}
@Composable
fun MapWithZoomLabel() {
MaplibreMap {
include(MapOverlay.Default)
ZoomLabel(Modifier.align(Alignment.TopCenter).safeDrawingPadding().padding(8.dp))
}
}

LocalMapState is null outside a map. LocalViewport gives the camera, size, and visible area of the last frame the map drew.

When your app draws UI over part of the map, such as a bottom panel, pass that area to viewportInsets. The default controls then stay above the panel, and camera moves center their target in the part of the map the user can see.

App.kt
val panelHeight = 160.dp
Box(Modifier.fillMaxSize()) {
MaplibreMap(viewportInsets = PaddingValues(bottom = panelHeight))
Surface(Modifier.align(Alignment.BottomCenter).fillMaxWidth().height(panelHeight)) {
Text("Ferry schedule", Modifier.padding(16.dp))
}
}

Your own controls can read the same insets from LocalViewportInsets and apply them as padding.

MapOverlayScope.placedAt puts a composable on a geographic position. The composable moves with the map when the user pans or zooms. Use it for a label or a button that belongs to a place. Apply it to a direct child of the overlay block.

App.kt
MaplibreMap {
include(MapOverlay.Default)
Surface(
modifier =
Modifier.placedAt(position, alignment = Alignment.BottomCenter).padding(bottom = 8.dp),
shape = RoundedCornerShape(8.dp),
) {
Text("Next sailing 12:40", Modifier.padding(8.dp))
}
}

The alignment chooses which point of the composable sits on the position. Alignment.BottomCenter puts the bottom of the label on the position, and the bottom padding lifts it a little above.

PointerPinButton shows a pin at the edge of the map that points toward a position outside the view. The pin moves along an oval that fills its layout, and it hides while the position is inside that oval. In this example, a tap moves the camera to the position.

App.kt
val mapState = rememberMapState()
val scope = rememberCoroutineScope()
MaplibreMap(state = mapState) {
include(MapOverlay.Default)
GeographicLayout(Modifier.safeDrawingPadding().padding(8.dp)) {
PointerPinButton(
targetPosition = position,
onClick = { scope.launch { mapState.animateCamera(CameraUpdate(center = position)) } },
) {
Icon(
painterResource(Res.drawable.filter_center_focus_24px),
contentDescription = "Show the ferry terminal",
)
}
}
}

GeographicLayout sets the area that the pin moves around in. Here the padding keeps the pin away from the system bars and the edge of the map.

To draw your own indicator, use MapOverlayScope.placedTowards with rememberPlacedTowardsState. The state’s angleDegrees tells you which way to rotate it.