Add a map to Nuxt

Nuxt renders on the server first, and a map cannot. The usual answer is <ClientOnly> around everything that touches MapLibre. @unmap/nuxt narrows that: the composables are safe to import during SSR, so only the element that mounts the canvas needs guarding, and the module puts your key on runtime config where it belongs.

Install

Module registers @unmap/nuxt, which auto-imports the composables into every component and publishes the key to public runtime config.

Registry copies an UnmapMap.vue into your project through shadcn-vue. With shadcn-nuxt it is auto-imported as <UiUnmapMap>, and it follows your design tokens.

npx nuxi module add @unmap/nuxt

Set the key

The module reads unmap.key from nuxt.config.ts, and NUXT_PUBLIC_UNMAP_KEY overrides it at runtime. Prefer the environment variable: it keeps the key out of the repository and lets staging and production differ without a rebuild.

# .env
NUXT_PUBLIC_UNMAP_KEY=um_live_...

This value reaches the browser, which is correct for a map key and worth being clear-eyed about: 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.

The page

useUnmap is auto-imported by the module, so the import line below is optional. It is written out because an explicit import is still valid Nuxt, and because it is the line that tells a reader where the composable comes from.

<script setup lang="ts">
import { useUnmap } from "@unmap/vue";
 
const config = useRuntimeConfig();
const { mapContainer } = useUnmap({
  key: config.public.unmap.key,
  center: [-114.07, 51.05],
  zoom: 11,
});
</script>
 
<template>
  <ClientOnly>
    <div ref="mapContainer" style="height: 100dvh" />
  </ClientOnly>
</template>

<ClientOnly> wraps the container, not the script. The composable itself is SSR-safe: it creates the map inside onMounted behind a typeof window guard and imports the maplibre-gl bundle lazily, so nothing touches the DOM on the server. Wrapping the element as well avoids a hydration mismatch on the container's own attributes.

What is checked here and what is not

The snippets on this page compile against the real package types, and config.public.unmap is checked against the shape the module actually publishes. Two things are not: Vue templates, which need the Vue language service rather than tsc, and Nuxt's own built-ins, which are stubbed. A typo in a :prop binding will still reach you rather than a test.

That is worth knowing before you copy a template wholesale from anywhere, this page included.

Common problems

window is not defined during build. Something imported maplibre-gl at module scope. Import it inside onMounted, or let the composable do it for you.

A hydration mismatch on the map container. The element rendered on the server and then changed on the client. <ClientOnly> around the container fixes it.

The key is empty and every tile 401s. NUXT_PUBLIC_UNMAP_KEY was not set in the environment that built or served the app. The module trims to an empty string rather than throwing, so the failure surfaces at the first request instead of at boot.