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 mapnpm i @unmap/vue maplibre-glThe 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><script setup lang="ts">
import { useUnmap } from "@unmap/vue";
const { mapContainer } = useUnmap({
key: import.meta.env.VITE_UNMAP_KEY as string,
center: [-114.07, 51.05],
zoom: 11,
});
</script>
<template>
<div ref="mapContainer" style="height: 100dvh" />
</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.
Related
- Add a map to Nuxt for the SSR version
- Components for the geocoder, routing panel, and markers
- Examples for the framework-free versions of each job