Skip to content
TrademarkTrademark
Docs
Modules

Map

Updated: October 21, 2026
View on GitHub
Soon
Not released yet. The published package does not include this yet, so it will not install today.

Map draws an interactive map with MapLibre GL on OpenFreeMap tiles: OpenStreetMap data, no API key, no account and no view limit. The basemap switches between light and dark with the theme, and its background takes the page background token, so a map sits on the page instead of on top of it. Markers are React content, so prices, avatars and buttons on the map are ordinary Once UI components.

Installation

maplibre-gl is an optional peer dependency, like recharts for the charts. Install it next to Once UI and import the map from its own entry point, so an app without a map never loads it:

Positions are [longitude, latitude], the order MapLibre and GeoJSON use. The map renders on the client only; on the server and until MapLibre loads it draws an empty frame of the same size, so the page does not shift.

Listings, clustering and popups

MapMarkers takes your items (each with an id and a position) and renders one marker per item. cluster merges markers that overlap into a count that zooms in when pressed. activeId lifts one marker above the rest, for the item hovered in a result list. bounds frames a list of positions and frames it again whenever the list changes, so a new search is always in view.

MapPin is the marker Once UI draws: a pill with a label, or a dot without one. With href it is a link and with onClick a button, so it is reachable by Tab; tabbing onto a pin outside the view pans it in. MapPopup is a surface card above a point; render it while it is open, and Escape or its close button call onClose.

Any content as a marker

MapMarker pins whatever you give it. anchor says which part of the content touches the point: bottom (the default) for a pin, center for an avatar.

Areas

MapCircle marks an area on the ground, sized in metres, in a token colour. It scales with the map, which makes it the honest way to show an approximate address.

Controlled viewport

viewport, defaultViewport and onViewportChange follow the same contract as every other controlled prop in Once UI. onViewportChange fires when a pan or zoom ends, which is also the moment to search the visible area.

19.0402, 47.4979 · zoom 11.0

Tiles from another provider

The default style is free, but you can point mapStyle at any MapLibre style: a URL or a style object, or a { light, dark } pair that follows the theme. A provider that needs a key usually takes it in the style URL; one that wants a header gets it through transformRequest.

Keep the attribution the provider asks for: the map shows the attribution each style declares in its bottom corner. Pass tint={false} to keep a style's own background colour.

Keyboard and motion

The map is a focusable region: arrow keys pan, + and - zoom, and the frame shows a focus ring. Pins with href or onClick are links and buttons in the tab order. With reduced motion on, fitting, zooming and following a controlled viewport jump instead of animating, and pins do not scale on hover. When WebGL is off, the frame says the map could not load instead of staying blank; set the text with labels.

Escape hatch

useMap() inside a Map returns the MapLibre instance and the loaded module, and onMapLoad hands you the instance once the first style has loaded, for layers the components do not cover (routes, heatmaps, GeoJSON).

Type
Default
viewport
Controlled camera: `{ center, zoom, bearing?, pitch? }`. Pair with `onViewportChange`.
MapViewport—
defaultViewport
Where the camera starts when `viewport` is not controlled.
MapViewport—
onViewportChange
Fires when a pan, zoom or rotation ends.
(viewport: MapViewport) => void—
bounds
Positions to frame, on first render and whenever the list changes.
readonly LngLat[]—
boundsPadding
Space around framed positions, in pixels. Default 56.
number—
boundsMaxZoom
The closest a fit zooms in. Default 15.
number—
mapStyle
Any MapLibre style URL or object, or a `{ light, dark }` pair. Default OpenFreeMap.
stringStyleSpecification{ light: MapStyleSource; dark: MapStyleSource; }
—
tint
Paint the basemap background with the page background token. Default true.
boolean—
controls
Show the zoom buttons. Default true.
boolean—
interactive
False draws a static map. Default true.
boolean—
cooperativeGestures
Scroll zooms only with Ctrl/⌘ and touch pans with two fingers.
boolean—
minZoom
number—
maxZoom
number—
onMapClick
Fires on a click on the map itself, with the position.
(position: LngLat) => void—
onMapLoad
The MapLibre instance once the first style has loaded.
(map: MapLibreMap) => void—
workerUrl
string—
transformRequest
Adds a key or header to tile requests.
MapOptions["transformRequest"]—
labels
Accessible names and the fallback text: `map`, `zoomIn`, `zoomOut`, `unavailable`.
Partial<MapLabels>—
children
React.ReactNode—
…and every FlexProps propinherited—
Type
Default
itemsrequired
readonly T[]—
renderItem
(item: T) => React.ReactNode—
activeId
string—
cluster
booleanMapClusterOptions
false
renderCluster
(cluster: { items: T[]; position: LngLat; zoomIn: () => void; }) => React.ReactNode—
clusterLabel
(count: number) => string(count) => `${count} places, zoom in`
Type
Default
positionrequired
LngLat—
children
React.ReactNode—
anchor
centertoprightbottomlefttop-lefttop-rightbottom-rightbottom-left
bottom
offset
[number, number]—
active
booleanfalse
draggable
booleanfalse
onDragEnd
(position: LngLat) => void—
Type
Default
children
React.ReactNode—
prefix
React.ReactNode—
variant
pilldot
—
active
booleanfalse
color
neutralbrandaccentinfodangerwarningsuccess
brand
href
string—
onClick
(event: React.MouseEvent<HTMLElement>) => void—
Type
Default
positionrequired
LngLat—
childrenrequired
React.ReactNode—
onClose
() => void—
gap
number40
closeLabel
stringClose
…and every FlexProps propinherited—
Type
Default
positionrequired
LngLat—
radiusrequired
number—
color
neutralbrandaccentinfodangerwarningsuccess
brand
opacity
number0.2
Edit this page on GitHub