Docs / Basics / Info bubble

Info bubble

A bubble anchored to a marker, written in each platform own UI code — a Compose composable, a SwiftUI view, React elements. The SDK only handles positioning and the tail.

ANDROID
com.mapconductor.compose.info
iOS
MapConductorCore
REACT
@mapconductor/js-sdk-react

01 · How it works

The same shape on all three platforms: hold the selected marker in state and render the bubble only while it is selected. Opening and closing is app state, not SDK state.

STEP 01
Hold the selection

Keep the selected marker (or id) in state — a Set when several bubbles may be open.

STEP 02
Render inside the map

Put Marker and InfoBubble inside the map container and render the bubble only when selected.

STEP 03
Clear on map click

The map onMapClick clears the selection; marker onClick sets it.

Sample · a bubble opened by selecting its marker

02 · Four patterns

The four sample pages under infobubble map one-to-one onto four implementation patterns. Pick a pattern and a platform to see that sample.

Platform
Single line

The smallest form: one line of text from marker extra. Border, tail and padding stay at their defaults, so all you write is the content.

SimpleTextBubblePage.kt
var selectedMarker by remember { mutableStateOf<MarkerState?>(null) }
val markerState = remember {
    MarkerState(
        position = GeoPoint.fromLatLong(37.7749, -122.4194),
        icon = DefaultMarkerIcon(fillColor = Color.Blue, label = "SF"),
        extra = "San Francisco - The Golden Gate City",
        onClick = { selectedMarker = it },
    )
}

MapViewContainer(
    modifier = Modifier.fillMaxSize(),
    state = mapViewState,
    onMapClick = { selectedMarker = null },
    onMapLoaded = { selectedMarker = markerState },
) {
    Marker(markerState)

    selectedMarker?.let { marker ->
        InfoBubble(marker = marker) {
            Text(
                text = marker.extra as? String ?: "No information",
                color = MaterialTheme.colorScheme.primary,
                modifier = Modifier.padding(4.dp),
            )
        }
    }
}
Preview
San Francisco - The Golden Gate City
SF

The default tail sits directly above the marker (tailOffset x:0.5 / y:1.0).

Restyled

Change the frame. To adjust colours and radii, Android and React take individual arguments and iOS takes an InfoBubbleStyle. To draw the frame and tail yourself, all three platforms use InfoBubbleCustom — Compose Canvas, SwiftUI Shape, CSS ::before / ::after.

StyledInfoBubblePage.kt
val markerState1 by remember {
    mutableStateOf(
        MarkerState(
            id = "marker1",
            position = GeoPoint.fromLatLong(37.7749, -122.4194),
            icon = DefaultMarkerIcon(
                fillColor = Color.Blue,
                infoAnchor = Offset(0.5f, 0.25f),
                label = "1",
            ),
            draggable = true,
            onClick = onMarkerClick,
        ),
    )
}

selectedMarker?.let { marker ->
    val text = GeoPoint.from(marker.position).toUrlValue(6)
    InfoBubbleCustom(
        marker = marker,
        tailOffset = Offset(0f, 0.5f), // 左中央で接続
    ) {
        RightTailInfoBubble(
            bubbleColor = Color.White,
            borderColor = Color.Black,
        ) {
            Text(text = text, color = MaterialTheme.colorScheme.primary)
        }
    }
}
StyledInfoBubblePage.kt · RightTailInfoBubble
@Composable
private fun RightTailInfoBubble(
    bubbleColor: Color,
    borderColor: Color,
    contentPadding: Dp = 8.dp,
    cornerRadius: Dp = 4.dp,
    tailSize: Dp = 8.dp,
    content: @Composable () -> Unit,
) {
    Box(modifier = Modifier.wrapContentSize()) {
        Canvas(modifier = Modifier.matchParentSize()) {
            val path = Path().apply {
                // 角丸の矩形を描き、左辺の中央に三角のしっぽを足す
                lineTo(tail, height / 2 + tail / 2)
                lineTo(0f, height / 2)
                lineTo(tail, height / 2 - tail / 2)
                close()
            }
            drawPath(path, color = bubbleColor, style = Fill)
            drawPath(path, color = borderColor, style = Stroke(width = 2f))
        }
        Box(modifier = Modifier.padding(start = contentPadding + tailSize)) {
            content()
        }
    }
}
Preview
1
37.7749,-122.419404

The connection point comes from tailOffset (bubble side) and the icon infoAnchor (marker side).

Rich content

Put an object in extra and embed a full layout — heading, description, rating. Frame styling stays in bubbleColor / borderColor / contentPadding / cornerRadius so your content code only deals with content.

