Use unmap basemaps with mapcn
mapcn is a set of shadcn-style React map components on MapLibre: a <Map>,
controls, markers, popups and routes, copied into your project like any shadcn component. unmap
gives those components a Canadian-built basemap, in twelve styles, with French labels on request.
The whole change is two style URLs passed to <Map>. Your components, markers and layers stay
exactly as they are.
Install
Add mapcn's map component, which copies components/ui/map.tsx into your project and installs
maplibre-gl:
npx shadcn@latest add @mapcn/mapThen get a key from the dashboard and put it in your public env file:
# .env.local
NEXT_PUBLIC_UNMAP_KEY=um_live_...A key used by a map in a browser is readable by anyone who loads the page, at every maps vendor.
Restrict it to your production origins in the dashboard rather than trying to hide it, and use a
um_test_ key while you develop. On Vite the same key is VITE_UNMAP_KEY, read from
import.meta.env. See Authentication.
Point the map at unmap
One small helper builds the style URL, so the key and the style name live in one place:
// lib/unmap.ts
const key = process.env.NEXT_PUBLIC_UNMAP_KEY;
export function unmapStyle(mode: "light" | "dark", style = "base") {
return `https://api.unmap.dev/styles/${style}.json?key=${key}&mode=${mode}`;
}Pass one URL per mode to mapcn's styles prop:
// components/city-map.tsx
import { Map, MapControls } from "@/components/ui/map";
import { unmapStyle } from "@/lib/unmap";
const styles = { light: unmapStyle("light"), dark: unmapStyle("dark") };
export function CityMap() {
return (
<div className="h-[420px] w-full">
<Map styles={styles} center={[-114.0719, 51.0447]} zoom={11}>
<MapControls />
</Map>
</div>
);
}That is the whole integration. mapcn picks the mode itself: it watches for a dark or light
class, or a data-theme attribute, on <html>, falls back to the operating system's preference,
and swaps between your two URLs when either changes. center and zoom are MapLibre options that
<Map> passes straight through.
There is no transformRequest to write. The gateway copies the key that fetched the style into
every tile, glyph and sprite URL inside it, so MapLibre authenticates each request on its own.
The URL takes the rest of what an unmap map can do:
- Style.
baseis the default. The others aremuted,outdoor,blueprint,blush,orchid,canopy,lagoon,tropic,sunset,boldandpastel, each in both modes, sounmapStyle("dark", "outdoor")is a dark outdoor map. Every one is on Maps API. - Language. Append
&lang=frfor French labels. Without it, the gateway reads the browser'sAccept-Language, so a reader whose browser prefers French gets French labels with no code. - Theme. Append
&theme=<code>with a code from /create to apply your own cartography. This is unrelated to mapcn'sthemeprop, which forces its light or dark choice.
The style carries its own attribution (unmap and OpenStreetMap contributors), and mapcn's compact attribution control shows it. Keep it visible: the OpenStreetMap licence requires it.
If your site sends a strict Content Security Policy, add https://api.unmap.dev to connect-src
beside the worker entries mapcn's own installation page
lists.
Routes and search
mapcn's <MapRoute> draws any list of [lng, lat] pairs, and its markers take a longitude and a
latitude. unmap's typed clients return exactly those, so you keep mapcn's components and call ours
for the data:
npm i @unmap/routing @unmap/geocodingIn a Next.js App Router page, a Server Component can make both calls before it renders:
// app/directions/page.tsx
import { Geocoder } from "@unmap/geocoding";
import { Router } from "@unmap/routing";
import { Map, MapControls, MapMarker, MarkerContent, MarkerPopup, MapRoute } from "@/components/ui/map";
import { unmapStyle } from "@/lib/unmap";
// Server-side calls send no Origin header, so they use an unrestricted server key.
const key = process.env.UNMAP_API_KEY ?? "";
const geocoder = new Geocoder({ key });
const router = new Router({ key });
export default async function Directions() {
const [[from], [to]] = await Promise.all([
geocoder.search("Calgary Tower", { limit: 1 }),
geocoder.search("Calgary Zoo", { limit: 1 }),
]);
const route = await router.route([from.lng, from.lat], [to.lng, to.lat], { mode: "auto" });
return (
<div className="h-[420px] w-full">
<Map styles={{ light: unmapStyle("light"), dark: unmapStyle("dark") }} center={[from.lng, from.lat]} zoom={12}>
<MapRoute coordinates={route.geometry.coordinates} />
{[from, to].map((place) => (
<MapMarker key={place.id} longitude={place.lng} latitude={place.lat}>
<MarkerContent />
<MarkerPopup>{place.name}</MarkerPopup>
</MapMarker>
))}
<MapControls />
</Map>
</div>
);
}route.geometry is a GeoJSON LineString, so its coordinates go into <MapRoute> unchanged.
Each geocoding result carries id, name, lng and lat, and geocoder.autocomplete(q) returns
the same shape for a search box. Keep this server key out of any NEXT_PUBLIC_ variable. Outside
Next.js, or in a client component, make the same two calls in an effect with the browser key.
mode also takes car, bicycle, pedestrian and truck. The Routing API and
Geocoding API pages have every option.
What a request costs
unmap meters per request. Each style document is one call, and so is each tile, each geocoding
query and each route; glyphs and sprites are never billed. A map that loads 40 tiles costs 40
calls, the same as 40 autocomplete keystrokes, and switching between light and dark fetches the
other style document. Requests from localhost and other dev origins are counted but never billed.
A live domain needs a paid plan, because a Hobby key lists dev origins only. The numbers are on
Plans & Limits.
mapcn's default basemap is CARTO's. mapcn's
README notes that commercial use
of CARTO basemaps requires a CARTO Enterprise licence; the details are in CARTO's
basemap terms. Passing your own styles, as above, replaces that
default entirely.
When to use the unmap registry instead
mapcn is a good fit when you want its components and only need a basemap. unmap also publishes its own shadcn registry at registry.unmap.dev, and it is the better fit when:
- You build in Vue or Nuxt. mapcn is React only. The unmap registry has a shadcn-vue lane with the same items.
- You want search and directions prebuilt. The
geocoderandrouting-panelitems are already wired to the gateway, on your own shadcn Combobox, Field, Input, Button and Alert. - You want the map to follow your design system. MapLibre's controls and popups take your
tokens, markers and routes default to your
--primary, and the style and theme are props rather than a URL you build.
Start with npx @unmap/cli add map. Components has the full list and
both install paths.