Map

Vector map whose basemap is drawn from your theme tokens.

Alpha — the API is still moving. Expect it to change in a minor release.

A vector map whose basemap is drawn from your theme tokens rather than shipped as a finished style.

A hosted style gives you two maps, a light one and a dark one. PanelUI has six themes, and a map that stays grey while the rest of the screen turns green is the one rectangle on the page that visibly does not belong to it. The tiles still come from a provider — that is the part worth paying for — but every colour in the style is a token your theme already resolved, so a new theme gets a matching basemap without anyone drawing one.

Map needs a development build

The renderer is native code and is not part of the Expo SDK, so Map cannot run in Expo Go. Install @maplibre/maplibre-react-native, add its config plugin to your app config, and rebuild:

npx expo install @maplibre/maplibre-react-native expo-dev-client
npx expo prebuild --clean
npx expo run:ios   # or: eas build --profile development --platform ios
app.json
{ "expo": { "plugins": ["@maplibre/maplibre-react-native"] } }

Without it Map renders a message saying so rather than throwing, so a screen that embeds one still loads. Check hasMapLibre before routing somewhere whose whole content is a map.

Installation

Map ships with the library — no separate install.

import { Map, Text, type LngLat, hasMapLibre, type MapHandle, Button, Card } from 'panelui-native';

Or copy the source into your project, to own and edit it:

npx panelui-cli@latest add map

Usage

<Map center={[-0.118, 51.509]} zoom={11}>
  <Map.Marker lngLat={[-0.118, 51.509]}>
    <Map.Popup title="Charing Cross">
      <Text size="xs" muted>The point every distance to London is measured from.</Text>
    </Map.Popup>
  </Map.Marker>
  <Map.Controls locate />
</Map>

Composition

<Map>
  <Map.Marker lngLat={[…]}>
    <Map.Label>…</Map.Label>
    <Map.Popup>…</Map.Popup>
  </Map.Marker>
  <Map.Route coordinates={[…]} />
  <Map.Arc from={[…]} to={[…]} />
  <Map.GeoJSON data={…} />
  <Map.Cluster data={…} />
  <Map.Heatmap data={…} />
  <Map.UserLocation />
  <Map.Overlay>…</Map.Overlay>
  <Map.Controls />
</Map>
  • Map.Marker — A point drawn as React views, so it can hold anything the rest of the library can draw.
  • Map.Label — A caption pinned to a marker and always visible, unlike a popup. size and tone are for maps carrying a lot of them at once.
  • Map.Popup — A card anchored to a point — to the marker it sits inside, or to a coordinate of its own.
  • Map.Controls — Zoom, compass and locate, as themed views rather than the renderer's own ornaments. inset and safeArea move the stack in from the map's edges.
  • Map.Overlay — A container for your own views over the map, pinned to the top, the bottom or a corner. The map measures it, and a Map.Controls in the same corner sits past it.
  • Map.Route — A path across the map, drawn as a style layer so its cost does not grow with its length.
  • Map.Arc — A curved connection between two points, bowed so arcs sharing an endpoint stay tellable apart.
  • Map.GeoJSON — Arbitrary geography as a themed layer. fill takes a style expression, which is what makes a choropleth one layer instead of one per bucket.
  • Map.Cluster — Dense points merged as they get too close to tell apart — the layer to reach for past a few dozen.
  • Map.Heatmap — Point density as a continuous field — the layer for where, once the points are too many to count. points hands over to the records themselves as it fades out past maxZoom, so no zoom level shows neither.
  • Map.UserLocation — The device's own position, drawn by the renderer.

Examples

Markers and popups

A popup inside a marker anchors to it and opens when it is pressed. Given a lngLat of its own it stands alone at that coordinate instead — the same component either way, because the difference is where it hangs rather than what it is.

