Skip to content

Overlay Compose UI

MaplibreMap draws controls on top of the map. The overlay block replaces the default controls when you supply it. The default draws a scale bar and a compass along the top edge. It draws the MapLibre logo and an attribution button along the bottom edge. The scale bar and the compass appear only while they are relevant.

App.kt
MaplibreMap()

Two variants cover common needs. MapOverlay.Full adds zoom in and zoom out buttons at the middle of the end edge. MapOverlay.AttributionOnly draws only the logo and the attribution button.

An empty block, or MapOverlay.None, draws no controls. Use it when your app shows the attribution somewhere else, such as an about screen.

App.kt
MaplibreMap {}

The trailing block draws the controls that you list. Inside a Box, Modifier.align positions a control against an inset edge of the map.

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 base library draws controls with a fixed palette that stays legible on any basemap. The maplibre-compose-material3 module draws the same controls with your Material 3 color scheme and typography. Add the dependency:

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

MapOverlay.Material3 draws the same controls as MapOverlay.Default in the same places. Include it in the block:

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

MapOverlay.Material3AttributionOnly and MapOverlay.Material3Full apply the theme to the other presets.

Each Material 3 control is also a composable that you can place in the trailing block:

App.kt
MaplibreMap {
Box(Modifier.fillMaxSize().safeDrawingPadding().padding(8.dp)) {
val mapState = checkNotNull(LocalMapState.current)
ScaleBar(
metersPerDp = { mapState.viewport?.metersPerDpAtTarget ?: 0.0 }, // (1)!
modifier = Modifier.align(Alignment.TopStart),
) // (2)!
CompassButton(modifier = Modifier.align(Alignment.TopEnd))
ZoomButtons(Modifier.align(Alignment.CenterEnd))
MaplibreLogo(Modifier.align(Alignment.BottomStart))
ExpandingAttributionButton(
modifier = Modifier.align(Alignment.BottomEnd),
contentAlignment = Alignment.BottomEnd,
)
}
}
  1. ScaleBar calls metersPerDp while it draws, so read the map state inside the lambda rather than passing a value.
  2. ScaleBar and CompassButton stay on screen. The Disappearing versions that MapOverlay.Material3 uses fade in when the zoom or the orientation changes, and fade out once it settles.

Overlay content uses MapOverlayScope to position direct children. Controls are ordinary composables that read the enclosing map through LocalMapState. LocalViewport exposes its current viewport, and LocalViewportInsets exposes its viewport insets. These values remain available inside nested composables and layouts. A nested map provides its own context.

The overlay fills the map without implicit padding. Arrange custom controls with Compose layouts such as Box, Row, and Column, and apply padding and system insets to those layouts.

The built-in presets use the larger of viewportInsets and safe-drawing insets on each edge, then add MapOverlay.Spacing. They consume padding and insets using Compose layout rules, accounting for safe areas handled by an outer layout. The padding takes effect without waiting for a map frame.

viewportInsets also shifts the camera center. For custom controls that follow this padding, read LocalViewportInsets and apply it to their container.

App.kt
val mapInsets = WindowInsets.safeDrawing.union(WindowInsets(bottom = 128.dp)) // (1)!
MaplibreMap(viewportInsets = mapInsets.asPaddingValues())
  1. union takes the larger inset on each edge.

Camera padding frames the target inside these insets without moving controls. Padding on a bounding-box camera move adds a margin inside both for that fit.

MapOverlayScope.placedAt puts a child on a geographic position. Include MapOverlay.Default to keep the default controls. A pan, a zoom, or a window resize moves the child with the map.

App.kt
MaplibreMap {
include(MapOverlay.Default)
Text(
"Next sailing 12:40",
Modifier.placedAt(position, alignment = Alignment.BottomCenter)
.padding(bottom = 8.dp), // (1)!
)
}
  1. Alignment.BottomCenter sits the bottom edge of the chip on the point. Padding lifts the chip above the feature.

Use MapOverlayScope.placedTowards for a direction indicator when a location is out of view. It places the child along the parent layout’s inscribed ellipse and hides it while the location is inside that ellipse.

Use GeographicLayout to choose a smaller placement region. Apply sizing and padding to that layout; geographic positions still refer to the enclosing map, including when the layout moves. Placement modifiers apply to direct children of the map overlay or GeographicLayout. To place a compound marker, apply the modifier to its outer container. Sizing and padding on a child describe that child, independent of the placement modifier’s position in the chain.

App.kt
MaplibreMap {
include(MapOverlay.Default)
GeographicLayout(Modifier.safeDrawingPadding().padding(8.dp)) {
val placement = rememberPlacedTowardsState() // (1)!
Text(
"▲",
Modifier.placedTowards(position, state = placement).graphicsLayer {
rotationZ = placement.angleDegrees // (2)!
},
)
}
}
  1. The state stores the placement that the overlay computed.
  2. angleDegrees is the direction of the target, clockwise from screen-up.

PointerPinButton uses this modifier to display a pin-shaped button that points at the target. The Material 3 module provides a themed version of it.