Map

An 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#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add map

Usage#

usage.tsxTSX
import { Map, MapControls, MapMarker, MapPin } from "@/components/ui/map"export function Example() {  return (    <Map center={[-73.9855, 40.758]} zoom={13} className="h-96">      <MapMarker lngLat={[-73.9855, 40.758]}>        <MapPin label="Times Square" />      </MapMarker>      <MapControls />    </Map>  )}

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.

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). Name the landmarks and leave the small places bare, and the map reads big to small. The emoji is decorative: the label names the pin. selected and visited work as on every pin.

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.

2.9 km · 12 min

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 region that hosts the map. Camera: center ([lng, lat]), zoom, bearing, pitch, minZoom, maxZoom, maxBounds; they seed the map once. Behaviour: interactive, cooperativeGestures, options (any other MapLibre option), workerUrl (where MapLibre's worker loads from). Basemap: labels, buildings, mapStyle. frame (rounded · flush), label, and the callbacks onLoad(map), onMoveEnd(view), onMapClick(lngLat, map).

MapMarker

lngLat (updating it moves the marker), anchor, offset, flat, and any content as children.

MapPin

label, variant (default · price · emoji), emoji (the emoji variant's mark), tone (brand · neutral · info · success · warning · destructive · purple · teal; the dot's colour, so the price and emoji variants have none), selected, visited, and every button prop.

MapControls · MapCorner

position (top-left · top-right · bottom-left · bottom-right). MapControls adds zoom, compass, locate, locateZoom and onLocate.

useMap · useMapColor · readColor

useMap() returns { map, lib, loaded }: the MapLibre instance, the MapLibre module (for new lib.Popup() and the like) and whether the style has loaded. useMapColor(color) resolves a CSS colour to rgb for a paint property and updates when the theme changes; readColor(element, color) does it once, outside React.

FAQ#

No. The default basemap uses OpenFreeMap's OpenStreetMap vector tiles, which are free and keyless. Keep the attribution: it is what the data licence asks for. For heavy production traffic, point mapStyle at your own tiles or a paid provider.