<Map center={[-74.006, 40.713]} zoom={11}>
  {offices.map((office) => (
    <Map.Marker
      key={office.id}
      lngLat={office.lngLat}
      accessibilityLabel={office.name}
      accessibilityHint="Show office details"
    >
      <Map.Popup title={office.name}>
        <Text size="xs" muted>{office.headcount} people</Text>
      </Map.Popup>
    </Map.Marker>
  ))}
</Map>

A blank canvas for data

blank drops the basemap and keeps only the ground colour, for data that carries its own geography. Street detail under a choropleth is noise rather than context.

<Map blank bounds={[-125, 24, -66, 50]}>
  <Map.GeoJSON
    data={states}
    fill={['interpolate', ['linear'], ['get', 'share'], 0, muted, 1, primary]}
    accessibility={(feature) => ({
      label: `${feature.properties.name}: ${feature.properties.share}%`,
    })}
  />
</Map>

Clustering dense points

Map.Marker mounts a React view each, which a thousand points cannot afford — and a thousand overlapping pins would be unreadable even if it could. Map.Cluster does it in a style layer, merging points as they get too close to tell apart.

accessibility reads the same inline Feature or FeatureCollection passed to the layer. It creates a clipped screen-reader list, and activating an entry calls the layer's onPress with that exact feature. The rendered map geometry stays decorative. Resolve URL data before passing it when the layer needs a nonvisual list; PanelUI does not keep a second feature copy that can drift from the map.

<Map center={[10, 50]} zoom={3}>
  <Map.Cluster
    data={sightings}
    radius={60}
    onPress={inspect}
    accessibility={(feature) => ({
      label: feature.properties.species,
      hint: 'Inspect sighting',
      state: { selected: feature.id === selectedId },
    })}
  />
</Map>

Routes and arcs

A route follows the ground; an arc does not pretend to. The bow on an arc is not geography — it is there so two connections sharing an endpoint stay tellable apart, which a bundle of straight lines through one city does not.

<Map blank bounds={[-130, 20, 30, 60]}>
  <Map.Route id="leg" coordinates={delivered} />
  <Map.Route id="remaining" coordinates={planned} dashed opacity={0.6} />
  {links.map((link) => (
    <Map.Arc key={link.id} id={link.id} from={link.from} to={link.to} />
  ))}
</Map>

Cards over a full-screen map

Put a card over the map in a Map.Overlay. top and bottom span the map; a corner is as wide as its content. The overlay is measured, so a Map.Controls in a corner it covers sits 12 points past it and follows when the card changes height.

safeArea keeps the card clear of the status bar and the home indicator on a map that runs under them. It needs a SafeAreaProvider above the map, which Expo Router supplies.

inset is the distance from the map's edges, 12 by default. Pass 0 for a card that runs edge to edge, or one side to clear chrome that floats over the map from outside it, such as a tab bar.

<Map center={[-0.0986, 51.5052]} zoom={13}>
  <Map.Route coordinates={route} />

  <Map.Overlay position="top" safeArea>
    <Card>
      <Card.Content>
        <Text weight="semibold">Thames Path</Text>
        <Text size="sm" muted>3.6 km · 19:42</Text>
      </Card.Content>
    </Card>
  </Map.Overlay>

  <Map.Overlay position="bottom" safeArea>
    <Card>
      <Card.Content>
        <Text weight="medium">Splits</Text>
      </Card.Content>
    </Card>
  </Map.Overlay>

  {/* Each stack sits past the card on its corner. */}
  <Map.Controls position="top-right" zoom={false} locate />
  <Map.Controls position="bottom-right" recenter />
</Map>

{/* Clear a floating tab bar that is not a child of the map. */}
<Map>
  <Map.Controls inset={{ bottom: 96 }} />
</Map>

Moving the camera from code

Give Map a ref to move it in response to something that is not a gesture on the map — a list row, a search result, a filter. fitBounds frames a box, [west, south, east, north]; its second argument is the padding in points, or an object with padding and duration. Padding can be set per side, so the box stays clear of a sheet covering the bottom of the map.

