rguides

Interactive Maps with leaflet

The leaflet package brings interactive maps to R. It wraps the popular Leaflet JavaScript library through the htmlwidgets framework, letting you create zoomable, pannable maps directly from R. You can use these maps in RStudio, Shiny applications, and R Markdown documents.

This guide walks you through creating maps, adding markers with popups, and layering different data types.

Installing and loading leaflet

Install leaflet from CRAN:

install.packages("leaflet")
library(leaflet)

The package depends on R 3.5.0 or later. It automatically loads htmltools for popup content and magrittr for the pipe operator.

Creating your first map

Start with the leaflet() function to initialize a map. By default, it shows a blank world view. Add tiles using addTiles() to display map imagery:

leaflet() %>%
  addTiles()

This gives you an interactive map with zoom and pan controls. The map centers on the world by default.

Set a specific location and zoom level using setView():

leaflet() %>%
  addTiles() %>%
  setView(lng = -122.4194, lat = 37.7749, zoom = 12)

This centers the map on San Francisco with a zoom level of 12.

Adding markers

The addMarkers() function places markers on the map. You can provide coordinates directly or use data from a data frame:

# Single marker
leaflet() %>%
  addTiles() %>%
  addMarkers(lng = -122.4194, lat = 37.7749, popup = "San Francisco")

When working with data frames, use the formula notation to specify column names:

# Multiple markers from data frame
df <- data.frame(
  name = c("Tokyo", "London", "New York"),
  lat = c(35.6762, 51.5074, 40.7128),
  lng = c(139.6503, -0.1278, -74.0060)
)

leaflet(df) %>%
  addTiles() %>%
  addMarkers(~lng, ~lat, popup = ~name)

The ~lng and ~lat formulas tell leaflet which columns contain the coordinates.

Popups and labels

Popups appear when users click a marker. Labels appear on hover by default. Use the popup and label arguments to add both:

leaflet(df) %>%
  addTiles() %>%
  addMarkers(
    ~lng, ~lat,
    popup = ~paste0("<b>", name, "</b>"),
    label = ~name
  )

The htmlEscape() function from htmltools sanitizes text to prevent HTML injection:

library(htmltools)

leaflet(df) %>%
  addTiles() %>%
  addMarkers(
    ~lng, ~lat,
    popup = ~htmlEscape(name)
  )

For labels that are always visible, use labelOptions() with noHide = TRUE:

leaflet() %>%
  addTiles() %>%
  addMarkers(
    lng = -122.4194, lat = 37.7749,
    label = "Always visible label",
    labelOptions = labelOptions(noHide = TRUE)
  )

Customize label appearance with text size, color, and CSS:

leaflet() %>%
  addTiles() %>%
  addMarkers(
    lng = -122.4194, lat = 37.7749,
    label = "Styled label",
    labelOptions = labelOptions(
      noHide = TRUE,
      textsize = "15px",
      style = list("color" = "red", "font-weight" = "bold")
    )
  )

Tile providers

The default OpenStreetMap tiles work, but leaflet supports many providers. The addProviderTiles() function loads different tile layers:

leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lng = -122.4194, lat = 37.7749, zoom = 12)

Popular providers include CartoDB.Positron for a clean light theme, CartoDB.DarkMatter for dark mode, and Esri.WorldImagery for satellite imagery. The providers object contains dozens of tile options from various mapping services.

Switch between providers easily:

# Satellite view
leaflet() %>%
  addProviderTiles(providers$Esri.WorldImagery) %>%
  setView(lng = -122.4194, lat = 37.7749, zoom = 14)

Adding shapes

Beyond markers, you can add circles, lines, and polygons:

# Circle
leaflet() %>%
  addTiles() %>%
  addCircles(lng = -122.4194, lat = 37.7749, radius = 500)

# Rectangle (polygon)
leaflet() %>%
  addTiles() %>%
  addRectangles(
    lng1 = -122.43, lat1 = 37.78,
    lng2 = -122.40, lat2 = 37.77,
    popup = "Area popup"
  )

Circles are useful for showing reach or coverage areas. Polygons draw arbitrary shapes on the map.

Customizing markers

The default blue markers work, but you can customize icons and colors. Use addAwesomeMarkers() for additional icon options:

# Custom colored icons
library(leaflet.extras)

leaflet(df) %>%
  addTiles() %>%
  addAwesomeMarkers(
    ~lng, ~lat,
    icon = awesomeIcons(
      library = "fa",
      icon = "star",
      markerColor = "red",
      iconColor = "white"
    )
  )

Available marker colors include red, blue, green, purple, orange, darkred, lightred, beige, darkblue, darkgreen, and more.

Marker clustering

When displaying many markers, clustering improves performance and usability. The leaflet.extras package provides this feature:

install.packages("leaflet.extras")
library(leaflet.extras)

leaflet(df) %>%
  addTiles() %>%
  addMarkerClusterOptions()

This groups nearby markers into clusters that expand when clicked. Each cluster displays the number of markers it contains.

Performance considerations

Leaflet renders tiles and markers in the browser, so large datasets can slow down map interaction. For more than a few thousand points, cluster markers with addMarkers(clusterOptions = markerClusterOptions()), this groups nearby markers at lower zoom levels and reveals them as you zoom in. For polygon-heavy maps with many vertices, simplify geometries with sf::st_simplify() before passing to leaflet to reduce file size and render time. For datasets with tens of thousands of points, consider heatmaps (addHeatmap() from leaflet.extras) or raster tiles generated server-side instead of vector markers.

