Skip to content

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/map

Then 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. base is the default. The others are muted, outdoor, blueprint, blush, orchid, canopy, lagoon, tropic, sunset, bold and pastel, each in both modes, so unmapStyle("dark", "outdoor") is a dark outdoor map. Every one is on Maps API.
  • Language. Append &lang=fr for French labels. Without it, the gateway reads the browser's Accept-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's theme prop, 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.

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/geocoding

In 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 geocoder and routing-panel items 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.