recenter() returns to the view the map opened on. You do not need to remount the map with a new key to reset it.

const map = useRef<MapHandle>(null);

<Map ref={map} bounds={[-0.2, 51.47, -0.05, 51.54]}>
  <Map.Marker lngLat={[-0.118, 51.509]} />
</Map>

<Button onPress={() => map.current?.fitBounds([2.25, 48.81, 2.42, 48.9], { padding: { top: 48, right: 48, bottom: 320, left: 48 }, duration: 600 })}>
  Show Paris
</Button>
<Button onPress={() => map.current?.flyTo({ center: [-74.006, 40.713], zoom: 12 })}>
  Show New York
</Button>
<Button variant="outline" onPress={() => map.current?.recenter()}>
  Back to start
</Button>

A button that returns to the start

recenter on Map.Controls adds a button that does the same as recenter() on the ref: it goes back to the map's opening bounds, or its center and zoom.

<Map bounds={[-0.2, 51.47, -0.05, 51.54]}>
  <Map.Controls recenter compass />
</Map>

Bringing your own tiles

The tiles are the one part of the style that is somebody else's to license. source swaps the provider while keeping the token-built layers, so the map still follows your theme. The default is CARTO, which is free for non-commercial use and needs a licence for everything else.

Any provider serving the OpenMapTiles schema works. This one is MapTiler: create a key in your MapTiler account, and keep it in an environment variable rather than in source. fonts has to name a font stack the glyph endpoint serves.

const key = process.env.EXPO_PUBLIC_MAPTILER_KEY;

<Map
  center={[2.35, 48.86]}
  zoom={12}
  source={{
    url: `https://api.maptiler.com/tiles/v3/tiles.json?key=${key}`,
    glyphs: `https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key=${key}`,
    fonts: ['Noto Sans Regular'],
    attribution: '© MapTiler © OpenStreetMap contributors',
  }}
>
  <Map.Controls />
</Map>

Using a hosted style

mapStyle replaces the token-built style with a finished one, given as a URL or as the style document. The map no longer follows your theme, and source is ignored because the style names its own sources.

Some styles written for the browser use properties the native renderer cannot parse, and it drops every layer that uses one. Map changes those properties to the older names native reads before it hands the style over. To do that it fetches a URL itself, so nothing is drawn until the style arrives. If the fetch fails, the URL goes to the renderer unchanged.

To keep the theme and change only where the tiles come from, use source instead.

const key = process.env.EXPO_PUBLIC_MAPTILER_KEY;

<Map
  center={[2.35, 48.86]}
  zoom={12}
  mapStyle={`https://api.maptiler.com/maps/streets-v4/style.json?key=${key}`}
>
  <Map.Controls />
</Map>

Versions

Places

A street map that is the whole screen, with the search field, the locate control and the place card sitting over it. On a map every point of chrome is a point of geography you cannot see, so nothing here takes a strip of its own — including the way back, which lives in the search bar. The card appears only once something is selected, and the pin you picked is the only one carrying a name: four labels at once is a map with the names of everything on it and the shape of nothing.

const [selected, setSelected] = useState(null);
const map = useRef(null);

<Map ref={map} center={here} zoom={14}>
  {places.map((place) => (
    <Map.Marker
      key={place.id}
      lngLat={place.lngLat}
      onPress={() => {
        setSelected(place);
        map.current?.flyTo({ center: place.lngLat, zoom: 15 });
      }}
    >
      <Pin active={place.id === selected?.id} />
      {place.id === selected?.id ? (
        <Map.Label side="top" size="sm" tone="primary">{place.name}</Map.Label>
      ) : null}
    </Map.Marker>
  ))}

  {route ? <Map.Route coordinates={[here, selected.lngLat]} width={5} /> : null}
  <Map.Controls locate position="top-right" />
</Map>

Choropleth

One layer shaded by a style expression rather than one layer per bucket — which is what lets the metric switch without rebuilding the map.

