Map
ProAn interactive vector map that wears your theme. OpenStreetMap tiles with no key, painted from the theme's own tokens, re-painted in place when the theme changes, with pins, controls and the full MapLibre instance when you need it.
Installation#
Usage#
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Give the map a height: it fills its box. MapLibre loads on the client after the first paint, so the frame shows the theme's land colour until the tiles fade in, and nothing renders on the server.
Anatomy#
import { Map, MapMarker, MapPin, MapControls, MapCorner } from "@/components/ui/map"
<Map>
<MapMarker lngLat={…}>
<MapPin />
</MapMarker>
<MapControls />
<MapCorner>…</MapCorner>
</Map>Theming#
The basemap is painted from the --map-* tokens, which mix the theme's own --background, --foreground, --info and --success. Light grounds get grey land under white streets; dark grounds flip it, with streets that brighten with their rank. Switch the theme and the map re-paints in place, without reloading tiles or touching your own layers. Re-tint one map with a utility, such as [--map-water:var(--purple)].
labels={false} and buildings={false} quieten it further, for a backdrop under your own data.
Pins#
MapMarker pins any React content to a coordinate and keeps it in your tree, so Context, state and events work as usual. MapPin is the canonical marker to put in it: a chip on the popover surface with the tone as a leading dot, or the dot alone without a label (name it with aria-label). It is a button only when it has something to do, an onClick or a Popover around it, so a pin that only marks a place stays out of the tab order. selected inverts it to ink.
Prices#
variant="price" turns the pin into the chip a map of stays or listings wears: the figure alone, bolder and in tabular figures, so a dozen of them read as numbers to compare. Open one and it inverts to ink; visited greys the ones already seen, so the eye moves on. The hovered, focused or selected pin rises above its neighbours, so a crowded cluster never buries the one you are looking at. Format the price yourself, with Intl.NumberFormat, and name the pin with the place and the price.
Emoji#
variant="emoji" marks a place by what it is instead of by a status: a café, a beach, a museum, for a map of things to do or a neighbourhood guide. The emoji takes the dot's place; beside a label it sits in its own circle, concentric with the chip, and without one it is the pin on its own (name it with aria-label). labelMinZoom sets the zoom a label shows from: further out the pin is its emoji alone and the name opens on hover and on focus. Give every pin the same zoom and the whole map changes detail at once: zoom in to read the names. The emoji is decorative: the label names the pin. selected and visited work as on every pin.
Symbols#
A dispatch, fleet or incident map marks things by what they are and how urgent, not by a name on a chip. Give MapPin an icon, a sprite src or a shape and it becomes a symbol: a circle with a category icon, a square for a fixed asset, a diamond for a numbered priority, a dot for a moving unit, a cross for a unit of another service. Fill it with a tone, or with any CSS color straight from your data: the icon's ink is picked by contrast (set ink to decide it yourself).
halo draws a still ring (glow, ring or dashed, for a unit in the air), pulse="ping" a slow live halo for units on duty and pulse="alert" a fast, strong one for a panic. caption puts a callsign beside the symbol or a title under it. label names the symbol for assistive tech, and selected grows it and rings it in ink. Click the symbols.
Three sizes: sm 22, md 28 and lg 36px for a shape that holds an icon, 12, 16 and 20px for a dot or a cross. Pass a sprite as src (a vehicle silhouette) and it takes the icon's place. The symbol's box is the shape alone, so MapMarker's default center anchor lands on its middle; the halo, the pulse and the caption spill over it. The pulses animate transform and opacity only and hold still under reduced motion, as a faint ring.
Your own layers#
useMap() hands any child the MapLibre instance. Add sources and layers once loaded is true and remove them on cleanup. MapLibre reads neither oklch() nor color-mix(), so useMapColor resolves a token to an rgb value it can paint, and resolves it again when the theme changes: set it in an effect and your route re-paints with the basemap. Switch the theme to see it. MapCorner floats your own parts, such as this ETA, in a corner, inset like the controls.
Controls#
MapControls is a floating vertical Toolbar: zoom, a compass that turns with the map and resets north and tilt, and locate, which asks for the visitor's position and flies there. position picks the corner; the tooltips open on the side away from the edge. This map starts rotated, so the compass is turned.
Any style#
mapStyle swaps the basemap for any MapLibre style, by URL or as an object: a provider's satellite tiles, your own design from Maputnik. The theming steps aside; pins, controls and useMap work the same.
API reference#
Map
A labelled role="region" that hosts a MapLibre map; it fills its box, so give it a height. Forwards native <div> props. Every part accepts className, merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| center | [number, number] | [-73.9855, 40.758] | Where the camera starts, [longitude, latitude]. Seeds the map once. |
| zoom | number | 12 | Starting zoom. Seeds the map once. |
| bearing | number | 0 | Starting rotation in degrees. |
| pitch | number | 0 | Starting tilt in degrees. |
| minZoom | number | - | The furthest the camera zooms out. |
| maxZoom | number | - | The closest the camera zooms in. |
| maxBounds | [[number, number], [number, number]] | - | Keeps the camera inside [[west, south], [east, north]]. |
| interactive | boolean | true | Pan, zoom and rotate with pointer and keyboard. |
| cooperativeGestures | boolean | false | Scroll and one-finger drag pass to the page; zoom takes Ctrl or ⌘ plus scroll. For a map in a scrolling page. |
| frame | "rounded" | "flush" | "rounded" | rounded frames the map with a hairline; flush is for a map that is the page. |
| labels | boolean | true | Street, water and place names on the Koala basemap. |
| buildings | boolean | true | Building footprints from zoom 14. |
| mapStyle | string | StyleSpecification | - | Any MapLibre style, a URL or an object, in place of the Koala basemap. Theming steps aside. |
| label | string | "Map" | The region's accessible name. |
| options | Partial<MapOptions> | - | Any other MapLibre option, passed at creation (container and style aside). |
| workerUrl | string | - | Where MapLibre's worker loads from. Defaults to jsDelivr at the installed version; self-host it for a strict CSP. |
| onLoad | (map: MapLibreMap) => void | - | The style has loaded: add your sources and layers here. |
| onMoveEnd | (view: MapView) => void | - | The camera came to rest, with { center, zoom, bearing, pitch }. |
| onMapClick | (lngLat: [number, number], map: MapLibreMap) => void | - | A click on the map itself, not on a marker. |
| CSS variable | Description |
|---|---|
| --map-* | The basemap palette (land, water, park, road, label and the rest), mixed from the theme. Re-tint one map with a utility, e.g. [--map-water:var(--purple)]. |
MapMarker
Pins any React content to a coordinate; the content stays in your tree and is portalled into the marker. Forwards native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| lngLat* | [number, number] | - | Where the marker sits. Updating it moves the marker. |
| anchor | PositionAnchor | "center" | Which point of the content touches the coordinate. |
| offset | [number, number] | - | Nudge in pixels, [x, y]. |
| flat | boolean | true | Keeps the marker upright and flat while the map pitches and rotates. |
| children* | React.ReactNode | - | The marker's content: a MapPin, an Avatar, a Popover trigger. |
MapPin
The marker visual. A <button> when given onClick (forwards its props), otherwise a <span> that stays out of the tab order.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | React.ReactNode | - | The words on the chip. Without one the pin is the dot alone: name it with aria-label. |
| variant | "default" | "price" | "emoji" | "default" | default leads with the tone dot; price is the figure alone, bolder; emoji leads with emoji. |
| emoji | string | - | The mark the emoji variant shows, e.g. "☕". Decorative. |
| tone | "brand" | "neutral" | "info" | "success" | "warning" | "destructive" | "purple" | "teal" | "neutral" | The dot's colour, so price and emoji have none. |
| labelMinZoom | number | - | The zoom the label shows from. Below it the pin is its mark alone, opening on hover and focus. Not for price. |
| selected | boolean | - | The chosen pin: the chip inverts to ink and sets aria-pressed. |
| visited | boolean | - | A place already looked at: the chip greys to the muted step. |
| color | string | - | Any CSS colour for the dot (or the symbol), winning over tone. |
| icon | React.ReactNode | - | Turns the pin into a symbol: a shape holding this glyph (a dispatch blip, a fleet unit, an incident). Decorative. |
| src | string | - | Symbol: a sprite image in place of the icon. |
| shape | "circle" | "square" | "diamond" | "dot" | "cross" | "circle" | Symbol: the marker's shape. |
| size | "sm" | "md" | "lg" | "md" | Symbol: 22, 28 or 36px for a shape holding an icon; 12, 16 or 20px for a dot or a cross. |
| ink | string | - | Symbol: the icon's colour. Unset, it is picked by contrast (pinInk(color) returns the same pick). |
| halo | boolean | "glow" | "ring" | "dashed" | - | Symbol: a still ring or glow around the shape (true is glow). |
| pulse | "ping" | "alert" | - | Symbol: a live halo that grows out and fades; alert doubles it. Still under reduced motion. |
| caption | React.ReactNode | - | Symbol: words beside or under the shape (a callsign, an incident's title). |
| captionSide | "right" | "bottom" | "right" | Symbol: where the caption sits. |
| Data attribute | Description |
|---|---|
| data-selected | Present when selected. |
| data-visited | Present when visited. |
| data-collapsed | Present while the label is folded below labelMinZoom. |
MapControls
Zoom, a compass that turns with the map, and an optional locate, as a floating vertical Toolbar with tooltips. Forwards native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| position | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "top-right" | The corner of the map it floats in. |
| zoom | boolean | true | Zoom in and out buttons. |
| compass | boolean | true | Turns with the map; resets north and tilt. |
| locate | boolean | false | Fly to the visitor's position (asks for permission). |
| locateZoom | number | 14 | Where to land after locating. |
| onLocate | (result: GeolocationPosition | GeolocationPositionError) => void | - | Called with the position found, or the error. |
MapCorner
A slot floating in a corner of the map, for your own legend or buttons. Forwards native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| position | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "top-right" | The corner of the map it floats in. |
useMap
Returns { map, lib, loaded }: the MapLibre instance (null on the server and the first frames), the MapLibre module for new lib.Popup(), and whether the style has loaded. Use inside Map.
useMapColor
useMapColor(color) resolves a CSS colour ("var(--brand)") for a paint property and updates when the theme changes. readColor(element, color) does it once, outside React.