Skip to content

Vue 3 Quickstart

Install

bash
npm install maplibre-gl

Component

vue
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue';
import maplibregl, { type Map } from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';

const el = ref<HTMLDivElement>();
let map: Map | null = null;

const KEY = import.meta.env.VITE_MFD_PUB_KEY;

onMounted(() => {
  if (!el.value) return;
  map = new maplibregl.Map({
    container: el.value,
    style: `https://api.mapsfordevs.com/styles/standard.json?key=${KEY}`,
    center: [28.04, -26.20],
    zoom: 11
  });
});

onBeforeUnmount(() => {
  map?.remove();
  map = null;
});
</script>

<template>
  <div ref="el" class="w-full h-screen" />
</template>

Composable

ts
// composables/useMapLibre.ts
import { onMounted, onBeforeUnmount, ref, type Ref } from 'vue';
import maplibregl, { type MapOptions, type Map } from 'maplibre-gl';

export function useMapLibre(container: Ref<HTMLElement | undefined>, opts: Omit<MapOptions, 'container'>) {
  const map = ref<Map | null>(null);

  onMounted(() => {
    if (!container.value) return;
    map.value = new maplibregl.Map({ container: container.value, ...opts });
  });

  onBeforeUnmount(() => {
    map.value?.remove();
    map.value = null;
  });

  return { map };
}

Usage:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { useMapLibre } from '@/composables/useMapLibre';

const el = ref<HTMLDivElement>();
const { map } = useMapLibre(el, {
  style: `https://api.mapsfordevs.com/styles/standard.json?key=${import.meta.env.VITE_MFD_PUB_KEY}`,
  center: [28.04, -26.20],
  zoom: 11
});
</script>
<template><div ref="el" class="w-full h-screen" /></template>

Language switcher (vue-i18n integration)

vue
<script setup lang="ts">
import { watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useMapLibreContext } from '@/composables/useMapLibre';

const { locale } = useI18n();
const { map } = useMapLibreContext();

watch([locale, map], ([newLocale, newMap]) => {
  if (!newMap) return;
  const apply = () => {
    ['place_label', 'transportation_name', 'poi_label'].forEach(layer => {
      newMap.setLayoutProperty(layer, 'text-field', [
        'coalesce',
        ['get', `name:${newLocale}`],
        ['get', 'name:latin'],
        ['get', 'name']
      ]);
    });
  };
  newMap.isStyleLoaded() ? apply() : newMap.once('styledata', apply);
});
</script>

Pinia store for shared map state

ts
// stores/map.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
import type { Map } from 'maplibre-gl';

export const useMapStore = defineStore('map', () => {
  const instance = ref<Map | null>(null);
  const setMap = (m: Map) => { instance.value = m; };
  return { instance, setMap };
});

Geocoding composable

ts
// composables/useGeocode.ts
import { ref } from 'vue';

export function useGeocode(apiKey: string) {
  const results = ref<any[]>([]);
  const loading = ref(false);

  async function search(q: string) {
    loading.value = true;
    try {
      const r = await fetch(
        `https://api.mapsfordevs.com/geocode/forward?q=${encodeURIComponent(q)}&limit=5`,
        { headers: { Authorization: `Bearer ${apiKey}` } }
      );
      const body = await r.json();
      if (body.ok) results.value = body.items;
    } finally {
      loading.value = false;
    }
  }

  return { results, loading, search };
}