<Map blank bounds={[-11, 35, 31, 63]}>
  <Map.GeoJSON
    data={europeFeatures(values)}
    fill={['interpolate', ['linear'], ['get', 'value'], 0, muted, peak, primary]}
    fillOpacity={0.9}
  />
  <Map.Controls position="top-right" />
</Map>

Heatmap

Density as a field, with the ramp passed in and the points it was made of fading in as the field fades out. The derived ramp is the base colour at rising opacity, which is right over empty ground; over streets and coastline the opacity alone stops being separable from what is underneath, and the hue has to carry part of the reading — which is what colors is for.

<Map bounds={[-10, 38, 20, 56]}>
  <Map.Heatmap
    data={reports}
    weight="weight"
    radius={28}
    colors={['#fff7bc', '#fee391', '#fec44f', '#fe9929', '#d7301f']}
    points
    maxZoom={9}
  />
</Map>

Delivery tracker

A driven leg and a planned one, told apart by dash rather than by colour — the two mean different things, and colour is already carrying the theme.

<Map center={[0.02, 51.545]} zoom={10.5}>
  <Map.Route id="done" coordinates={driven} width={4} />
  <Map.Route id="left" coordinates={planned} width={4} dashed opacity={0.5} />
</Map>

Store locator

A list and a map on one selection, so pressing either moves the other.

<Map center={selected.lngLat} zoom={13}>
  {stores.map((store) => (
    <Map.Marker
      key={store.id}
      lngLat={store.lngLat}
      onPress={() => setSelected(store)}
    >
      <View className={store.id === selected.id ? activePin : pin} />
    </Map.Marker>
  ))}
  <Map.Controls locate position="top-right" />
</Map>

Logistics network

Arcs between sites. The bow is not geography — it is there so two lanes sharing a hub stay tellable apart, which a bundle of straight lines through one city does not.

<Map bounds={[-12, 36, 26, 60]}>
  {lanes.map((lane) => (
    <Map.Arc
      key={lane.id}
      id={lane.id}
      from={byId[lane.from].lngLat}
      to={byId[lane.to].lngLat}
      curvature={0.18}
    />
  ))}
</Map>

Uptime monitor

Edge nodes coloured by state, spread across the world. A marker is a React view, so the status dot is the same one the rest of the app uses.

<Map center={[0, 25]} zoom={1.1}>
  {nodes.map((node) => (
    <Map.Marker key={node.id} lngLat={node.lngLat}>
      <View className="items-center">
        <View className={dotFor(node.state)} />
        <Map.Label>{node.id.toUpperCase()}</Map.Label>
      </View>
    </Map.Marker>
  ))}
</Map>

Cards over the map

A card along the top and another along the bottom, each in a Map.Overlay. Opening the splits makes the bottom card taller, and the zoom buttons move up with it.

<Map center={[-0.0986, 51.5052]} zoom={13.3}>
  <Map.Route coordinates={run} width={5} />

  <Map.Overlay position="top" safeArea>
    <Card>…</Card>
  </Map.Overlay>

  <Map.Overlay position="bottom" safeArea>
    <Card>
      <Card.Content>
        <Button size="sm" variant="outline" onPress={() => setSplits((open) => !open)}>
          {splits ? 'Hide' : 'Show'}
        </Button>
        {splits ? <Splits /> : null}
      </Card.Content>
    </Card>
  </Map.Overlay>

  <Map.Controls position="top-right" zoom={false} locate />
  <Map.Controls position="bottom-right" recenter />
</Map>

API Reference

Map

