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/nuxtnpx @unmap/cli add mapSet 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.
Related
- Add a map to Vue for the plain Vue version
- Components for the geocoder, routing panel, and markers
- Examples for the framework-free versions of each job