RichContentBubblePage.kt
data class LocationInfo(
    val name: String,
    val description: String,
    val rating: Float,
) : Serializable

selectedMarker?.let { marker ->
    val info = marker.extra as? LocationInfo ?: return@let
    InfoBubble(
        marker = marker,
        bubbleColor = if (isDarkTheme) Color.Black else Color.White,
        borderColor = if (isDarkTheme) Color.Gray else Color.Black,
        contentPadding = 16.dp,
        cornerRadius = 12.dp,
    ) {
        Column(modifier = Modifier.width(200.dp)) {
            Text(info.name, style = MaterialTheme.typography.headlineSmall,
                fontWeight = FontWeight.Bold)
            Spacer(Modifier.height(8.dp))
            Text(info.description, style = MaterialTheme.typography.bodyMedium)
            Spacer(Modifier.height(8.dp))
            Row(verticalAlignment = Alignment.CenterVertically) {
                repeat(5) { index ->
                    Icon(Icons.Default.Star, contentDescription = null,
                        tint = if (index < info.rating.toInt()) Color.Yellow else Color.Gray,
                        modifier = Modifier.size(16.dp))
                }
                Text(" ${info.rating}/5", style = MaterialTheme.typography.bodySmall)
            }
        }
    }
}
Preview
Golden Gate Park

A large urban park with gardens, museums, and recreational areas.

★★★★4.5/5
🌳

The Android and iOS samples also swap frame and background colours for dark mode.

Multiple bubbles

Hold the selection as a Set to keep several bubbles open. Render InfoBubble only for open markers, and put a tap handler in the content so the bubble can close itself.

MultipleBubblesPage.kt
var selectedMarkers by remember { mutableStateOf(setOf<String>()) }

val onMarkerClick: OnMarkerEventHandler = { markerState ->
    selectedMarkers =
        if (selectedMarkers.contains(markerState.id)) {
            selectedMarkers - markerState.id
        } else {
            selectedMarkers + markerState.id
        }
}

MapViewContainer(
    state = mapViewState,
    onMapClick = { selectedMarkers = emptySet() },
) {
    markerStates.forEach { markerState ->
        Marker(markerState)

        if (selectedMarkers.contains(markerState.id)) {
            InfoBubble(
                marker = markerState,
                bubbleColor = Color.White,
                borderColor = Color.Black,
            ) {
                Column(modifier = Modifier.clickable(true, onClick = {
                    selectedMarkers = selectedMarkers - markerState.id
                })) {
                    Text(markerState.extra as String, fontWeight = FontWeight.Bold)
                    Text("Tap to close", color = Color.Gray)
                }
            }
        }
    }
}
Preview
Restaurant ATap to close
1
Hotel BTap to close
2
Shop CTap to close
3

Map click closes all of them (back to an empty Set) in every sample.

03 · API

InfoBubble draws the framed, tailed bubble for you; InfoBubbleCustom only does the positioning. On iOS the styling arguments are bundled into InfoBubbleStyle.

PARAM
ANDROID
REACT
Description
marker
required
required
The marker the bubble is anchored to; its position and icon size set the connection point.
bubbleColor
Color.White
'#ffffff'
Bubble background colour.
borderColor
Color.Black
'#000000'
Border colour.
contentPadding
8.dp
8
Inner padding.
cornerRadius
4.dp
4
Corner radius.
tailSize
8.dp
8
Tail size.
content / children
required
required
Bubble contents — ordinary platform UI code.
InfoBubbleStyle · iOS

iOS bundles the styling arguments into this struct and passes it as style.

InfoBubbleStyle(
  bubbleColor: Color,
  borderColor: Color,
  contentPadding: CGFloat,
  cornerRadius: CGFloat,
  tailSize: CGFloat
)
InfoBubbleCustom

Draw the frame and tail yourself; the SDK only aligns the element.

marker: MarkerState
tailOffset: Offset   // ios: CGPoint
content / children
InfoBubble (position)

Anchors a bubble to a coordinate instead of a marker. All three platforms use InfoBubble with a position — the InfoBubble(position:) overload on Compose and iOS.

position: GeoPoint
// react: <InfoBubble position=… />
// compose / ios: InfoBubble(position:)

04 · Notes

No overlap without an icon

A marker with no icon is treated as the default 48px pin anchored at its tip, so the bubble is offset by the real pin size instead of collapsing onto the marker.

Shift with infoAnchor

The connection point on the icon side comes from infoAnchor (an Offset in Compose, x/y in React). The bubble keeps following while the marker is dragged.

Bubble content takes events

Compose clickable, SwiftUI gestures and React onClick all work inside the bubble, independent of the map onMapClick — so the close action can live in the content.

Related pages