PropTypeDefaultDescription
centerLngLat—Initial centre, [longitude, latitude].
zoomnumber2Initial zoom. 0 is the whole world; 18 is a building.
bearingnumber—Initial bearing in degrees, clockwise from north.
pitchnumber—Initial tilt in degrees. 0 looks straight down.
boundsLngLatBounds—Frame these bounds instead of centring — [west, south, east, north]. Wins over center and zoom when both are given.
blankbooleanfalseDrop the basemap and keep only the ground colour. For data that carries its own geography — a choropleth, an arc diagram — where streets underneath are noise rather than context.
sourceBasemapSource—Where the vector tiles come from. Defaults to CARTO, which is free for non-commercial use and licensed for everything else. Ignored when mapStyle is set, because that style names its own sources.
mapStylestring | StyleSpecification—Use this style wholesale instead of building one from tokens — a URL, or the style document itself. For a map that has to match something outside the app. Styles written for the browser can use properties the native renderer cannot parse, and it drops every layer that has one. Map translates them to the older names the renderer does read before handing the style over. To do that it fetches a URL itself, and draws nothing until the style has arrived. If the fetch fails, the URL goes to the renderer unchanged.
rotatablebooleanfalseLet the map rotate and tilt. Off by default — most maps only pan and zoom.
interactivebooleantrueTurn off panning and zooming, for a map that is an illustration.
onViewStateChange(state: ViewState) => void—Fires continuously while the map moves.
onPress(lngLat: LngLat, point: PixelPoint) => void—Fires when the map is pressed somewhere that is not a feature. The second argument is the same press in screen coordinates, for anchoring something of your own to where the finger landed.
onReady() => void—Fires once the style has loaded and the first frame is drawn.

Map.Marker

PropTypeDefaultDescription
lngLatLngLat—Where the marker sits, [longitude, latitude].
anchor'center' | 'top' | 'bottom' | 'left' | 'right''center'Which part of the marker sits on the coordinate. A pin drawn above its point wants bottom; a dot centred on it wants the default.
onPress() => void—Pressing the marker. Adds a button role when given.
accessibilityLabelstring—Explicit spoken name. Other React Native accessibility props pass through too.
classNamestring—

Map.Label

PropTypeDefaultDescription
classNamestring—
side'top' | 'bottom''bottom'Which side of the marker the label sits on.
size'sm' | 'md''md'sm for a map carrying a lot of them, where the pills start to collide.
tone'default' | 'muted' | 'primary''default'How loud the label is. muted for codes and counts that support the map without being its subject; primary for the one place being pointed at.

Map.Popup

PropTypeDefaultDescription
classNamestring—
titlestring—Heading above the content. Strings are wrapped for you.
lngLatLngLat—Anchor to this coordinate instead of to an enclosing marker. Required when the popup is not inside one.

Map.Controls

PropTypeDefaultDescription
positionMapControlsPosition'bottom-right'Which corner the stack sits in.
insetnumber | ViewPadding—How far the stack sits from the map's edges, in points: one number for every side, or only the sides that need one. Defaults to 12. For chrome that floats over the map without being inside it, such as a translucent tab bar or a header.
safeAreabooleanfalseKeep clear of the status bar and the home indicator, for a map that runs underneath them. Reads the insets from the nearest SafeAreaProvider and does nothing without one.
zoombooleantrueZoom in and out. On by default — it is the one control a map always needs.
locatebooleanfalseRecentre on the device's location. Needs a location permission.
compassbooleanfalseReset bearing and pitch to north and flat.
recenterbooleanfalseReturn to the view the map opened on — its bounds, or its center and zoom. The same move as MapHandle.recenter().
classNamestring—
onLocate(lngLat: LngLat) => void—Called with the located coordinate, so a caller can react to it.

Map.Overlay

PropTypeDefaultDescription
positionMapOverlayPosition'bottom'Where it is pinned: top or bottom for a band across the map between its insets, or a corner (top-left, top-right, bottom-left, bottom-right), which is as wide as what is inside it.
insetnumber | ViewPadding—How far it sits from the map's edges, in points: one number for every side, or only the sides that need one. Defaults to 12. Pass 0 for a card that runs edge to edge.
safeAreabooleanfalseKeep clear of the status bar and the home indicator, for a map that runs underneath them. Reads the insets from the nearest SafeAreaProvider and does nothing without one.
classNamestring—

