Add a map to Vue

Vue has several MapLibre component libraries, and they solve the renderer half. unmap is the other half: the tiles, styles, geocoding and routing the renderer draws, on one key. The composable hands you the MapLibre map object rather than wrapping it, so you can use either alongside the other.

On Nuxt, read Add a map to Nuxt instead: auto-imports and SSR change the answer.

Install

Registry copies an UnmapMap.vue into your project through shadcn-vue, where it follows your design tokens and you own the file. It needs a project with a components.json.

Package installs @unmap/vue and gives you composables, which is the right layer when the map goes inside UI you have already built.

npx @unmap/cli add map

The component

<script setup lang="ts">
import { UnmapMap } from "@/components/ui/map";
 
const apiKey = import.meta.env.VITE_UNMAP_KEY as string;
</script>
 
<template>
  <div class="h-dvh">
    <UnmapMap :api-key="apiKey" :center="[-114.07, 51.05]" :zoom="11" class="h-full" />
  </div>
</template>

The key is spelled differently on each path, and that is not an accident. The copied component takes api-key, because it is a component and Vue reserves key as a special attribute. The composable takes key, matching the SDK option everywhere else. Passing apiKey to the composable gets you an unauthenticated map and a 401.

On the package path, import maplibre-gl/dist/maplibre-gl.css once in your entry file. The copied component does that for you.

What the composable gives back

mapContainer is a template ref you bind to the element. map is a shallowRef holding the MapLibre map itself, null until the client has mounted, and ready flips true once the map has been created. Watch ready rather than map if you need the earliest safe point to add a source.

<script setup lang="ts">
import { watch } from "vue";
import { useUnmap } from "@unmap/vue";
 
const { mapContainer, map, ready } = useUnmap({
  key: import.meta.env.VITE_UNMAP_KEY as string,
  center: [-114.07, 51.05],
  zoom: 11,
});
 
watch(ready, (isReady) => {
  if (!isReady || !map.value) return;
  map.value.addSource("sites", { type: "geojson", data: { type: "FeatureCollection", features: [] } });
  map.value.addLayer({ id: "sites", type: "circle", source: "sites" });
});
</script>
 
<template>
  <div ref="mapContainer" style="height: 100dvh" />
</template>

The composable is SSR-safe on its own: the map is created inside onMounted behind a typeof window guard, and the maplibre-gl bundle is imported lazily so importing the composable during SSR pulls in nothing that touches the DOM.

Where the key lives

Vite inlines anything prefixed VITE_ into the browser bundle. That is correct for a map key: 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.

# .env
VITE_UNMAP_KEY=um_live_...

Common problems

The map is a blank box. The container has no height. MapLibre measures the element it is given, and an element of zero pixels reports no error.

A 401 on every tile. You passed apiKey to the composable, which takes key. See above.

map.value is null in onMounted. The map is created inside the composable's own onMounted, and ordering between the two is not guaranteed. Watch ready instead.