Maps in React Native: Adding interactive markers
In this article, you'll learn how to add, customize and animate markers. We also cover how to implement a custom callout component

Search for a command to run...
In this article, you'll learn how to add, customize and animate markers. We also cover how to implement a custom callout component

I would like to understand if there is a way to view and create routes using these libraries. An article in that would be most appreciated.
Thanks for the feedback, Matthew! I'll keep this in mind for the upcoming articles. Cheers!
In this series, we'll cover everything related to maps in React Native. - Setting up the project - Getting API keys - Working with MapViews, Markers, Callouts - Customizing colors in maps - More
Introduction Maps are everywhere and can be used for a wide range of purposes, allowing you to represent different types of data, and enabling powerful interactions for your users. Whether you need to display store locations for a delivery app, show ...
💡 This is the third part of the "Collecting payments" series. Before proceeding, make sure to read Part 1, Part 2 & Part 3 We've finally reached the last part of the series! With all the tedious aspects taken care of, we just need to connect everyt...

💡 This is the third part of the "Collecting payments" series. Before proceeding, make sure to read Part 1 and Part 2 Before diving into this third part, I highly recommend watching this video to become familiar with some essential RevenueCat conce...

💡 This is the third part of the "Collecting payments" series. Before proceeding, make sure to read Part 1 SpiroKit for SaaS In case you want to save weeks of painful hours of work & research on your next React Native app, checkout SpiroKit for Sa...

Introduction Creating a mobile app involves numerous challenges and time-consuming tasks that must be completed before we can release our app in the stores. In-app purchases and subscriptions are especially difficult examples of this. They require co...

Building apps take time Building a mobile app is a challenging task. From coming up with an idea, prototyping, and finally building the app, there are many things to consider and problems we'll have to figure out: How do we make sure the entire app ...

In a previous article, we covered the initial setup process for a React Native project and explained how to configure a basic MapView component using the react-native-maps library. If you haven't read that article yet, I highly recommend you check it out.
This time, I’ll skip the project setup and basic MapView configuration and focus on adding markers, customizing their icons and callouts, implementing animations, and handling marker interactions.
Markers are an essential component in react-native-maps that allow us to place points of interest on the map. Each marker can have various properties, such as coordinates (latitude and longitude), title, description, and even custom icons.
To add markers to the MapView, we first need to define their properties. Let's start by updating the App.tsx from the previous article.
import React from "react";
import { StyleSheet } from "react-native";
- import MapView, { PROVIDER_GOOGLE } from "react-native-maps";
+ import MapView, { Callout, Marker, PROVIDER_GOOGLE } from "react-native-maps";
export default function App() {
+ const markers = [
+ {
+ coordinate: {
+ latitude: -34.603851,
+ longitude: -58.381775,
+ },
+ title: "Obelisco",
+ description:
+ "The Obelisco is an iconic monument located in Buenos Aires, Argentina. Standing tall at the intersection of Avenida 9 de Julio and Avenida Corrientes, it serves as a symbol of the city and a tribute to its historical and cultural significance.",
+ },
+ {
+ coordinate: {
+ latitude: -34.6011,
+ longitude: -58.3835,
+ },
+ title: "Teatro Colón",
+ description:
+ "Teatro Colón, also known as the Colon Theatre, is a world-renowned opera house situated in Buenos Aires, Argentina. With its stunning architecture and rich history, it is considered one of the finest opera houses globally, hosting exceptional performances and captivating audiences with its grandeur.",
+ },
+ // Add more markers as needed
+ ];
+ const renderMarkers = () => {
+ return markers.map((marker, index) => (
+ <Marker
+ key={index}
+ coordinate={marker.coordinate}
+ title={marker.title}
+ description={marker.description}
+ />
+ ));
+ };
return (
<MapView
provider={PROVIDER_GOOGLE} // Specify Google Maps as the provider
style={styles.map}
initialRegion={{
latitude: -34.603738,
longitude: -58.38157,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
}}
>
+ {renderMarkers()}
</MapView>
);
}
const styles = StyleSheet.create({
map: {
flex: 1,
},
});
A few things to mention about the code snippet above:
App.tsx to define an array of markers, each with its own coordinate, title, and description.renderMarkers function to render a <Marker> component for each item in the markers array, passing in the corresponding properties.<Marker> components within the <MapView> component, the markers will be displayed on the map at their respective coordinates.If everything went ok, you should see something like this:

That’s fine for a basic scenario, but if you are like me, you may think that we could improve the UI for that “tooltip” bubble that appears after pressing a marker. That tooltip is called “Callout”, and react-native-maps includes a <Callout> component we can use to wrap our custom card, so let’s create a <CustomCallout> component!
Before working on our new component, we’ll need to update our array of markers to add a new property called imageUrl. We’ll use this URL to display an Image for each point of interest. We are also adding a new type called MarkerWithMetadata to get better IntelliSense later in the Callout component.
// App.tsx
+ export type MarkerWithMetadata = {
+ coordinate: MapMarkerProps["coordinate"];
+ title?: MapMarkerProps["title"];
+ description?: MapMarkerProps["description"];
+ imageUrl?: string;
+ };
export default function App() {
const markers: MarkerWithMetadata[] = [
{
coordinate: {
latitude: -34.603851,
longitude: -58.381775,
},
title: "Obelisco",
+ imageUrl: "https://upload.wikimedia.org/wikipedia/commons/f/fc/Buenos_Aires_%2820234294752%29.jpg",
description: "The Obelisco is an iconic monument located in Buenos Aires, Argentina. Standing tall at the intersection of Avenida 9 de Julio and Avenida Corrientes, it serves as a symbol of the city and a tribute to its historical and cultural significance.",
},
{
coordinate: {
latitude: -34.6011,
longitude: -58.3835,
},
+ imageUrl: "https://upload.wikimedia.org/wikipedia/commons/1/1e/Buenos_Aires_Teatro_Colon_2.jpg",
title: "Teatro Colón",
description: "Teatro Colón, also known as the Colon Theatre, is a world-renowned opera house situated in Buenos Aires, Argentina. With its stunning architecture and rich history, it is considered one of the finest opera houses globally, hosting exceptional performances and captivating audiences with its grandeur.",
},
// Add more markers as needed
];
...
}
Create a new folder called components and an empty file inside for our new component by running the following command on your terminal
mkdir components
touch components/CustomCallout.tsx
Now, open the CustomCallout file and add the following code
import React from "react";
import { View, StyleSheet, Dimensions, Image, Text } from "react-native";
import { Callout } from "react-native-maps";
import { MarkerWithMetadata } from "../App";
const screenWidth = Dimensions.get("window").width;
const CustomCallout: React.FC<{
marker: MarkerWithMetadata;
}> = ({ marker }) => {
return (
<Callout tooltip>
<View>
<View style={styles.container}>
<Image
source={{
uri: marker.imageUrl,
}}
resizeMode="cover"
style={{ width: 100, height: "100%" }}
></Image>
<View style={{ paddingHorizontal: 16, paddingVertical: 8, flex: 1 }}>
<Text
style={{
fontWeight: "bold",
fontSize: 18,
}}
>
{marker.title}
</Text>
<Text>{marker.description}</Text>
</View>
</View>
<View style={styles.triangle}></View>
</View>
</Callout>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: "white",
width: screenWidth * 0.8,
flexDirection: "row",
borderWidth: 2,
borderRadius: 12,
overflow: "hidden",
},
triangle: {
left: (screenWidth * 0.8) / 2 - 10,
width: 0,
height: 0,
backgroundColor: "transparent",
borderStyle: "solid",
borderTopWidth: 20,
borderRightWidth: 10,
borderBottomWidth: 0,
borderLeftWidth: 10,
borderTopColor: "black",
borderRightColor: "transparent",
borderBottomColor: "transparent",
borderLeftColor: "transparent",
},
});
export default CustomCallout;
A few things to mention:
<Callout> component from react-native-maps. We also pass the tooltip prop so we can take absolute control over the UI.Finally, we need to update the App.tsx file to render our custom callout
// App.tsx
...
+ import CustomCallout from "./components/CustomCallout";
export default function App() {
const markers: MarkerWithMetadata[] = [
...
];
const renderMarkers = () => {
return markers.map((marker, index) => {
return (
<Marker
key={index}
coordinate={marker.coordinate}
- title={marker.title}
- description={marker.description}
>
+ <CustomCallout marker={marker}></CustomCallout>
</Marker>
);
});
};
return (
...
);
}
That’s it! You should now look something like this:

Another cool thing about react-native-maps is that you can customize the marker icon to suit your needs. In the library docs, you can find two props available within the <Marker> component: image and icon.
The differences between these two properties are not clear to me (except for the fact that icon only works with the google maps provider), and in both cases you can pass a local image resource like this:
// App.tsx
...
+ import MarkerIcon from "./assets/marker.png";
export default function App() {
const markers: MarkerWithMetadata[] = [
...
];
const renderMarkers = () => {
return markers.map((marker, index) => {
return (
<Marker
+ image={MarkerIcon}
+ // You can also use icon with Google Maps
+ icon={MarkerIcon}
key={new Date().getTime() + index}
coordinate={marker.coordinate}
>
<CustomCallout marker={marker}></CustomCallout>
</Marker>
);
});
};
return (
...
);
}
But there’s a catch! Because these props expect an ImageSource, it’s a little bit limited. As example, I couldn’t find an easy way to change the size of the image.
If you want to go crazy and implement a complex marker, you can add a children component inside the <Marker>, and the rendered content will replace the marker symbol. In this example, I’m only using an Image component and customizing the background color and borders, but feel free to experiment:
+ import MarkerIcon from "./assets/marker.png";
export default function App() {
const markers: MarkerWithMetadata[] = [
...
];
const renderMarkers = () => {
return markers.map((marker, index) => {
return (
<Marker
key={new Date().getTime() + index}
coordinate={marker.coordinate}
>
+ <Image source={MarkerIcon} style={styles.marker}></Image>
<CustomCallout marker={marker}></CustomCallout>
</Marker>
);
});
};
return (
...
);
}
const styles = StyleSheet.create({
map: {
flex: 1,
},
+ marker: {
+ width: 60,
+ height: 60,
+ resizeMode: "contain",
+ backgroundColor: "yellow",
+ borderRadius: 30,
+ borderWidth: 2,
+ }
});
After these changes, your custom markers should look like this:

This is just a basic example, but the possibilities are endless. As example, if you are working on a food delivery app, you could design a custom icon to represent how expensive is each restaurant, like: "💲", "💲💲", "💲💲💲", etc. You can then use conditional rendering to display the right icon based on your marker metadata.
Disclaimer: I’m not good at animations, but I thought it could be fun to add a little touch to the map. What if each marker would provide subtle feedback to the user when it’s tapped?
Let’s make some changes to our App.tsx file, then I’ll explain a few points.
- import { StyleSheet, Image } from "react-native";
+ import { StyleSheet, Image, Animated } from "react-native";
...
export type MarkerWithMetadata = {
+ id: number;
coordinate: MapMarkerProps["coordinate"];
title?: MapMarkerProps["title"];
description?: MapMarkerProps["description"];
imageUrl?: string;
};
export default function App() {
const markers: MarkerWithMetadata[] = [
{
+ id: 1,
...
},
{
+ id: 2,
...
},
// Add more markers as needed
];
+ const markerScales = React.useRef<{
+ [key: number]: Animated.Value;
+ }>({});
+ for (const marker of markers) {
+ markerScales.current[marker.id] = new Animated.Value(1);
+ }
+ const handleMarkerPress = (marker: MarkerWithMetadata) => {
+ const scale = markerScales.current[marker.id];
+ Animated.timing(scale, {
+ toValue: 1.25,
+ duration: 100,
+ useNativeDriver: false,
+ }).start(() => {
+ Animated.timing(scale, {
+ toValue: 1,
+ duration: 100,
+ useNativeDriver: false,
+ }).start();
+ });
+ };
const renderMarkers = () => {
return markers.map((marker) => {
return (
<Marker
+ key={marker.id}
coordinate={marker.coordinate}
onPress={() => handleMarkerPress(marker)}
>
+ <Animated.View
+ style={{
+ padding: 10,
+ transform: [{ scale: markerScales.current[marker.id] }],
+ }}
>
<Image source={MarkerIcon} style={[styles.marker]}></Image>
+ </Animated.View>
<CustomCallout marker={marker}></CustomCallout>
</Marker>
);
});
};
return (
...
}
What’s happening here?
id prop on each marker:id prop is added to each marker to uniquely identify them. This id will be used later to update the scale of the tapped marker.markerScales object is created using useRef to keep track of the scale value for each marker. It is initialized as an empty object.onPress event:handleMarkerPress function is called when a marker is pressed. It takes the marker object as a parameter, which contains the metadata for the pressed marker. Inside the function, an animation is triggered using Animated.timing to increase the scale of the marker to 1.25 over a duration of 100 milliseconds. Once this animation completes, a second animation is started to bring the marker's scale back to 1. This creates a visual effect of the marker briefly expanding and returning to its original size.Animated.View:renderMarkers function, an Animated.View is used to wrap the marker. The transform style property is applied to the Animated.View to scale the marker based on the corresponding markerScales.current[marker.id].scale value.If everything went as expected, you should look something like this:

As we saw in the previous example, the <Marker> component comes with an onPress event that allows you to run a custom logic every time a marker is pressed.
But react-native-maps also includes a few more things I would like to mention:
If you set draggable in your marker, you can also use onDragStart and onDragEnd to update your marker’s position.
// App.tsx
export default function App() {
...
const renderMarkers = () => {
return markers.map((marker) => {
return (
<Marker
key={marker.id}
+ draggable
+ onDragEnd={(event) => {
+ // use the new coordinates to update marker location
+ const newCoordinate = event.nativeEvent.coordinate;
+ marker.coordinate = newCoordinate;
}}
coordinate={marker.coordinate}
onPress={() => handleMarkerPress(marker)}
>
...
</Marker>
);
});
};
return (
...
);
}

You can also handle the onPress event inside the Callout component like this
// CustomCallout.tsx
...
const CustomCallout: React.FC<{
marker: MarkerWithMetadata;
}> = ({ marker }) => {
return (
<Callout
tooltip
+ onPress={() => {
+ Alert.alert(`${marker.title} pressed`);
+ }}
>
...
</Callout>
);
};
...
export default CustomCallout;
That’s it for this article. Please let me know if you find it useful. I’m planning to keep writing about Maps in React Native, so feel free to reach out if there is any specific topic you would want me to cover in the future.
To learn more about the <Marker> component, you can also visit the docs on GitHub
Happy coding!