Popups vs tooltips

addPopups() and the popup argument in addMarkers() create click-activated HTML boxes. label and addLabelOnlyMarkers() create hover-activated text. For rich popups with formatted HTML, build the string with sprintf() or htmltools::tags$div(): popup = sprintf("<b>%s</b><br>Population: %s", city, population). The popupOptions() function controls popup behavior, autoClose = TRUE closes other popups when a new one opens.

Leaflet fundamentals

leaflet() initializes a map widget. addTiles() adds the default OpenStreetMap tile layer. Every subsequent call adds a layer or modifies map options. The final object is an htmlwidget that renders in Shiny, R Markdown, Quarto, and the RStudio viewer.

Tile providers: addTiles() uses OpenStreetMap. addProviderTiles(providers$CartoDB.Positron) uses CartoDB’s clean light basemap. addProviderTiles(providers$Esri.WorldImagery) adds satellite imagery. leaflet.providers package lists all available providers. For production, use a provider that allows your use case, OSM has attribution requirements.

setView(lng, lat, zoom) centers the map and sets the initial zoom level. fitBounds(lng1, lat1, lng2, lat2) fits the view to a bounding box, automatically choosing the appropriate zoom.

Adding layers

addMarkers(lng, lat, popup, label, icon) adds pin markers. popup accepts HTML strings displayed when clicking. label shows on hover. addCircleMarkers(radius, color, fillOpacity) draws circles, more useful for representing quantities because you can vary the radius.

addPolylines(data = sf_lines, color, weight, opacity) renders line features. addPolygons(data = sf_polys, fillColor, fillOpacity, weight, color) renders polygon features. Both accept sf objects with the ~ formula syntax for mapping column values to aesthetics.

addLegend(position = "bottomright", pal = color_pal, values = ~variable, title = "Title") adds a color legend. The pal argument takes a palette function created with colorNumeric(), colorBin(), or colorFactor().

Clustering and performance

addMarkers(clusterOptions = markerClusterOptions()) clusters nearby markers automatically, expanding on zoom. This is essential for datasets with thousands of points, rendering each point individually is too slow for browsers.

addCircleMarkers() without clustering is appropriate for hundreds of points. For tens of thousands, use clustering or spatial aggregation before rendering.

For large polygon datasets, simplify geometries first: rmapshaper::ms_simplify(sf_polys, keep = 0.05) reduces vertex count by 95%. Send simplified geometries to leaflet; the visual difference is minimal at typical zoom levels.

Custom icons and controls

makeIcon(iconUrl = "marker.png", iconWidth = 24, iconHeight = 24) creates a custom marker icon. iconList() creates a set of different icons for use with awesomeIcons() or makeIconList().

addMeasure() adds a measurement tool for drawing lines and polygons to measure distances and areas. addScaleBar() adds a distance scale. addMiniMap() adds an inset overview map in the corner.

addDrawToolbar() from the leaflet.extras package enables drawing shapes on the map. Drawn shapes are returned as GeoJSON and can be captured in Shiny via input$map_id_draw_new_feature. This powers tools where users define a study area by drawing on the map.

Leaflet for interactive mapping

Leaflet is the most widely used R package for interactive web maps. It wraps the Leaflet JavaScript library with an R htmlwidgets interface, producing maps that render in browsers and work in R Markdown, Quarto, and Shiny without additional server infrastructure. For maps that users need to zoom, pan, and click for details, leaflet is the standard choice.

The mental model for building leaflet maps is additive: start with a base map, then add layers. Each layer type, markers, polygons, polylines, rasters, heatmaps, has a corresponding addXxx function. Layers are rendered in the order they are added, so background polygons should be added before foreground markers. Named groups allow toggling layers on and off through the layer control widget.

Styling and theming

Tile layers provide the base map style. The default OpenStreetMap tile is recognizable but not always visually appropriate for data presentations. Alternative providers, CartoDB.Positron for light backgrounds, CartoDB.DarkMatter for dark, Esri.WorldImagery for satellite, change the map’s visual character significantly. addProviderTiles from the leaflet.providers package lists all available tile providers.

Colors for polygon fills and line strokes typically encode a data attribute. The colorNumeric, colorBin, colorQuantile, and colorFactor functions from the leaflet package create color palettes for different scale types. Using the same palette consistently across multiple layers and the legend produces a coherent visual. The addLegend function adds a legend that explains the color encoding, which is necessary for any map where color represents data.

Shiny integration

In Shiny, leaflet maps update reactively when inputs change. renderLeaflet renders the initial map; leafletProxy modifies an existing map without re-rendering it from scratch. The proxy approach is essential for maps where only a data layer changes, re-rendering the full map causes jarring visual resets. Using leafletProxy to update marker data, polygon fills, or layer visibility preserves the user’s pan and zoom state while updating the data.

Input values from map interactions are available as reactive inputs in Shiny: the click event provides coordinates and popup content, the bounds event provides the visible map extent, and the zoom event provides the current zoom level. Using these inputs to filter displayed data based on map extent — showing only the data in the current view — creates a map that serves as an interactive filter for accompanying charts and tables.

Conclusion

The leaflet package makes interactive mapping accessible from R. You can create maps with a few lines of code, add markers with popups, customize labels, switch tile providers, layer multiple shape types, and even cluster markers for better performance. The package integrates smoothly with Shiny for building map-based applications.

For more advanced spatial visualization, explore the sf package for working with vector spatial data formats.

See also