# Table of Contents - [Introduction - mapcn-react-native](#introduction-mapcn-react-native) - [Installation - mapcn-react-native](#installation-mapcn-react-native) - [CLI - mapcn-react-native](#cli-mapcn-react-native) - [API Reference - mapcn-react-native](#api-reference-mapcn-react-native) - [Commercial Use - mapcn-react-native](#commercial-use-mapcn-react-native) - [Companion App - mapcn-react-native](#companion-app-mapcn-react-native) - [Basic Map - mapcn-react-native](#basic-map-mapcn-react-native) - [Map Controls - mapcn-react-native](#map-controls-mapcn-react-native) - [Markers - mapcn-react-native](#markers-mapcn-react-native) - [Popups - mapcn-react-native](#popups-mapcn-react-native) - [Gotchas - mapcn-react-native](#gotchas-mapcn-react-native) - [Clusters - mapcn-react-native](#clusters-mapcn-react-native) - [Routes - mapcn-react-native](#routes-mapcn-react-native) - [Mapbox Map Source - mapcn-react-native](#mapbox-map-source-mapcn-react-native) - [Advanced Usage - mapcn-react-native](#advanced-usage-mapcn-react-native) - [Default Map Source - mapcn-react-native](#default-map-source-mapcn-react-native) - [MapTiler Map Source - mapcn-react-native](#maptiler-map-source-mapcn-react-native) --- # Introduction - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Introduction ============ Beautiful, accessible map components. **mapcn-react-native** provides beautifully designed, accessible, and customizable map components. Built on [MapLibre React Native v11](https://maplibre.org/maplibre-react-native/) / [Mapbox React Native](https://rnmapbox.github.io/) , styled with [NativeWind/Uniwind](https://www.nativewind.dev/) and designed to work with the same component code for both styling libraries, alongside [React Native Reusables (shadcn/ui for React Native)](https://reactnativereusables.com/) . Why mapcn-react-native? ----------------------- There's no proper copy-paste, easy-to-use map integration for React Native. Most solutions require complex configurations, API keys, or heavy wrapper libraries. mapcn-react-native gives you beautiful maps with a single command. Features -------- ### Zero Config Works out of the box with free map tiles. No API keys required. ### Theme Aware Automatically adapts to light and dark mode. ### Composable Build complex UIs with simple, composable components. ### TypeScript Full type safety with comprehensive TypeScript support. [NextInstallation](https://mapcn-rn.dev/docs/installation) --- # Installation - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Installation ============ How to install and set up mapcn in your React Native project. **Expo Development Client:** MapLibre React Native (and Mabpox) require a [development build](https://docs.expo.dev/develop/development-builds/introduction/) . They will not work with Expo Go. Run a development build ----------------------- Use a development build to run the native map SDK on a simulator or device. npmpnpmbunyarn npx expo run:ios npmpnpmbunyarn npx expo run:android Prerequisites ------------- A React Native project with [Expo](https://expo.dev/) or bare React Native. This component library works best with Expo and requires [NativeWind/Uniwind](https://www.nativewind.dev/) for styling. The component code is the same for both. **Note:** The map uses CARTO basemap tiles by default, which are only free for NON-COMMERCIAL use. For cheaper commercial use, check out the [Mapbox/Maptiler based version of the component](https://mapcn-rn.dev/docs/commercial-use) . Tiles automatically switch between light and dark themes based on your app's color scheme. Install Map Component --------------------- There are three versions of the component: * using maplibre with CARTO Basemaps (enterprise pricing for commercial use, free for non-commercial) * using mapbox (free tier - pay after reaching limit) * using maplibre with maptiler maps (free tier - pay after reaching limit) Install CARTO Basemaps-based map: --------------------------------- npmpnpmbunyarn npx mapcn-rn add Install mapbox-based map: ------------------------- [Additionial setup](https://mapcn-rn.dev/docs/commercial-use) npmpnpmbunyarn npx mapcn-rn add --provider=mapbox Install maptiler map (uses maplibre base): ------------------------------------------ [Additionial setup](https://mapcn-rn.dev/docs/commercial-use) npmpnpmbunyarn npx mapcn-rn add --provider=maptiler This will add the map component to `components/ui/map.tsx` in your project. Check out the [commercial use page](https://mapcn-rn.dev/docs/commercial-use) for more information and additional setup steps. For the full wrapper CLI flow, including provider selection, see the [CLI page](https://mapcn-rn.dev/docs/cli) . Configure Permissions --------------------- Under plugins, you cannot have both maplibre and rnmapbox packages, and it will throw an error if both are inside the app. If you plan to use location features, configure permissions for iOS and Android: #### Expo (app.json) { "expo": { ... "newArchEnabled": true, "ios": { ... "infoPlist": { "ITSAppUsesNonExemptEncryption": false, "NSAppTransportSecurity": { "NSAllowsArbitraryLoads": true }, "NSLocationWhenInUseUsageDescription": "This app needs access to your location to show you on the map.", "NSLocationAlwaysAndWhenInUseUsageDescription": "This app needs access to your location to show you on the map." } }, "android": { ... "permissions": [\ "ACCESS_FINE_LOCATION",\ "ACCESS_COARSE_LOCATION"\ ] }, "plugins": [\ ...\ // this will show a warning for no valid plugin, but it is required\ "@maplibre/maplibre-react-native" // "@rnmapbox/maps" if using mapbox version\ ], ... } } ### Using Bare React Native #### iOS (Info.plist) NSLocationWhenInUseUsageDescription We need your location to show it on the map #### Android (AndroidManifest.xml) Usage ----- Import and use the map component in your screens: import { Map } from "@/components/ui/map"; import { View } from "react-native"; export default function BasicMapExample() { return ( ); } [PreviousIntroduction](https://mapcn-rn.dev/docs) [NextCLI](https://mapcn-rn.dev/docs/cli) --- # CLI - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) CLI === Install mapcn with a single command and choose your provider when you need it. `mapcn-rn` is a thin wrapper around the React Native Reusables CLI. It asks for your map provider, resolves the correct registry URL, and installs the component into `components/ui/map.tsx`. Interactive install ------------------- Run the CLI with no flags if you want to choose the provider interactively. npmpnpmbunyarn npx mapcn-rn add Pick a provider up front ------------------------ If you already know which provider you want, pass it directly and skip the prompt. ### CARTO npmpnpmbunyarn npx mapcn-rn add --provider=carto ### MapTiler npmpnpmbunyarn npx mapcn-rn add --provider=maptiler ### Mapbox npmpnpmbunyarn npx mapcn-rn add --provider=mapbox Provider notes -------------- * `carto` uses the default MapLibre-based component with CARTO basemaps. * `maptiler` keeps the MapLibre setup and switches the basemap provider. * `mapbox` switches the native implementation to `@rnmapbox/maps`. Native setup still matters after install. For permissions and Expo configuration, see the [installation guide](https://mapcn-rn.dev/docs/installation) . For provider-specific keys and commercial guidance, see [commercial use](https://mapcn-rn.dev/docs/commercial-use) . [PreviousInstallation](https://mapcn-rn.dev/docs/installation) [NextCommercial use](https://mapcn-rn.dev/docs/commercial-use) --- # API Reference - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) API Reference ============= Complete reference for all React Native map components and their props. **Note:** This library is built on top of [MapLibre React Native](https://maplibre.org/maplibre-react-native/) . Components use React Native primitives (View, Text, Pressable) and StyleSheet for styling. Refer to the [MapLibre React Native API](https://maplibre.org/maplibre-react-native/docs/setup/getting-started) for additional options. Component Anatomy ----------------- All parts of the component that you can use and combine to build your map. Map --- The root container component that initializes MapLibre React Native and provides context to child components. Automatically handles theme switching between light and dark modes based on the device color scheme. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | `—` | Child components (markers, popups, controls, routes). | | `center` | `[number, number]` | `[0, 0]` | Initial center coordinate \[longitude, latitude\]. | | `zoom` | `number` | `10` | Initial zoom level (0-20). | | `styles` | `{ light?: string \| object; dark?: string \| object }` | `—` | Custom map styles for light and dark themes. Overrides the default CARTO basemap tiles. | | `className` | `string` | `—` | Container style (tailwindcss). | | `showLoader` | `boolean` | `true` | Show loading indicator while map initializes. | useMap ------ A hook that provides access to the MapLibre map and camera refs, plus loading state and theme. Must be used within a `Map` component. const { mapRef, cameraRef, isLoaded, theme } = useMap(); // Access map instance const center = await mapRef.current?.getCenter(); Returns an object with: * `mapRef` - Ref to MapLibre [MapView](https://maplibre.org/maplibre-react-native/docs/components/general/map-view) * `cameraRef` - Ref to MapLibre Camera for programmatic movement * `isLoaded` - Boolean indicating if map is loaded and ready * `theme` - Current theme ("light" or "dark") MapControls ----------- Renders map control buttons (zoom in/out and locate). Must be used inside `Map`. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `position` | `"top-left" \| "top-right" \| "bottom-left" \| "bottom-right"` | `"bottom-right"` | Position of the controls on the map. | | `showZoom` | `boolean` | `true` | Show zoom in/out buttons. | | `showLocate` | `boolean` | `false` | Show locate button to find user's location. Requires location permissions. | | `className` | `string` | `—` | Additional styles for the controls container. (tailwindcss) | | `onLocate` | `(coords: { longitude: number; latitude: number }) => void` | `—` | Callback with user coordinates when located. | MapMarker --------- A container for marker-related components. Provides context for its children and handles marker positioning. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `coordinate` | `[number, number]` | `—` | Marker position as \[longitude, latitude\]. Alternative to using longitude/latitude props separately. | | `longitude` | `number` | `—` | Longitude coordinate. Use with latitude prop, or use coordinate instead. | | `latitude` | `number` | `—` | Latitude coordinate. Use with longitude prop, or use coordinate instead. | | `children` | `ReactNode` | `—` | Marker subcomponents (MarkerContent, MarkerPopup, MarkerLabel). | | `label` | `string` | `—` | Quick shorthand to add a label without using MarkerLabel component. | | `anchor` | `{ x: number; y: number }` | `{ x: 0.5, y: 0.5 }` | Anchor point for the marker (0.0 to 1.0). Default is center. | | `allowOverlap` | `boolean` | `false` | Allow marker to overlap with other markers. | | `onPress` | `() => void` | `—` | Callback when marker is pressed. | MarkerContent ------------- Renders the visual content of a marker. Must be used inside `MapMarker`. If no children provided, renders a default blue dot marker. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | `—` | Custom marker content. Defaults to a blue dot. Can contain any React Native components. | | `className` | `string` | `—` | Additional styles for the marker container. (tailwindcss) | MarkerPopup ----------- Renders a popup (callout) attached to the marker that opens when the marker is pressed. Must be used inside `MapMarker`. Uses MapLibre's native Callout component. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | `—` | Popup content. Can contain any React Native components. | | `title` | `string` | `—` | Optional title text for the callout. | | `className` | `string` | `—` | Additional styles for the popup container. (tailwindcss) | MarkerLabel ----------- Renders a text label above or below the marker. Must be used inside `MarkerContent`. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | `—` | Label text content. | | `className` | `string` | `—` | Additional styles for the label container. (tailwindcss) | | `classNameText` | `string` | `—` | Text styles (tailwindcss) | | `position` | `"top" \| "bottom"` | `"top"` | Position of the label relative to the marker. | MapRoute -------- Renders a polyline on the map connecting coordinate points. Must be used inside `Map`. Drawn using native MapLibre layers for optimal performance. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `coordinates` | `Array<[number, number]>` | `—` | Array of \[longitude, latitude\] coordinate pairs defining the route. | | `color` | `string` | `"#4285F4"` | Line color (CSS color value). | | `width` | `number` | `3` | Line width in pixels. | | `opacity` | `number` | `0.8` | Line opacity (0 to 1). | | `dashArray` | `[number, number]` | `—` | Dash pattern \[dash length, gap length\] for dashed lines. | MapUserLocation --------------- Shows the user's current location on the map with automatic permission handling. Must be used inside `Map`. **Permissions Required:** Make sure you've configured location permissions in your Info.plist (iOS) and AndroidManifest.xml (Android). See the [Installation guide](https://mapcn-rn.dev/docs/installation#configure-permissions) . | Prop | Type | Default | Description | | --- | --- | --- | --- | | `visible` | `boolean` | `true` | Show user location on the map. | | `showAccuracy` | `boolean` | `true` | Show accuracy circle around user location. | | `showHeading` | `boolean` | `false` | Show heading arrow indicating device direction. | | `animated` | `boolean` | `true` | Whether the location marker is animated between updates. | | `minDisplacement` | `number` | `—` | Minimum delta in meters for location updates. Helps save battery. | | `onPress` | `() => void` | `—` | Callback when user location marker is pressed. | | `autoRequestPermission` | `boolean` | `true` | Automatically request location permissions if not granted. | useCurrentPosition ------------------ A hook re-exported from MapLibre React Native that provides the user's current location. import { useCurrentPosition } from "@/components/ui/map"; const position = useCurrentPosition(); // position contains: // - coords: { longitude, latitude, altitude, accuracy, ... } // - timestamp: number Location Permissions -------------------- For manual location permission handling, use `expo-location` directly. This is the recommended approach and is used internally by `MapUserLocation`. import * as Location from "expo-location"; import { useEffect, useState } from "react"; function MyComponent() { const [hasPermission, setHasPermission] = useState(false); useEffect(() => { (async () => { // Request location permissions const { status } = await Location.requestForegroundPermissionsAsync(); setHasPermission(status === "granted"); })(); }, []); // Get current position const getCurrentLocation = async () => { if (hasPermission) { const location = await Location.getCurrentPositionAsync({}); console.log(location.coords.longitude, location.coords.latitude); } }; return ( {hasPermission && } ); } Styling ------- All components accept `className` string props for additional styling using Tailwind CSS classes (via NativeWind/Uniwind). The code is the same for both. This allows easy customization of layout, colors, spacing, and more using utility-first CSS directly in your JSX. [PreviousInstallation](https://mapcn-rn.dev/docs/installation) [NextCompanion App](https://mapcn-rn.dev/docs/companion) --- # Commercial Use - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Commercial use ============== How to setup your project for commercial use. CARTO Basemaps -------------- If you have a commercial license for CARTO basemaps, you can disregard this. Mapbox based component ---------------------- Run the following command to add the mapbox-based map component: npmpnpmbunyarn npx mapcn-rn add --provider=mapbox Get a Mapbox API key from [console.mapbox.com/account/access-token](https://console.mapbox.com/account/access-tokens) . Mapbox pricing (Maps SDK): * Free tier: 25.000 Monthly Active Users * Additional pricing info is available at: [Mapbox Pricing](https://www.mapbox.com/pricing) Add the API key to your environment variables: # .env EXPO_PUBLIC_MAPBOX_API_KEY=pk... This will add the mapbox-based map component to `components/ui/map.tsx` in your project. Maptiler based component ------------------------ Run the following command to add the map component: npmpnpmbunyarn npx mapcn-rn add --provider=maptiler Get a Maptiler API key from the [Maptiler](https://cloud.maptiler.com/account/keys) website. Maptiler pricing: * Free tier: 100,000 requests/month * Additional pricing info is available at: [Maptiler Pricing](https://www.maptiler.com/cloud/pricing) Add the API key to your environment variables: # .env EXPO_PUBLIC_MAPTILER_API_KEY=your_maptiler_api_key_here This will add the maptiler-based map component to `components/ui/map.tsx` in your project. [PreviousCLI](https://mapcn-rn.dev/docs/cli) [NextAPI Reference](https://mapcn-rn.dev/docs/api-reference) --- # Companion App - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Companion App ============= Download the companion app for previewing map components. iOS Download ------------ ![QR code for iOS app](https://mapcn-rn.dev/_next/image?url=https%3A%2F%2Fmapcn-rn.b-cdn.net%2Fqr%2Fapple_app_store.png&w=640&q=75)[Open App Store](https://apps.apple.com/us/app/mapcn/id6757542720) Android Download ---------------- ![QR code for Android app](https://mapcn-rn.dev/_next/image?url=https%3A%2F%2Fmapcn-rn.b-cdn.net%2Fqr%2Fgoogle_play.png&w=640&q=75)[Open Google Play](https://play.google.com/store/apps/details?id=si.aiken.mapcnrn) [PreviousAPI Reference](https://mapcn-rn.dev/docs/api-reference) [NextBasic Map](https://mapcn-rn.dev/docs/basic-map) --- # Basic Map - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Basic Map ========= The simplest way to add a map to your React Native application. The `Map` component handles MapLibre React Native setup, theming, and provides context for child components. Use the `center` and `zoom` props to set the initial map position. PreviewCodeApp Preview ![Map example screenshot](https://mapcn-rn.dev/_next/image?url=https%3A%2F%2Fmapcn-rn.b-cdn.net%2Fscreenshots%2Fbasic-map.png&w=1920&q=75) [PreviousCompanion App](https://mapcn-rn.dev/docs/companion) [NextMap Controls](https://mapcn-rn.dev/docs/controls) --- # Map Controls - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Map Controls ============ Add interactive controls to your map for zoom and user location. The `MapControls` component provides interactive controls that can be positioned on any corner of the map. Available controls include zoom in/out buttons and a locate button to find the user's current location. **Location Permissions:** The locate button requires location permissions. Make sure you've configured permissions in your Info.plist (iOS) and AndroidManifest.xml (Android). See the `MapUserLocation` component for automatic permission handling. PreviewCodeApp Preview ![Map example screenshot](https://mapcn-rn.dev/_next/image?url=https%3A%2F%2Fmapcn-rn.b-cdn.net%2Fscreenshots%2Fmap-controls.png&w=1920&q=75) User Location ------------- Use the `MapUserLocation` component to show the user's current location on the map with automatic permission handling: [PreviousBasic Map](https://mapcn-rn.dev/docs/basic-map) [NextMarkers](https://mapcn-rn.dev/docs/markers) --- # Markers - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Markers ======= Add interactive markers to your map with labels and popups. Use `MapMarker` to place markers on the map. Each marker can have custom content, labels, and popups that open when pressed. Markers accept coordinates in two formats: `coordinate={[lng, lat]}` or separate `longitude` and `latitude` props. **Performance tip:** `MapMarker` uses native MapLibre markers and works well for hundreds of markers. For very large datasets (1000+), see the [GeoJSON layers example](https://mapcn-rn.dev/docs/advanced-usage#example-markers-via-layers) for better performance. Basic Example ------------- Simple markers with labels and popups showing location information. Press a marker to see its popup. PreviewCodeApp Preview ![Map example screenshot](https://mapcn-rn.dev/_next/image?url=https%3A%2F%2Fmapcn-rn.b-cdn.net%2Fscreenshots%2Fmarkers.png&w=1920&q=75) Rich Popups ----------- Build complex popups with React Native components like View, Text, Image, and Pressable for interactive elements. PreviewCodeApp Preview ![Map example screenshot](https://mapcn-rn.dev/_next/image?url=https%3A%2F%2Fmapcn-rn.b-cdn.net%2Fscreenshots%2Fpopup.png&w=1920&q=75) [PreviousMap Controls](https://mapcn-rn.dev/docs/controls) [NextPopups](https://mapcn-rn.dev/docs/popups) --- # Popups - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Popups ====== Display information popups when markers are pressed. Use `MarkerPopup` inside a `MapMarker` to display callouts when the marker is pressed. Popups are implemented using MapLibre's native Callout component and can contain custom React Native components. **React Native Note:** Unlike the web version, standalone `MapPopup` components are not available in React Native. All popups must be attached to markers via `MarkerPopup`. Rich Popups ----------- Build complex popups with React Native components like View, Text, Image, and Pressable for interactive elements. Press a marker to see the popup with detailed information and actions. PreviewCodeApp Preview ![Map example screenshot](https://mapcn-rn.dev/_next/image?url=https%3A%2F%2Fmapcn-rn.b-cdn.net%2Fscreenshots%2Fpopup.png&w=1920&q=75) [PreviousMarkers](https://mapcn-rn.dev/docs/markers) [NextRoutes](https://mapcn-rn.dev/docs/routes) --- # Gotchas - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Gotchas ======= Platform-specific behavior and common pitfalls when using mapcn-react-native. Using Maps Inside a ScrollView (Android) ---------------------------------------- On Android, embedding a `Map` inside a React Native `ScrollView`can cause gesture conflicts. Vertical scroll gestures from the parent ScrollView often "win" over map pan and zoom, so the map flickers while the screen scrolls. **Scope:** This pattern is only needed when a map is rendered inside a `ScrollView`. If your map fills the screen (no parent ScrollView), you do not need any special handling. Solution: ScrollView Gesture Wrapper ------------------------------------ The simplest fix is to temporarily disable the parent ScrollView while the user is interacting with the map area. You can do this with a small wrapper component that notifies the parent when touch events start and end. In the example app, this helper is implemented as `ScrollViewMapWrapper` and used around all maps inside a ScrollView. It is kept local to the example app and is not part of the public library API. Example: ScrollView Wrapper Pattern ----------------------------------- This example shows the core pattern using a dedicated wrapper around the map container. The ScrollView uses a `scrollEnabled` state that is toggled based on touch events on the map area. import { useState } from "react"; import { ScrollView, View, Text } from "react-native"; import { Map } from "@/components/ui/map"; import { ScrollViewMapWrapper } from "@/components/scroll-view-map-wrapper"; export function MapInScrollView() { const [scrollEnabled, setScrollEnabled] = useState(true); return ( Map in ScrollView Scroll the page outside the map, pan and zoom inside the map. ); } Wrapper Implementation (Example App) ------------------------------------ This is the implementation used in the example app. You can copy this into your project and adapt it as needed. It works for both MapLibre and Mapbox versions of the components. import React, { useCallback, ReactNode } from "react"; import { View, ViewStyle } from "react-native"; interface ScrollViewMapWrapperProps { children: ReactNode; onScrollEnabledChange?: (enabled: boolean) => void; style?: ViewStyle; className?: string; } // Helper for using map components inside a ScrollView without gesture conflicts export function ScrollViewMapWrapper({ children, onScrollEnabledChange, style, className, }: ScrollViewMapWrapperProps) { const handleTouchStart = useCallback(() => { onScrollEnabledChange?.(false); }, [onScrollEnabledChange]); const handleTouchEnd = useCallback(() => { onScrollEnabledChange?.(true); }, [onScrollEnabledChange]); return ( {children} ); } Notes and Best Practices ------------------------ * Set a fixed height for the map container when used inside a ScrollView (for example,`h-[400px]` or `h-[500px]`). * Always re-enable scrolling in both `onTouchEnd` and`onTouchCancel` to avoid leaving the ScrollView disabled after an interrupted gesture. * Wrap the immediate parent of the map rather than the entire screen, so only the map area controls the scroll locking behavior. * Test on physical Android devices where gesture behavior is most noticeable. iOS generally behaves better with nested gestures, but using this pattern keeps behavior consistent across platforms. Related Examples ---------------- The example app applies this pattern to all map screens that are rendered inside a ScrollView, including [Routes](https://mapcn-rn.dev/docs/routes) , [Markers](https://mapcn-rn.dev/docs/markers) , and [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) . [PreviousAdvanced Usage](https://mapcn-rn.dev/docs/advanced-usage) --- # Clusters - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Clusters ======== Clustering large datasets on React Native maps. **Wrapper limitation:** The `MapClusterLayer` component from the web version is not exposed by mapcn's wrappers. Clustering is still possible in both the MapLibre and Mapbox variants when you drop down to their raw GeoJSON source and layer APIs. GeoJSON Clustering ------------------ MapLibreMapbox For clustered datasets, provide a point `FeatureCollection` through a `GeoJSONSource`/`ShapeSource`, turn on source clustering, then render clustered and unclustered features with separate native layers. Upstream MapLibre React Native exposes clustering controls like `cluster`, `clusterRadius`, `clusterMinPoints`, `clusterMaxZoomLevel`, and `clusterProperties`, plus helper methods like `getClusterExpansionZoom`, `getClusterLeaves`, and `getClusterChildren` for zooming into or inspecting a cluster after a press event. mapcn-react-native does not wrap that API today, but you can build it directly with the same low-level GeoJSON layering approach shown in the [Advanced Usage GeoJSON layers example](https://mapcn-rn.dev/docs/advanced-usage#example-markers-via-layers) and the upstream [ShapeSource documentation](https://maplibre.org/maplibre-react-native/docs/components/sources/shape-source/) . Possible with GeoJSON datasets * Cluster point features when they are provided through a GeoJSON or shape source. * Style clustered and unclustered output with native layers such as circle and symbol layers. * Aggregate cluster metadata with `clusterProperties`. * Respond to taps with source `onPress` and inspect clusters with the cluster query methods above. * Tap individual unclustered GeoJSON points and read `event.features[0].properties`. * Branch between cluster expansion and point selection in the same source. * Trigger app actions like drawers, bottom sheets, modals, or navigation from tapped point properties. * Render large point datasets more efficiently than many individual `MapMarker` components. * Use GeoJSON for lines and polygons too, as rendered layers rather than clustered markers. Minimal example import { Map, useMap } from "@/components/ui/map"; import { GeoJSONSource, Layer } from "@maplibre/maplibre-react-native"; import { useRef, useState } from "react"; import { Pressable, Text, View } from "react-native"; type PlaceFeature = { type: "Feature"; properties: { id: string; title: string; description: string; }; geometry: { type: "Point"; coordinates: [number, number]; }; }; const places = { type: "FeatureCollection" as const, features: [\ {\ type: "Feature" as const,\ properties: {\ id: "mission",\ title: "Mission District",\ description: "Tap a point to open a local detail sheet.",\ },\ geometry: {\ type: "Point" as const,\ coordinates: [-122.4194, 37.7599] as [number, number],\ },\ },\ {\ type: "Feature" as const,\ properties: {\ id: "downtown",\ title: "Downtown",\ description: "Use feature properties to drive app UI.",\ },\ geometry: {\ type: "Point" as const,\ coordinates: [-122.4058, 37.7897] as [number, number],\ },\ },\ {\ type: "Feature" as const,\ properties: {\ id: "north-beach",\ title: "North Beach",\ description: "Clusters and single-point actions can share one source.",\ },\ geometry: {\ type: "Point" as const,\ coordinates: [-122.4101, 37.8061] as [number, number],\ },\ },\ ], }; type TappableGeoJSONLayerProps = { onSelectPlace: (place: PlaceFeature["properties"]) => void; }; function TappableGeoJSONLayer({ onSelectPlace }: TappableGeoJSONLayerProps) { const { cameraRef } = useMap(); const sourceRef = useRef(null); return ( <> { const feature = event.features?.[0]; if (!feature) return; if (feature.properties?.cluster && cameraRef.current && sourceRef.current) { const expansionZoom = await sourceRef.current.getClusterExpansionZoom(feature); cameraRef.current.flyTo({ center: feature.geometry.coordinates as [number, number], zoom: expansionZoom, duration: 500, }); return; } onSelectPlace(feature.properties as PlaceFeature["properties"]); }} > ); } export default function GeoJSONTapMapLibreExample() { const [selectedPlace, setSelectedPlace] = useState(null); return ( {selectedPlace && ( {selectedPlace.title} {selectedPlace.description} setSelectedPlace(null)} className="mt-4 rounded-lg bg-muted px-3 py-2 items-center" > Close )} ); } Not possible or not provided here * No first-class `MapClusterLayer` abstraction in mapcn's exported API. * No automatic clustering for `MapMarker` components. * No clustering for non-point GeoJSON geometries; clustering applies to point shapes. * No built-in drawer, modal, bottom sheet, or router abstraction for layer-based GeoJSON points. * No `MarkerPopup`, `MarkerLabel`, or arbitrary React Native marker subtree inside layer-based clusters. * No per-feature React component tree or `MapMarker`\-style popup behavior for source-rendered points. [PreviousRoutes](https://mapcn-rn.dev/docs/routes) [NextAdvanced Usage](https://mapcn-rn.dev/docs/advanced-usage) --- # Routes - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Routes ====== Draw polylines and paths connecting coordinates on the map. Use `MapRoute` to draw polylines connecting a series of coordinates. Perfect for showing directions, trails, delivery routes, or any path between points. Routes are drawn using native MapLibre layers for optimal performance. Basic Route ----------- Draw a route with numbered stop markers along the path. PreviewCodeApp Preview ![Map example screenshot](https://mapcn-rn.dev/_next/image?url=https%3A%2F%2Fmapcn-rn.b-cdn.net%2Fscreenshots%2Froute.png&w=1920&q=75) Route Planning -------------- Display multiple route options and let users select between them. This example fetches real driving directions from the [OSRM API](https://project-osrm.org/) . Press on a route or use the buttons to switch. PreviewCodeApp Preview ![Map example screenshot](https://mapcn-rn.dev/_next/image?url=https%3A%2F%2Fmapcn-rn.b-cdn.net%2Fscreenshots%2Fosrm-route.png&w=1920&q=75) [PreviousPopups](https://mapcn-rn.dev/docs/popups) [NextClusters](https://mapcn-rn.dev/docs/clusters) --- # Mapbox Map Source - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Mapbox Map Source ================= Full copyable source for the @rnmapbox/maps-based map component. This variant switches the underlying native implementation to `@rnmapbox/maps`while keeping the public component API close to the default version. Use it when you need Mapbox-specific infrastructure, pricing, or SDK behavior. **Mapbox token required:** This variant reads `EXPO_PUBLIC_MAPBOX_API_KEY`and calls `Mapbox.setAccessToken(...)` during module init. It also swaps the underlying implementation from MapLibre to `@rnmapbox/maps`. Install via CLI --------------- If you just want the generated file in your app, use `mapcn-rn`. It resolves the right registry entry and writes the component to `components/ui/map.tsx` for you. npmpnpmbunyarn npx mapcn-rn add --provider=mapbox Required dependencies --------------------- If you are copying the source manually instead of using the CLI, install the runtime packages first. npmpnpmbunyarn npx expo install @rnmapbox/maps expo-location Environment variables --------------------- Add this to your `.env` or Expo environment configuration. EXPO_PUBLIC_MAPBOX_API_KEY=your_mapbox_token Expo config ----------- These components require a development build, not Expo Go. Make sure your Expo config includes location permissions and the correct native plugin. { "expo": { "newArchEnabled": true, "ios": { "infoPlist": { "ITSAppUsesNonExemptEncryption": false, "NSAppTransportSecurity": { "NSAllowsArbitraryLoads": true }, "NSLocationWhenInUseUsageDescription": "This app needs access to your location to show you on the map.", "NSLocationAlwaysAndWhenInUseUsageDescription": "This app needs access to your location to show you on the map." } }, "android": { "permissions": [\ "ACCESS_FINE_LOCATION",\ "ACCESS_COARSE_LOCATION"\ ] }, "plugins": [\ "@rnmapbox/maps"\ ] } } For more context on native setup, see the [installation guide](https://mapcn-rn.dev/docs/installation) . Full component source --------------------- Copy this file into `components/ui/map.tsx`. import { useTheme } from "@/lib/theme-context"; import { cn } from "@/lib/utils"; import Mapbox, { type Camera as MapboxCameraRef, type MapView as MapboxMapViewRef, } from "@rnmapbox/maps"; import * as Location from "expo-location"; import React, { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, type ReactNode, } from "react"; import { ActivityIndicator, Pressable, Text, View } from "react-native"; const MAPBOX_ACCESS_TOKEN = process.env.EXPO_PUBLIC_MAPBOX_API_KEY; if (MAPBOX_ACCESS_TOKEN) { Mapbox.setAccessToken(MAPBOX_ACCESS_TOKEN); } else { console.warn( "[map-mapbox] EXPO_PUBLIC_MAPBOX_API_KEY not found. " + "Get your free Mapbox token at: https://account.mapbox.com/access-tokens/", ); } // Camera wrapper to provide MapLibre-compatible API type CameraAPI = { flyTo: (params: { center: [number, number]; zoom: number; duration: number; }) => void; easeTo: (params: { center: [number, number]; zoom: number; duration: number; }) => void; }; type CameraWrapperRef = { current: CameraAPI | null; }; function createCameraWrapper( internalCameraRef: React.RefObject, ): CameraWrapperRef { return { current: { flyTo: ({ center, zoom, duration }) => { internalCameraRef.current?.setCamera({ centerCoordinate: center, zoomLevel: zoom, animationDuration: duration, animationMode: "flyTo", }); }, easeTo: ({ center, zoom, duration }) => { internalCameraRef.current?.setCamera({ centerCoordinate: center, zoomLevel: zoom, animationDuration: duration, animationMode: "easeTo", }); }, }, }; } type MapContextValue = { mapRef: React.RefObject; cameraRef: CameraWrapperRef; isLoaded: boolean; theme: "light" | "dark"; registerOverlay: (id: string, element: ReactNode) => void; unregisterOverlay: (id: string) => void; }; const MapContext = createContext(null); function useMap() { const context = useContext(MapContext); if (!context) { throw new Error("useMap must be used within a Map component"); } return context; } const defaultStyles = { dark: Mapbox.StyleURL.Dark, light: Mapbox.StyleURL.Street, }; type MapStyleOption = string | object; type MapProps = { children?: ReactNode; /** Custom map styles for light and dark themes. Overrides the default Mapbox styles. */ styles?: { light?: MapStyleOption; dark?: MapStyleOption; }; /** Initial center coordinate [longitude, latitude] */ center?: [number, number]; /** Initial zoom level */ zoom?: number; /** Container style */ className?: string; /** Show loading indicator */ showLoader?: boolean; }; const DefaultLoader = () => ( ); function Map({ children, styles, center = [0, 0], zoom = 10, className, showLoader = true, }: MapProps) { const mapRef = useRef(null); const internalCameraRef = useRef(null); const [isLoaded, setIsLoaded] = useState(false); const [overlays, setOverlays] = useState>({}); const { colorScheme } = useTheme(); const theme = colorScheme === "dark" ? "dark" : "light"; // Create camera wrapper with MapLibre-compatible API const cameraRef = useMemo(() => createCameraWrapper(internalCameraRef), []); const registerOverlay = useCallback((id: string, element: ReactNode) => { setOverlays((prev) => ({ ...prev, [id]: element })); }, []); const unregisterOverlay = useCallback((id: string) => { setOverlays((prev) => { const next = { ...prev }; delete next[id]; return next; }); }, []); const mapStyle = theme === "dark" ? (styles?.dark ?? defaultStyles.dark) : (styles?.light ?? defaultStyles.light); const handleMapIdle = () => { if (!isLoaded) { setIsLoaded(true); } }; return ( {children} {showLoader && !isLoaded && } {Object.entries(overlays).map(([id, element]) => ( {element} ))} ); } type MarkerContextValue = { coordinate: [number, number]; }; const MarkerContext = createContext(null); type MapMarkerProps = { children?: ReactNode; label?: string; /** Anchor point for the marker (0.0 to 1.0). Default is center (0.5, 0.5) */ anchor?: { x: number; y: number }; /** Allow marker to overlap with other markers */ allowOverlap?: boolean; /** Callback when marker is pressed */ onPress?: () => void; } & ( | { coordinate: [number, number]; longitude?: never; latitude?: never } | { longitude: number; latitude: number; coordinate?: never } ); function MapMarker({ children, label, anchor = { x: 0.5, y: 0.5 }, allowOverlap = false, onPress, ...positionProps }: MapMarkerProps) { const id = useId(); const coordinate: [number, number] = "coordinate" in positionProps && positionProps.coordinate ? positionProps.coordinate : [positionProps.longitude, positionProps.latitude]; return ( {children || } {label && {label}} ); } type MarkerContentProps = { children?: ReactNode; className?: string; }; function MarkerContent({ children, className }: MarkerContentProps) { return ( {children || } ); } function DefaultMarkerIcon() { return ( ); } type MarkerPopupProps = { children: ReactNode; className?: string; /** Title text for the callout */ title?: string; }; function MarkerPopup({ children, className, title }: MarkerPopupProps) { return ( {children} ); } type MarkerLabelProps = { children: ReactNode; className?: string; classNameText?: string; position?: "top" | "bottom"; }; function MarkerLabel({ children, className, classNameText, position = "top", }: MarkerLabelProps) { return ( {children} ); } type MapControlsProps = { position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; showZoom?: boolean; showLocate?: boolean; className?: string; onLocate?: (coords: { longitude: number; latitude: number }) => void; }; function MapControls({ position = "bottom-right", showZoom = true, showLocate = false, className, onLocate, }: MapControlsProps) { const { cameraRef, mapRef, isLoaded, registerOverlay, unregisterOverlay } = useMap(); const [waitingForLocation, setWaitingForLocation] = useState(false); const [currentZoom, setCurrentZoom] = useState(10); const overlayId = useId(); const handleZoomIn = useCallback(async () => { if (cameraRef.current && mapRef.current) { const center = await mapRef.current.getCenter(); const newZoom = Math.min(currentZoom + 1, 20); setCurrentZoom(newZoom); cameraRef.current.easeTo({ center: center as [number, number], zoom: newZoom, duration: 300, }); } }, [cameraRef, mapRef, currentZoom]); const handleZoomOut = useCallback(async () => { if (cameraRef.current && mapRef.current) { const center = await mapRef.current.getCenter(); const newZoom = Math.max(currentZoom - 1, 0); setCurrentZoom(newZoom); cameraRef.current.easeTo({ center: center as [number, number], zoom: newZoom, duration: 300, }); } }, [cameraRef, mapRef, currentZoom]); const handleLocate = useCallback(async () => { setWaitingForLocation(true); try { const location = await Location.getCurrentPositionAsync({}); const coords = { longitude: location.coords.longitude, latitude: location.coords.latitude, }; if (cameraRef.current) { cameraRef.current.flyTo({ center: [coords.longitude, coords.latitude], zoom: 14, duration: 1500, }); } if (onLocate) { onLocate(coords); } } catch (error) { console.error("Error getting location:", error); } finally { setWaitingForLocation(false); } }, [cameraRef, onLocate]); const positionStyle = useMemo( () => ({ "top-left": { top: 8, left: 8, zIndex: 1000 }, "top-right": { top: 8, right: 8, zIndex: 1000 }, "bottom-left": { bottom: 8, left: 8, zIndex: 1000 }, "bottom-right": { bottom: 8, right: 8, zIndex: 1000 }, })[position], [position] ); const controlsElement = useMemo( () => ( {showZoom && ( + )} {showLocate && ( {waitingForLocation ? ( ) : ( 📍 )} )} ), [\ className,\ positionStyle,\ showZoom,\ showLocate,\ handleZoomIn,\ handleZoomOut,\ handleLocate,\ waitingForLocation,\ ] ); useEffect(() => { if (isLoaded) { registerOverlay(overlayId, controlsElement); } return () => { unregisterOverlay(overlayId); }; }, [isLoaded, overlayId, registerOverlay, unregisterOverlay, controlsElement]); // Return null because the actual rendering happens via the overlay portal return null; } function ControlButton({ onPress, label, children, disabled = false, }: { onPress: () => void; label: string; children: React.ReactNode; disabled?: boolean; }) { return ( {children} ); } type MapRouteProps = { coordinates: Array<[number, number]>; color?: string; width?: number; opacity?: number; dashArray?: [number, number]; }; function MapRoute({ coordinates, color = "#4285F4", width = 3, opacity = 0.8, dashArray, }: MapRouteProps) { const id = useId(); const sourceId = `route-source-${id}`; const layerId = `route-layer-${id}`; if (coordinates.length < 2) { return null; } const shape = { type: "Feature" as const, properties: {}, geometry: { type: "LineString" as const, coordinates, }, }; return ( ); } type MapUserLocationProps = { /** Show user location on the map */ visible?: boolean; /** Show accuracy circle around user location */ showAccuracy?: boolean; /** Show heading arrow indicating device direction */ showHeading?: boolean; /** Whether the location marker is animated between updates */ animated?: boolean; /** Minimum delta in meters for location updates */ minDisplacement?: number; /** Callback when user location is pressed */ onPress?: () => void; /** Auto-request location permissions if not granted */ autoRequestPermission?: boolean; }; function MapUserLocation({ visible = true, showAccuracy = true, showHeading = false, animated = true, minDisplacement, onPress, autoRequestPermission = true, }: MapUserLocationProps) { const [hasPermission, setHasPermission] = useState(false); const [permissionChecked, setPermissionChecked] = useState(false); useEffect(() => { let mounted = true; const checkAndRequestPermissions = async () => { try { if (autoRequestPermission) { const { status } = await Location.requestForegroundPermissionsAsync(); if (mounted) { setHasPermission(status === "granted"); setPermissionChecked(true); } } else { if (mounted) { setPermissionChecked(true); } } } catch (error) { console.error("Error requesting location permissions:", error); if (mounted) { setHasPermission(false); setPermissionChecked(true); } } }; if (visible) { checkAndRequestPermissions(); } return () => { mounted = false; }; }, [visible, autoRequestPermission]); if (!visible || !permissionChecked || !hasPermission) { return null; } return ( ); } export { Map, MapControls, MapMarker, MapRoute, MapUserLocation, MarkerContent, MarkerLabel, MarkerPopup, useMap }; [PreviousMapTiler Map Source](https://mapcn-rn.dev/docs/source-maptiler) --- # Advanced Usage - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Advanced Usage ============== Access the underlying MapLibre GL instance for advanced customization. Access the underlying MapLibre map instance to use any feature from the MapLibre React Native API. **Tip:** Check the [MapLibre React Native documentation](https://maplibre.org/maplibre-react-native/) for the full list of available methods and events. Using the Hook -------------- For child components rendered inside `Map`, use the `useMap` hook to access the map instance and listen to events. import { Map, useMap } from "@/components/ui/map"; import { View } from "react-native"; import { useEffect } from "react"; // For child components inside Map, use the useMap hook function MapContent() { const { mapRef, cameraRef, isLoaded } = useMap(); useEffect(() => { if (!mapRef.current || !isLoaded) return; const handleRegionChange = () => { mapRef.current?.getCenter().then((center) => { console.log("Map center:", center); }); }; // Access MapLibre methods via mapRef handleRegionChange(); }, [mapRef, isLoaded]); return null; } // Usage Example: Dynamic Layer Toggling ------------------------------- This example shows how to toggle map layers dynamically with React state. Control which routes and markers are visible on the map. PreviewCodeApp Preview ![Map example screenshot](https://mapcn-rn.dev/_next/image?url=https%3A%2F%2Fmapcn-rn.b-cdn.net%2Fscreenshots%2Fadvanced-usage.png&w=1920&q=75) Example: Custom GeoJSON Layer ----------------------------- Add custom GeoJSON data as layers with fill and outline styles using MapLibre's native layer system. PreviewCodeApp Preview ![Map example screenshot](https://mapcn-rn.dev/_next/image?url=https%3A%2F%2Fmapcn-rn.b-cdn.net%2Fscreenshots%2Fcustom-layer.png&w=1920&q=75) Example: Markers via Layers --------------------------- When displaying hundreds or thousands of markers, use GeoJSON layers with `CircleLayer` and `SymbolLayer` instead of `MapMarker` components for better performance. PreviewCodeApp Preview ![Map example screenshot](https://mapcn-rn.dev/_next/image?url=https%3A%2F%2Fmapcn-rn.b-cdn.net%2Fscreenshots%2Flayer-markers.png&w=1920&q=75) Extend to Build --------------- You can extend this to build custom features like: * **Real-time tracking** - Live location updates for delivery, rides, or fleet management * **Geofencing** - Trigger actions when users enter or leave specific areas * **Heatmaps** - Visualize density data like population, crime, or activity hotspots * **Drawing tools** - Let users draw polygons, lines, or place markers for custom areas * **3D buildings** - Extrude building footprints for urban visualization * **Animations** - Animate markers along routes or create fly-through experiences * **Custom data layers** - Overlay weather, traffic, or satellite imagery [PreviousClusters](https://mapcn-rn.dev/docs/clusters) --- # Default Map Source - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) Default Map Source ================== Full copyable source for the default MapLibre + CARTO map component. This is the direct source for the default `components/ui/map.tsx`file. Use it if you want to audit the implementation, copy it manually, or fork it into your own component system. **Default basemap:** This variant uses MapLibre React Native with CARTO basemaps. It is easy to drop in, but CARTO basemap licensing may not fit every commercial deployment. If you need a provider-backed commercial setup, use the MapTiler (uses maplibre base) or Mapbox variants instead. Install via CLI --------------- If you just want the generated file in your app, use `mapcn-rn`. It resolves the right registry entry and writes the component to `components/ui/map.tsx` for you. npmpnpmbunyarn npx mapcn-rn add Required dependencies --------------------- If you are copying the source manually instead of using the CLI, install the runtime packages first. npmpnpmbunyarn npx expo install @maplibre/maplibre-react-native expo-location Expo config ----------- These components require a development build, not Expo Go. Make sure your Expo config includes location permissions and the correct native plugin. { "expo": { "newArchEnabled": true, "ios": { "infoPlist": { "ITSAppUsesNonExemptEncryption": false, "NSAppTransportSecurity": { "NSAllowsArbitraryLoads": true }, "NSLocationWhenInUseUsageDescription": "This app needs access to your location to show you on the map.", "NSLocationAlwaysAndWhenInUseUsageDescription": "This app needs access to your location to show you on the map." } }, "android": { "permissions": [\ "ACCESS_FINE_LOCATION",\ "ACCESS_COARSE_LOCATION"\ ] }, "plugins": [\ "@maplibre/maplibre-react-native"\ ] } } For more context on native setup, see the [installation guide](https://mapcn-rn.dev/docs/installation) . Full component source --------------------- Copy this file into `components/ui/map.tsx`. import { useTheme } from "@/lib/theme-context"; import { cn } from "@/lib/utils"; import { Callout, Camera, GeoJSONSource, Layer, LocationManager, Map as MapLibreMap, Marker, UserLocation, useCurrentPosition, type CameraRef, type MapRef, type StyleSpecification, } from "@maplibre/maplibre-react-native"; import { createContext, use, useEffect, useId, useRef, useState, type ReactNode, } from "react"; import { ActivityIndicator, Pressable, Text, View } from "react-native"; type MapContextValue = { mapRef: React.RefObject; cameraRef: React.RefObject; isLoaded: boolean; theme: "light" | "dark"; }; const MapContext = createContext(null); function useMap() { const context = use(MapContext); if (!context) { throw new Error("useMap must be used within a Map component"); } return context; } const defaultStyles = { dark: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json", light: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json", }; type MapStyleOption = string | StyleSpecification; type MapProps = { children?: ReactNode; /** Custom map styles for light and dark themes. Overrides the default Carto styles. */ styles?: { light?: MapStyleOption; dark?: MapStyleOption; }; /** Initial center coordinate [longitude, latitude] */ center?: [number, number]; /** Initial zoom level */ zoom?: number; /** Container style */ className?: string; /** Show loading indicator */ showLoader?: boolean; }; const DefaultLoader = () => ( ); function Map({ children, styles, center = [0, 0], zoom = 10, className, showLoader = true, }: MapProps) { const mapRef = useRef(null); const cameraRef = useRef(null); const [isLoaded, setIsLoaded] = useState(false); const { colorScheme } = useTheme(); const theme = colorScheme === "dark" ? "dark" : "light"; const mapStyle = theme === "dark" ? styles?.dark ?? defaultStyles.dark : styles?.light ?? defaultStyles.light; const handleMapIdle = () => { if (!isLoaded) { setIsLoaded(true); } }; return ( {children} {showLoader && !isLoaded && } ); } function anchorObjectToAnchorString(anchor: { x: number; y: number }) { const horizontal = anchor.x <= 0.25 ? "left" : anchor.x >= 0.75 ? "right" : "center"; const vertical = anchor.y <= 0.25 ? "top" : anchor.y >= 0.75 ? "bottom" : "center"; if (horizontal === "center" && vertical === "center") return "center"; if (horizontal === "center") return vertical; if (vertical === "center") return horizontal; return `${vertical}-${horizontal}` as "top-left" | "top-right" | "bottom-left" | "bottom-right"; } type MarkerContextValue = { coordinate: [number, number]; }; const MarkerContext = createContext(null); type MapMarkerProps = { children?: ReactNode; label?: string; /** Anchor point for the marker (0.0 to 1.0). Default is center (0.5, 0.5) */ anchor?: { x: number; y: number }; /** Allow marker to overlap with other markers */ allowOverlap?: boolean; /** Callback when marker is pressed */ onPress?: () => void; } & ( | { coordinate: [number, number]; longitude?: never; latitude?: never } | { longitude: number; latitude: number; coordinate?: never } ); function MapMarker({ children, label, anchor = { x: 0.5, y: 0.5 }, allowOverlap: _allowOverlap = false, onPress, ...positionProps }: MapMarkerProps) { const id = useId(); const coordinate: [number, number] = 'coordinate' in positionProps && positionProps.coordinate ? positionProps.coordinate : [positionProps.longitude, positionProps.latitude]; return ( {children || } {label && {label}} ); } type MarkerContentProps = { children?: ReactNode; className?: string; }; function MarkerContent({ children, className }: MarkerContentProps) { return ( {children || } ); } function DefaultMarkerIcon() { return ; } type MarkerPopupProps = { children: ReactNode; className?: string; /** Title text for the callout */ title?: string; }; function MarkerPopup({ children, className, title }: MarkerPopupProps) { return ( {children} ); } type MarkerLabelProps = { children: ReactNode; className?: string; classNameText?: string; position?: "top" | "bottom"; }; function MarkerLabel({ children, className, classNameText, position = "top", }: MarkerLabelProps) { return ( {children} ); } type MapControlsProps = { position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; showZoom?: boolean; showLocate?: boolean; className?: string; onLocate?: (coords: { longitude: number; latitude: number }) => void; }; function MapControls({ position = "bottom-right", showZoom = true, showLocate = false, className, onLocate, }: MapControlsProps) { const { cameraRef, mapRef, isLoaded } = useMap(); const [waitingForLocation, setWaitingForLocation] = useState(false); const [currentZoom, setCurrentZoom] = useState(10); const handleZoomIn = async () => { if (cameraRef.current && mapRef.current) { const center = await mapRef.current.getCenter(); const newZoom = Math.min(currentZoom + 1, 20); setCurrentZoom(newZoom); cameraRef.current.easeTo({ center: center, // LngLat is already [longitude, latitude] zoom: newZoom, duration: 300, }); } }; const handleZoomOut = async () => { if (cameraRef.current && mapRef.current) { const center = await mapRef.current.getCenter(); const newZoom = Math.max(currentZoom - 1, 0); setCurrentZoom(newZoom); cameraRef.current.easeTo({ center: center, // LngLat is already [longitude, latitude] zoom: newZoom, duration: 300, }); } }; const handleLocate = async () => { setWaitingForLocation(true); try { // Location handling would need native permissions setup // This is a simplified version if (cameraRef.current && onLocate) { // You would get actual location here const coords = { longitude: 0, latitude: 0 }; cameraRef.current.flyTo({ center: [coords.longitude, coords.latitude], zoom: 14, duration: 1500, }); onLocate(coords); } } catch (error) { console.error("Error getting location:", error); } finally { setWaitingForLocation(false); } }; if (!isLoaded) return null; const positionStyle = { "top-left": { top: 8, left: 8 }, "top-right": { top: 8, right: 8 }, "bottom-left": { bottom: 8, left: 8 }, "bottom-right": { bottom: 8, right: 8 }, }[position]; return ( {showZoom && ( + )} {showLocate && ( {waitingForLocation ? ( ) : ( 📍 )} )} ); } function ControlButton({ onPress, label, children, disabled = false, }: { onPress: () => void; label: string; children: ReactNode; disabled?: boolean; }) { return ( {children} ); } type MapRouteProps = { coordinates: Array<[number, number]>; color?: string; width?: number; opacity?: number; dashArray?: [number, number]; }; function MapRoute({ coordinates, color = "#4285F4", width = 3, opacity = 0.8, dashArray, }: MapRouteProps) { const id = useId(); const sourceId = `route-source-${id}`; const layerId = `route-layer-${id}`; if (coordinates.length < 2) { return null; } const shape = { type: "Feature" as const, properties: {}, geometry: { type: "LineString" as const, coordinates, }, }; return ( ); } type MapUserLocationProps = { /** Show user location on the map */ visible?: boolean; /** Show accuracy circle around user location */ showAccuracy?: boolean; /** Show heading arrow indicating device direction */ showHeading?: boolean; /** Whether the location marker is animated between updates */ animated?: boolean; /** Minimum delta in meters for location updates */ minDisplacement?: number; /** Callback when user location is pressed */ onPress?: () => void; /** Auto-request location permissions if not granted */ autoRequestPermission?: boolean; }; function MapUserLocation({ visible = true, showAccuracy = true, showHeading = false, animated = true, minDisplacement, onPress, autoRequestPermission = true, }: MapUserLocationProps) { const [hasPermission, setHasPermission] = useState(false); const [permissionChecked, setPermissionChecked] = useState(false); useEffect(() => { let mounted = true; const checkAndRequestPermissions = async () => { try { if (autoRequestPermission) { const granted = await LocationManager.requestPermissions(); if (mounted) { setHasPermission(granted); setPermissionChecked(true); } } else { if (mounted) { setPermissionChecked(true); } } } catch (error) { console.error("Error requesting location permissions:", error); if (mounted) { setHasPermission(false); setPermissionChecked(true); } } }; if (visible) { checkAndRequestPermissions(); } return () => { mounted = false; }; }, [visible, autoRequestPermission]); if (!visible || !permissionChecked || !hasPermission) { return null; } return ( ); } // Re-export LocationManager for permission handling export { LocationManager }; export { Map, MapControls, MapMarker, MapRoute, MapUserLocation, MarkerContent, MarkerLabel, MarkerPopup, useCurrentPosition, useMap }; [PreviousGotchas](https://mapcn-rn.dev/docs/gotchas) [NextMapTiler Map Source](https://mapcn-rn.dev/docs/source-maptiler) --- # MapTiler Map Source - mapcn-react-native Basics * [Getting Started](https://mapcn-rn.dev/docs) * [Installation](https://mapcn-rn.dev/docs/installation) * [CLI](https://mapcn-rn.dev/docs/cli) * [Commercial use](https://mapcn-rn.dev/docs/commercial-use) * [API Reference](https://mapcn-rn.dev/docs/api-reference) * [Companion App](https://mapcn-rn.dev/docs/companion) Examples * [Basic Map](https://mapcn-rn.dev/docs/basic-map) * [Map Controls](https://mapcn-rn.dev/docs/controls) * [Markers](https://mapcn-rn.dev/docs/markers) * [Popups](https://mapcn-rn.dev/docs/popups) * [Routes](https://mapcn-rn.dev/docs/routes) * [Clusters](https://mapcn-rn.dev/docs/clusters) * [Advanced Usage](https://mapcn-rn.dev/docs/advanced-usage) * [Gotchas](https://mapcn-rn.dev/docs/gotchas) Direct Source * [Default Map](https://mapcn-rn.dev/docs/source-default) * [MapTiler Map](https://mapcn-rn.dev/docs/source-maptiler) * [Mapbox Map](https://mapcn-rn.dev/docs/source-mapbox) MapTiler Map Source =================== Full copyable source for the MapLibre + MapTiler map component. This variant keeps the MapLibre-based API but swaps the basemap provider to MapTiler. It is the version to use when you want a provider-backed commercial path without moving to the Mapbox SDK. **MapTiler key required:** This variant reads `EXPO_PUBLIC_MAPTILER_API_KEY`at runtime. Without it, the component deliberately renders no provider style and warns in the console. Install via CLI --------------- If you just want the generated file in your app, use `mapcn-rn`. It resolves the right registry entry and writes the component to `components/ui/map.tsx` for you. npmpnpmbunyarn npx mapcn-rn add --provider=maptiler Required dependencies --------------------- If you are copying the source manually instead of using the CLI, install the runtime packages first. npmpnpmbunyarn npx expo install @maplibre/maplibre-react-native expo-location Environment variables --------------------- Add this to your `.env` or Expo environment configuration. EXPO_PUBLIC_MAPTILER_API_KEY=your_maptiler_key Expo config ----------- These components require a development build, not Expo Go. Make sure your Expo config includes location permissions and the correct native plugin. { "expo": { "newArchEnabled": true, "ios": { "infoPlist": { "ITSAppUsesNonExemptEncryption": false, "NSAppTransportSecurity": { "NSAllowsArbitraryLoads": true }, "NSLocationWhenInUseUsageDescription": "This app needs access to your location to show you on the map.", "NSLocationAlwaysAndWhenInUseUsageDescription": "This app needs access to your location to show you on the map." } }, "android": { "permissions": [\ "ACCESS_FINE_LOCATION",\ "ACCESS_COARSE_LOCATION"\ ] }, "plugins": [\ "@maplibre/maplibre-react-native"\ ] } } For more context on native setup, see the [installation guide](https://mapcn-rn.dev/docs/installation) . Full component source --------------------- Copy this file into `components/ui/map.tsx`. import { useTheme } from "@/lib/theme-context"; import { cn } from "@/lib/utils"; import { Callout, Camera, GeoJSONSource, Layer, LocationManager, Map as MapLibreMap, Marker, UserLocation, useCurrentPosition, type CameraRef, type MapRef, type StyleSpecification, } from "@maplibre/maplibre-react-native"; import { createContext, use, useEffect, useId, useRef, useState, type ReactNode, } from "react"; import { ActivityIndicator, Pressable, Text, View } from "react-native"; type MapContextValue = { mapRef: React.RefObject; cameraRef: React.RefObject; isLoaded: boolean; theme: "light" | "dark"; }; const MapContext = createContext(null); function useMap() { const context = use(MapContext); if (!context) { throw new Error("useMap must be used within a Map component"); } return context; } const defaultStyles = { dark: getMapStyle("dark"), light: getMapStyle("light"), }; type MapStyleOption = string | StyleSpecification; type MapProps = { children?: ReactNode; /** Custom map styles for light and dark themes. Overrides the default Carto styles. */ styles?: { light?: MapStyleOption; dark?: MapStyleOption; }; /** Initial center coordinate [longitude, latitude] */ center?: [number, number]; /** Initial zoom level */ zoom?: number; /** Container style */ className?: string; /** Show loading indicator */ showLoader?: boolean; }; const DefaultLoader = () => ( ); function Map({ children, styles, center = [0, 0], zoom = 10, className, showLoader = true, }: MapProps) { const mapRef = useRef(null); const cameraRef = useRef(null); const [isLoaded, setIsLoaded] = useState(false); const { colorScheme } = useTheme(); const theme = colorScheme === "dark" ? "dark" : "light"; const mapStyle = theme === "dark" ? (styles?.dark ?? defaultStyles.dark) : (styles?.light ?? defaultStyles.light); const handleMapIdle = () => { if (!isLoaded) { setIsLoaded(true); } }; if (!mapStyle) { return ( No MapTiler API Key ); } return ( {children} {showLoader && !isLoaded && } ); } function anchorObjectToAnchorString(anchor: { x: number; y: number }) { const horizontal = anchor.x <= 0.25 ? "left" : anchor.x >= 0.75 ? "right" : "center"; const vertical = anchor.y <= 0.25 ? "top" : anchor.y >= 0.75 ? "bottom" : "center"; if (horizontal === "center" && vertical === "center") return "center"; if (horizontal === "center") return vertical; if (vertical === "center") return horizontal; return `${vertical}-${horizontal}` as | "top-left" | "top-right" | "bottom-left" | "bottom-right"; } type MarkerContextValue = { coordinate: [number, number]; }; const MarkerContext = createContext(null); type MapMarkerProps = { children?: ReactNode; label?: string; /** Anchor point for the marker (0.0 to 1.0). Default is center (0.5, 0.5) */ anchor?: { x: number; y: number }; /** Allow marker to overlap with other markers */ allowOverlap?: boolean; /** Callback when marker is pressed */ onPress?: () => void; } & ( | { coordinate: [number, number]; longitude?: never; latitude?: never } | { longitude: number; latitude: number; coordinate?: never } ); function MapMarker({ children, label, anchor = { x: 0.5, y: 0.5 }, allowOverlap: _allowOverlap = false, onPress, ...positionProps }: MapMarkerProps) { const id = useId(); const coordinate: [number, number] = "coordinate" in positionProps && positionProps.coordinate ? positionProps.coordinate : [positionProps.longitude, positionProps.latitude]; return ( {children || } {label && {label}} ); } type MarkerContentProps = { children?: ReactNode; className?: string; }; function MarkerContent({ children, className }: MarkerContentProps) { return ( {children || } ); } function DefaultMarkerIcon() { return ( ); } type MarkerPopupProps = { children: ReactNode; className?: string; /** Title text for the callout */ title?: string; }; function MarkerPopup({ children, className, title }: MarkerPopupProps) { return ( {children} ); } type MarkerLabelProps = { children: ReactNode; className?: string; classNameText?: string; position?: "top" | "bottom"; }; function MarkerLabel({ children, className, classNameText, position = "top", }: MarkerLabelProps) { return ( {children} ); } type MapControlsProps = { position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; showZoom?: boolean; showLocate?: boolean; className?: string; onLocate?: (coords: { longitude: number; latitude: number }) => void; }; function MapControls({ position = "bottom-right", showZoom = true, showLocate = false, className, onLocate, }: MapControlsProps) { const { cameraRef, mapRef, isLoaded } = useMap(); const [waitingForLocation, setWaitingForLocation] = useState(false); const [currentZoom, setCurrentZoom] = useState(10); const handleZoomIn = async () => { if (cameraRef.current && mapRef.current) { const center = await mapRef.current.getCenter(); const newZoom = Math.min(currentZoom + 1, 20); setCurrentZoom(newZoom); cameraRef.current.easeTo({ center: center, // LngLat is already [longitude, latitude] zoom: newZoom, duration: 300, }); } }; const handleZoomOut = async () => { if (cameraRef.current && mapRef.current) { const center = await mapRef.current.getCenter(); const newZoom = Math.max(currentZoom - 1, 0); setCurrentZoom(newZoom); cameraRef.current.easeTo({ center: center, // LngLat is already [longitude, latitude] zoom: newZoom, duration: 300, }); } }; const handleLocate = async () => { setWaitingForLocation(true); try { // Location handling would need native permissions setup // This is a simplified version if (cameraRef.current && onLocate) { // You would get actual location here const coords = { longitude: 0, latitude: 0 }; cameraRef.current.flyTo({ center: [coords.longitude, coords.latitude], zoom: 14, duration: 1500, }); onLocate(coords); } } catch (error) { console.error("Error getting location:", error); } finally { setWaitingForLocation(false); } }; if (!isLoaded) return null; const positionStyle = { "top-left": { top: 8, left: 8 }, "top-right": { top: 8, right: 8 }, "bottom-left": { bottom: 8, left: 8 }, "bottom-right": { bottom: 8, right: 8 }, }[position]; return ( {showZoom && ( + )} {showLocate && ( {waitingForLocation ? ( ) : ( 📍 )} )} ); } function ControlButton({ onPress, label, children, disabled = false, }: { onPress: () => void; label: string; children: ReactNode; disabled?: boolean; }) { return ( {children} ); } type MapRouteProps = { coordinates: Array<[number, number]>; color?: string; width?: number; opacity?: number; dashArray?: [number, number]; }; function MapRoute({ coordinates, color = "#4285F4", width = 3, opacity = 0.8, dashArray, }: MapRouteProps) { const id = useId(); const sourceId = `route-source-${id}`; const layerId = `route-layer-${id}`; if (coordinates.length < 2) { return null; } const shape = { type: "Feature" as const, properties: {}, geometry: { type: "LineString" as const, coordinates, }, }; return ( ); } type MapUserLocationProps = { /** Show user location on the map */ visible?: boolean; /** Show accuracy circle around user location */ showAccuracy?: boolean; /** Show heading arrow indicating device direction */ showHeading?: boolean; /** Whether the location marker is animated between updates */ animated?: boolean; /** Minimum delta in meters for location updates */ minDisplacement?: number; /** Callback when user location is pressed */ onPress?: () => void; /** Auto-request location permissions if not granted */ autoRequestPermission?: boolean; }; function MapUserLocation({ visible = true, showAccuracy = true, showHeading = false, animated = true, minDisplacement, onPress, autoRequestPermission = true, }: MapUserLocationProps) { const [hasPermission, setHasPermission] = useState(false); const [permissionChecked, setPermissionChecked] = useState(false); useEffect(() => { let mounted = true; const checkAndRequestPermissions = async () => { try { if (autoRequestPermission) { const granted = await LocationManager.requestPermissions(); if (mounted) { setHasPermission(granted); setPermissionChecked(true); } } else { if (mounted) { setPermissionChecked(true); } } } catch (error) { console.error("Error requesting location permissions:", error); if (mounted) { setHasPermission(false); setPermissionChecked(true); } } }; if (visible) { checkAndRequestPermissions(); } return () => { mounted = false; }; }, [visible, autoRequestPermission]); if (!visible || !permissionChecked || !hasPermission) { return null; } return ( ); } // Re-export LocationManager for permission handling export { LocationManager }; export { Map, MapControls, MapMarker, MapRoute, MapUserLocation, MarkerContent, MarkerLabel, MarkerPopup, useCurrentPosition, useMap }; function getMapStyle(theme: "light" | "dark"): string | undefined { const MAPTILER_API_KEY = process.env.EXPO_PUBLIC_MAPTILER_API_KEY; const MAPTILER_STYLES = { dark: "dataviz-dark", light: "streets-v2", }; if (!MAPTILER_API_KEY) { console.warn( "[map-maptiler] EXPO_PUBLIC_MAPTILER_API_KEY not found. Falling back to default basemaps.\n" + "Get your free MapTiler API key at: https://cloud.maptiler.com/account/keys/", ); return undefined; } return `https://api.maptiler.com/maps/${MAPTILER_STYLES[theme]}/style.json?key=${MAPTILER_API_KEY}`; } [PreviousDefault Map Source](https://mapcn-rn.dev/docs/source-default) [NextMapbox Map Source](https://mapcn-rn.dev/docs/source-mapbox) ---