Map and markers
map mounts a MapLibre map on the unmap gateway and shares it with every component placed inside it. Markers, popups and controls go inside it and follow your design tokens. Components covers installing, the two lanes and styling.
npx shadcn@latest registry add @unmap=https://registry.unmap.dev/r/{name}.json
npx shadcn@latest add @unmap/mapnpx shadcn-vue@latest add https://registry.unmap.dev/r/vue/map.jsonThe React line registers the registry once per project, so later items can be named @unmap/<item>; shadcn-vue has no registry add and takes the item URL directly. Files land in components/ui/ beside your own; nothing is overwritten without asking.
Browse every item on registry.unmap.dev| Item | What it is |
|---|---|
map-context | The shared map store the other items depend on. Installed automatically. |
map-themes | Named short theme presets (u00 and up) for the map's theme prop. Compose your own at unmap.dev/create. |
map | The map display component. Mounts a container, creates a MapLibre map on the unmap gateway, and shares it with its children. Restyles MapLibre's popups and controls onto your tokens. |
marker-popup | Declarative markers, popups, hover tooltips and labels positioned on the map. Pins can be dragged. |
map-controls | Zoom, compass, locate, fullscreen, globe and 3D terrain buttons on your own ButtonGroup, Button and Tooltip. Pair with controls={false} on the map. |
Markers and interaction
A <Marker> can be dragged, labelled and given a hover tooltip. draggable lets the reader move
the pin and onDragEnd (Vue: @dragend) receives where it was dropped. The pin stays there, and
lngLat moves it again whenever its coordinates change, so write the drop back into state if you
keep one. To reject a drop, set lngLat to a different value: the same value is not a change and
leaves the pin where it was dropped. <MarkerLabel> writes text beside the pin (position is
right, left, top or bottom), <MarkerTooltip> shows a small popup while the pointer is over
it, and <Popup trigger="hover"> opens a full popup the same way.
import { Map } from "@/components/ui/map";
import { Marker, MarkerLabel, MarkerTooltip, Popup } from "@/components/ui/marker-popup";
<Map apiKey={key} center={[-114.07, 51.05]} zoom={11} maxBounds="canada" minZoom={3} hash>
<Marker lngLat={site} draggable onDragEnd={setSite}>
<MarkerLabel>Site A</MarkerLabel>
<MarkerTooltip>Drag to move</MarkerTooltip>
<Popup>Details for site A</Popup>
</Marker>
</Map>Try it, then add it as a starting point:
React projects. Installs components/unmap-map-markers.tsx; render <MapMarkers apiKey={key} />.
The same in Vue:
<script setup lang="ts">
import { UnmapMap } from "@/components/ui/map";
import { UnmapMarker, UnmapMarkerLabel, UnmapMarkerTooltip, UnmapPopup } from "@/components/ui/marker-popup";
const apiKey = import.meta.env.VITE_UNMAP_KEY!;
const site: [number, number] = [-114.07, 51.05];
function onDragEnd(lngLat: [number, number]) {
console.log(lngLat);
}
</script>
<template>
<UnmapMap :api-key="apiKey" :center="[-114.07, 51.05]" :zoom="11" max-bounds="canada" :min-zoom="3" hash>
<UnmapMarker :lng-lat="site" draggable @dragend="onDragEnd">
<UnmapMarkerLabel>Site A</UnmapMarkerLabel>
<UnmapMarkerTooltip>Drag to move</UnmapMarkerTooltip>
<UnmapPopup>Details for site A</UnmapPopup>
</UnmapMarker>
</UnmapMap>
</template>- Hover means the pointer over the pin or keyboard focus on it; a press does not count. Touch
screens have no hover, so a tooltip never shows there, and a pin with a tooltip but no popup
cannot take focus unless it is
draggable(a default pin): keep anything a reader needs in a label or a popup. A hover popup still opens on a tap, and a click or Enter toggles it on any screen. It closes when the pointer or focus leaves the pin, so its content cannot be reached: keep it read-only, drop the close button (it defaults to off), and use the default click trigger for anything interactive. The tooltip follows the pin when it moves, steps aside while the pin's popup is open, and never shows beside a hover popup, which opens on the same hover. interactive={false}(Vue::interactive="false") turns off panning, zooming, rotating and the keyboard, for a map that is a picture. Pins, draggable ones included, popups and the zoom buttons keep working. Turning it back on respects anything the rawmaplibreoption switched off, such asdragRotate: false.cooperativeGestures(Vue:cooperative-gestures) asks for Ctrl or Cmd with the scroll wheel, and two fingers on a phone, so a map in a long page does not trap the scroll.maxBoundskeeps the camera inside[[west, south], [east, north]], or inside all of Canada with a margin when it is"canada".minZoomandmaxZoombound the zoom. Each of these falls back to the rawmaplibreoption when its prop is unset. A zoom limit MapLibre would reject (aminZoombelow -2 or above the max zoom, amaxZoombelow -2, or one that is not a finite number) is dropped with a console warning rather than throwing, and an invertedminZoomandmaxZoompair drops both. AmaxZoomabove 22 is kept as given. When a limit moves the camera,onViewportChange(Vue:@viewport-change) reports where it went.hashkeeps the camera in the page URL (#zoom/lat/lng), so a reload or a shared link opens the same view, and a position already in the URL wins overcenterandzoom. Give it a name (hash="map") to keep the rest of the hash, or to put two maps in one URL. Turning it off, or removing the map, takes the map's part out of the URL. A map recreated by a newapiKeystarts from the props' camera.- Every one of these changes in place, without rebuilding the map. The tooltip's compact style
comes with the
mapitem. Tooltips carry theunmap-tooltipclass and labels adata-unmap-marker-labelattribute, as hooks for your own CSS.
Map controls
map-controls puts zoom, compass, locate, fullscreen, globe and 3D terrain buttons on the map,
built on your ButtonGroup, Button and Tooltip, with icons from your icon library. Turn the
map's own buttons off with controls={false} so the two sets do not stack in the same corner. The
attribution stays: the OpenStreetMap licence requires it.
import { Map } from "@/components/ui/map";
import { MapControls } from "@/components/ui/map-controls";
<Map apiKey={key} style="outdoor" center={[-115.57, 51.18]} zoom={11} controls={false}>
<MapControls position="top-right" locate fullscreen globe terrain />
</Map><script setup lang="ts">
import { UnmapMap } from "@/components/ui/map";
import { UnmapMapControls } from "@/components/ui/map-controls";
const apiKey = import.meta.env.VITE_UNMAP_KEY!;
</script>
<template>
<UnmapMap :api-key="apiKey" map-style="outdoor" :center="[-115.57, 51.18]" :zoom="11" :controls="false">
<UnmapMapControls position="top-right" locate fullscreen globe terrain />
</UnmapMap>
</template>- Zoom and the compass are on by default;
locate,fullscreen,globeandterrainare opt-in, andzoom={false}orcompass={false}turns a default off.positiontakes any corner. Zoom in and zoom out are disabled at the map's zoom limits. - The compass needle follows the bearing, and a press turns the map back to north and flat.
locateasks the browser for the reader's position, flies there and marks it with a dot in your--primary. When the browser refuses, cannot find a position or has no location at all, anAlertunder the buttons says so. PasslocateAlert={false}and handleonLocateError(Vue:@locate-error) to show your own message instead.onLocate(Vue:@locate) receives the position. Geolocation needs HTTPS.fullscreenfills the screen with the map and its controls. Where the page cannot go fullscreen, such as an iframe without permission or Safari on iPhone, the map fills the window instead, and Escape leaves it. Anything that opens in a portal, such as these tooltips, the geocoder's suggestions or a select, stays outside the fullscreen map in both cases: native fullscreen shows only the map's element, and the map that fills the window covers the rest of the page at MapLibre's z-index of 99999. Every button keeps its accessible name. To bring a portal along, point itscontainer(Reka UI:to) at the map's element,map.getContainer(), in your primitive.globeswitches between the flat map and the globe. The button shows what the map shows, so it agrees with theprojectionprop, and the choice survives a change of mode, style or theme.terraintilts the map over 3D terrain. It appears only on a style that carries elevation, such asoutdoor; DEM extent follows the active release in /coverage.json. It stays on across a style change and comes back on the next style with elevation. Terrain loads elevation tiles, and each one is a request, like any tile.lang="fr"names the buttons and writes the messages in French. Toggles carryaria-pressed, and every button has a name and a tooltip.