@atomiqlab/react-native-mapbox-navigation
v2.0.4
Published
Native Mapbox turn-by-turn navigation for Expo and React Native (iOS + Android)
Maintainers
Readme
@atomiqlab/react-native-mapbox-navigation
Embedded Mapbox turn-by-turn navigation for Expo and React Native on iOS and Android.
This package is 2.x and embedded-only. Full-screen startNavigation(...) flows were removed. The main entry point is MapboxNavigationView.
What You Get
- Native Mapbox navigation UI embedded in a React Native view
- Expo config plugin for Mapbox token wiring and required native permissions
- Optional React overlay bottom sheet
- Optional React overlay floating buttons
- Per-button control over the built-in native floating buttons
- Package-managed end-of-route rating modal, or a custom replacement
- Runtime helpers (
setMuted,stopNavigation, etc.) and event listeners
Installation
npm install @atomiqlab/react-native-mapbox-navigationThis is a native module. After installing or changing config, rebuild the native app (npx expo prebuild, npx expo run:ios, npx expo run:android, or your normal native build flow).
Required Mapbox Tokens
The config plugin validates tokens during prebuild.
EXPO_PUBLIC_MAPBOX_ACCESS_TOKENA Mapbox public token starting withpk.MAPBOX_DOWNLOADS_TOKENA Mapbox secret token starting withsk.and includingDOWNLOADS:READ
The plugin also accepts these fallbacks:
MAPBOX_PUBLIC_TOKENexpo.extra.mapboxPublicTokenexpo.extra.expoPublicMapboxAccessTokenexpo.extra.mapboxAccessTokenexpo.extra.mapboxDownloadsToken
Example .env:
EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN=pk.your_public_token
MAPBOX_DOWNLOADS_TOKEN=sk.your_secret_tokenExpo Config Plugin
The package ships with an Expo config plugin that:
- injects the Mapbox Maven repository on Android
- writes
mapbox_access_tokeninto Android resources - sets
MBXAccessTokeninInfo.plist - adds required Android location/foreground-service permissions
- adds iOS location usage strings and
location/audiobackground modes
If you manage plugins explicitly, add the package to your app config:
{
"expo": {
"plugins": ["@atomiqlab/react-native-mapbox-navigation"]
}
}Minimal Usage
Request location permission in your app before enabling navigation. The view will emit LOCATION_PERMISSION_REQUIRED if mounted without permission.
import * as Location from "expo-location";
import { useEffect, useState } from "react";
import {
MapboxNavigationView,
type Waypoint,
} from "@atomiqlab/react-native-mapbox-navigation";
const DESTINATION: Waypoint = {
latitude: 37.7847,
longitude: -122.4073,
name: "Union Square",
};
export function EmbeddedNavigation() {
const [granted, setGranted] = useState(false);
const [origin, setOrigin] = useState<Waypoint | undefined>(undefined);
useEffect(() => {
void (async () => {
const permission = await Location.requestForegroundPermissionsAsync();
if (!permission.granted) {
return;
}
const position = await Location.getCurrentPositionAsync({});
setOrigin({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
name: "Current Location",
});
setGranted(true);
})();
}, []);
return (
<MapboxNavigationView
enabled={granted}
style={{ flex: 1 }}
startOrigin={origin}
destination={DESTINATION}
shouldSimulateRoute
/>
);
}Platform Behavior
- Android can start without
startOrigin; it falls back to the device location. - iOS currently requires
startOriginto begin routing. - Only one embedded navigation session should be active at a time.
- The package uses native UI for the main map/navigation chrome and React overlays for custom controls.
Display-Only Navigation Markers
navigationMarkers renders lightweight native pin annotations directly on the embedded navigation map.
These are display-only — they do not affect routing. Use waypoints for intermediate route stops.
<MapboxNavigationView
enabled
style={{ flex: 1 }}
startOrigin={origin}
destination={destination}
navigationMarkers={[
{
id: "pickup-1",
latitude: 37.7858,
longitude: -122.4064,
label: "Pickup – Alice",
glyph: "P",
badge: "2",
variant: "primary",
size: "large",
selected: true,
},
{
id: "dropoff-1",
latitude: 37.7901,
longitude: -122.4019,
label: "Dropoff – Alice",
glyph: "D",
variant: "success",
},
{
id: "custom-stop",
latitude: 37.788,
longitude: -122.408,
label: "Custom Stop",
glyph: "★",
// Fully custom color — overrides `variant`
color: "#7C3AED",
badgeColor: "#5B21B6",
opacity: 0.9,
markerStyle: "dot", // simple circle, no tail
size: "small",
},
]}
/>Marker fields
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| id | string | required | Stable key — used to update/remove markers in place |
| latitude | number | required | WGS84 latitude |
| longitude | number | required | WGS84 longitude |
| label | string | — | Accessibility label and debug description |
| glyph | string | "•" | Short text rendered inside the bubble (max 2 chars) |
| badge | string | — | Badge text in the upper-right corner (max 3 chars) |
| variant | NavigationMarkerVariant | "default" | Semantic color preset |
| color | string | — | Custom fill hex (e.g. "#7C3AED"). Overrides variant |
| badgeColor | string | — | Custom badge hex. Falls back to a darker shade of color/variant |
| opacity | number | auto | Marker opacity 0..1. Overrides the variant/selected default |
| size | NavigationMarkerSize | "medium" | Size preset |
| markerStyle | NavigationMarkerStyle | "pin" | "pin" = bubble + tail, "dot" = circle only |
| showTail | boolean | true | Show the pointer tail (only applies to "pin" style) |
| selected | boolean | auto | Forwarded to the native annotation selected state |
| allowOverlap | boolean | true | Allow overlap with other annotations |
| anchorOffsetY | number | auto | Custom Y-axis pixel offset from the anchor point |
Variant presets
| Variant | Fill color | Badge color | Default opacity |
|---------|-----------|-------------|-----------------|
| "default" | #1F2937 | #111827 | 0.92 (unselected) / 1 |
| "primary" | #2563EB | #1D4ED8 | 1 |
| "success" | #15803D | #166534 | 1 |
| "warning" | #C2410C | #9A3412 | 1 |
| "danger" | #B91C1C | #991B1B | 1 |
| "muted" | #475569 | #334155 | 0.72 |
Use color + badgeColor + opacity together for fully custom branding without touching variant.
Overlay Bottom Sheet
bottomSheet is overlay-only. The package renders a React layer above the native navigation UI.
<MapboxNavigationView
enabled
style={{ flex: 1 }}
startOrigin={origin}
destination={destination}
bottomSheet={{
enabled: true,
mode: "overlay",
initialState: "hidden",
collapsedHeight: 120,
expandedHeight: 320,
collapsedBottomOffset: 24,
showHandle: true,
colorMode: "dark",
builtInQuickActions: ["overview", "recenter", "toggleMute", "stop"],
}}
bottomSheetComponent={YourBottomSheet}
/>Supported bottom-sheet entry points:
bottomSheetContentrenderBottomSheet(context)bottomSheetComponent
BottomSheetRenderContext includes:
statehiddenexpandedshow(state?)hide()expand()collapse()toggle()bannerInstructionrouteProgresslocationstopNavigation()emitAction(actionId)
State behavior:
- Android uses
collapsedandexpanded - iOS maps collapsed behavior to
hidden/expanded
Custom Floating Buttons
Custom floating buttons are independent from the bottom sheet. You can render them with or without bottomSheet.
import {
MapboxNavigationFloatingButton,
MapboxNavigationFloatingButtonsStack,
type FloatingButtonsRenderContext,
} from "@atomiqlab/react-native-mapbox-navigation";
function ActionRail({
stopNavigation,
emitAction,
}: FloatingButtonsRenderContext) {
return (
<MapboxNavigationFloatingButtonsStack>
<MapboxNavigationFloatingButton
accessibilityLabel="Open chat"
onPress={() => emitAction("chat")}
>
CHAT
</MapboxNavigationFloatingButton>
<MapboxNavigationFloatingButton
accessibilityLabel="Stop navigation"
onPress={() => {
void stopNavigation();
}}
>
END
</MapboxNavigationFloatingButton>
</MapboxNavigationFloatingButtonsStack>
);
}
<MapboxNavigationView
enabled
style={{ flex: 1 }}
startOrigin={origin}
destination={destination}
floatingButtonsComponent={ActionRail}
/>Supported floating-button entry points:
floatingButtonsrenderFloatingButtons(context)floatingButtonsComponent
FloatingButtonsRenderContext includes:
show(state?)hide()expand()collapse()toggle()bannerInstructionrouteProgresslocationstopNavigation()emitAction(actionId)
By default, custom floating buttons automatically hide after arrival. Set hideFloatingButtonsOnArrival={false} if you need them to remain visible.
The default package helpers:
MapboxNavigationFloatingButtonMapboxNavigationFloatingButtonsStack
apply the same rounded dark rail styling used by the package examples.
Native Floating Buttons
Use nativeFloatingButtons to control built-in native map buttons without removing your custom React buttons.
<MapboxNavigationView
enabled
style={{ flex: 1 }}
startOrigin={origin}
destination={destination}
nativeFloatingButtons={{
showCameraModeButton: false,
showCompassButton: false,
}}
floatingButtonsComponent={ActionRail}
/>Supported keys:
showOverviewButton(iOS)showAudioGuidanceButton(iOS + Android)showFeedbackButton(iOS)showCameraModeButton(Android)showRecenterButton(Android)showCompassButton(Android action button)
End-of-Route Feedback
The library can show a package-managed rating modal when the trip finishes.
<MapboxNavigationView
enabled
style={{ flex: 1 }}
startOrigin={origin}
destination={destination}
showsEndOfRouteFeedback
onEndOfRouteFeedbackSubmit={({ rating, arrival }) => {
console.log("Trip rating:", rating, arrival?.name);
}}
/>You can also replace the default modal with your own UI:
renderEndOfRouteFeedback(context)endOfRouteFeedbackComponent
EndOfRouteFeedbackRenderContext includes:
arrivaldismiss()submitRating(rating)stopNavigation()
Important:
showsEndOfRouteFeedbackcontrols the package React modal, not a native Mapbox rating flow- a custom end-of-route renderer is automatically treated as enabled unless you explicitly set
showsEndOfRouteFeedback={false}
Runtime Functions
import {
getNavigationSettings,
setDistanceUnit,
setLanguage,
setMuted,
setVoiceVolume,
stopNavigation,
} from "@atomiqlab/react-native-mapbox-navigation";Available functions:
setMuted(muted: boolean): Promise<void>setVoiceVolume(volume: number): Promise<void>setDistanceUnit(unit: "metric" | "imperial"): Promise<void>setLanguage(language: string): Promise<void>getNavigationSettings(): Promise<NavigationSettings>stopNavigation(): Promise<boolean>
getNavigationSettings() currently exposes module-level settings state. Do not treat isNavigating as authoritative session state yet.
Component Callbacks
MapboxNavigationView supports these callbacks:
onLocationChange(location)onRouteProgressChange(progress)onRouteChange(event)onJourneyDataChange(data)onBannerInstruction(instruction)onArrive(event)onCancelNavigation()onError(error)onOverlayBottomSheetActionPress(event)onEndOfRouteFeedbackSubmit(event)onDestinationPreview(event)Android-onlyonDestinationChanged(event)Android-only
Listener Helpers
You can also subscribe with exported listeners:
import {
addArriveListener,
addBannerInstructionListener,
addBottomSheetActionPressListener,
addCancelNavigationListener,
addDestinationChangedListener,
addDestinationPreviewListener,
addErrorListener,
addJourneyDataChangeListener,
addLocationChangeListener,
addRouteChangeListener,
addRouteProgressChangeListener,
} from "@atomiqlab/react-native-mapbox-navigation";Current Limitations
These are important review findings from the current codebase and the docs below reflect them intentionally:
androidActionButtonsis still in the public types for compatibility, but it is effectively ignored in embedded mode today.showsReportFeedbackis not a reliable cross-platform toggle in embedded mode. UsenativeFloatingButtonsfor built-in floating-button visibility instead.- iOS currently requires
startOrigin; Android can fall back to device location. MapboxNavigationViewis embedded-only. There is no full-screen activity/controller API in2.x.