Map.Route

PropTypeDefaultDescription
coordinatesLngLat[]—The path, in order.
colorstring—Defaults to the primary token.
widthnumber3Line thickness in points.
dashedbooleanfalseDraw it dashed — for a leg that is planned rather than travelled.
opacitynumber10 is invisible, 1 is solid.
idstring'route'

Map.Arc

PropTypeDefaultDescription
fromLngLat—Where the arc starts.
toLngLat—Where it ends.
curvaturenumber0.2How far it bows. 0 is a straight line; 0.2 is the default lift.
colorstring—
widthnumber2
opacitynumber0.9
idstring'arc'

Map.GeoJSON

PropTypeDefaultDescription
dataunknown—A Feature, FeatureCollection, or the URL of one.
fillstring | unknown[]—Fill colour for polygons. A style expression works here too.
strokestring | unknown[]—Outline colour. Defaults to the border token.
strokeWidthnumber1Outline thickness.
fillOpacitynumber0.70 is invisible, 1 is solid.
onPress(feature: unknown) => void—Fires with the pressed feature.
accessibility(feature: unknown, index: number) => MapFeatureAccessibility—Describes each inline GeoJSON feature for the synchronized nonvisual list.
idstring'geojson'

Map.Cluster

PropTypeDefaultDescription
dataunknown—Point features to cluster.
colorstring—Defaults to the primary token.
textColorstring—Text colour inside a cluster bubble.
radiusnumber50How close two points have to be, in points, to merge.
maxZoomnumber14Above this zoom every point stands alone.
onPress(feature: unknown) => void—Fires with the pressed cluster or point.
accessibility(feature: unknown, index: number) => MapFeatureAccessibility—Describes each source point for the synchronized nonvisual list.
idstring'cluster'

Map.Heatmap

PropTypeDefaultDescription
dataunknown—Point features to spread.
weightstring—Feature property to weight each point by. Unweighted when omitted.
colorstring—Base colour of the field — a theme token by name, or a literal. The ramp is this colour at rising opacity, so density reads as more of the same thing rather than as a change of subject. Defaults to --color-chart-2, which is a saturated accent in every theme. It is deliberately not --color-chart-1: that is the series colour a chart is about, and every theme starts it on something close to the foreground — near-black in a light theme, near-white in a dark one — which over a basemap is a smudge rather than a measurement.
colorsstring[]—Replace the derived ramp outright, coolest first. For the conventional heat ramp, where the hue carries the reading as well as the opacity — worth it when the field sits over varied terrain and one hue at five opacities stops being separable from what is underneath it. The first stop is drawn at the lowest density, the last at the highest. Density zero stays fully transparent either way.
radiusnumber24Spread of a single point, in points, at street zoom. Larger blurs more. The drawn radius shrinks as the map zooms out, so a point keeps covering roughly the same ground rather than the same screen area.
intensitynumber1Overall strength. Raise it when the data is sparse.
opacitynumber0.850 is invisible, 1 is solid.
maxZoomnumber15Above this zoom the layer fades out — see the note on the component.
pointsbooleanfalseDraw the points themselves as the field fades out, coloured from the same ramp by weight. Without them, zooming past maxZoom leaves an empty map: the layer gets out of the way, and nothing takes its place.
idstring'heatmap'

Map.UserLocation

PropTypeDefaultDescription
headingbooleanfalseShow which way the device is facing, not just where it is.
accuracybooleanfalseDraw the ring showing how confident the fix is.

Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.

Notes

Why the basemap is assembled rather than downloaded

A hosted style ships its colours baked in, which gives you exactly two maps. Everything here is built from the same tokens as the rest of the library, so moon, grass and anything you add later get a basemap that matches them without a designer redrawing one per theme.

The layer list is deliberately short. A full street style runs to ninety-odd layers separating tunnel casings from bridge casings across eleven zoom stops; almost none of that survives being recoloured down to five greys, and every layer is another thing to keep in step with the tokens. What is there is the set that still reads as a map at any zoom: ground, water, green space, buildings, roads, boundaries, and the labels that make them findable.

Pass mapStyle to skip all of it and use a style wholesale, for a map that has to match something outside the app. See Using a hosted style above.

Tile licensing

Map defaults to CARTO's street tiles, which are free for non-commercial use and require a licence from CARTO for commercial use. That is a decision about your project rather than about this component, so source takes any provider serving the OpenMapTiles schema.

Controls and anything else you draw over the map

Map.Controls, Map.Overlay and any view of your own written as a child of Map are drawn above the map rather than inside the renderer. The renderer's own view lays its children out itself, so a view handed to it arrives stretched to the full size of the map and covering it.

Put layers inside Map and they reach the renderer: Map.Marker, Map.Route, Map.Arc, Map.GeoJSON, Map.Cluster, Map.Heatmap and Map.UserLocation. Everything else inside Map floats over it, with touches passing through wherever it is not drawing.

A view you position with your own classes is invisible to the controls, and a card placed in a corner covers the stack that was there. Wrap it in Map.Overlay instead. The controls then sit past it:

PropOnWhat it does
positionMap.Overlaytop or bottom for a band across the map, or one of the four corners.
insetbothDistance from the map's edges, in points. One number, or { top, right, bottom, left }. 12 by default.
safeAreabothAdds the device's safe-area insets, for a map under the status bar or the home indicator.

An overlay only moves controls in a corner it covers. A card at top-left leaves a stack at top-right where it is.

inset and safeArea leave the camera alone. The centre of the map is still the centre of the view, so a point you fly to can land under a card. Pass per-side padding to fitBounds to frame a box clear of them.

Moving the map from code

A ref on Map is a MapHandle:

MethodWhat it does
flyTo({ center, zoom?, duration? })Moves to a point.
fitBounds(bounds, padding | { padding?, duration? })Frames a box. Padding is one number, or { top, right, bottom, left }.
recenter(duration?)Returns to the view the map opened on.
getViewState()Resolves to where the map is looking now.

The center, zoom and bounds props set the opening view only. Changing them later does not move the map, so a re-render cannot pull it back mid-gesture. Use the ref to move it. For a control drawn inside the map, useMap() returns the same recenter.

Markers or clusters

Map.Marker is a React view, which is what lets it hold an avatar, a chip, or anything else the library draws. That also means a marker costs what a view costs. Past a few dozen points use Map.Cluster, which draws in a style layer and merges points as they crowd — by the time markers become expensive they have also become unreadable, so the two limits arrive together.

Labels on a crowded map

Map.Label is out of the marker's layout flow, and deliberately so: a marker sits on its coordinate by the centre of its box, so a label in flow beneath the pin would drag that centre down and lift every pin off the place it marks. The pill overhangs the marker by a wide margin on both sides, which is what lets a long name stay centred over a small dot without the marker being anchored by the name instead.

What that does not solve is a map with a dozen of them. size="sm" tightens the pill, and tone="muted" drops the text to the muted colour — together they are the difference between a network map and a wall of chips. The rule of thumb: label at default only the places the map is about, and let everything else support at muted. tone="primary" inverts the pill for the single place being pointed at.

Public exports

Values: Map, useMap, hasMapLibre, CARTO_SOURCE

Types: MapProps, MapHandle, MapFitBoundsOptions, MapMarkerProps, MapLabelProps, MapPopupProps, MapControlsProps, MapControlsPosition, MapOverlayProps, MapOverlayPosition, MapRouteProps, MapArcProps, MapGeoJSONProps, MapClusterProps, MapFeatureAccessibility, MapHeatmapProps, MapUserLocationProps, BasemapSource, BasemapTokens, LngLat, LngLatBounds, ViewPadding, ViewState

On this page