Skip to content

Controls

Learn how to add navigation controls, geolocation, and custom controls to your map.

Add standard navigation controls to your map:

vue
<template>
  <Maplibre :options="mapOptions" style="height: 400px;">
    <NavigationControl position="top-right" />
    <ScaleControl position="bottom-left" />
    <FullscreenControl position="top-left" />
  </Maplibre>
</template>

<script setup>
import { ref } from 'vue';
import {
  Maplibre,
  NavigationControl,
  ScaleControl,
  FullscreenControl,
} from 'vue3-maplibre-gl';

const mapOptions = ref({
  style: 'https://demotiles.maplibre.org/style.json',
  center: [0, 0],
  zoom: 2,
});
</script>

Geolocation Control

Add geolocation functionality:

vue
<template>
  <div>
    <div class="status" v-if="locationStatus">
      <p>{{ locationStatus }}</p>
    </div>

    <Maplibre :options="mapOptions" style="height: 400px;">
      <GeolocateControl
        position="top-right"
        :track-user-location="true"
        :show-accuracy-circle="true"
        @geolocate="onGeolocate"
        @error="onGeolocationError"
      />
    </Maplibre>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { Maplibre, GeolocateControl } from 'vue3-maplibre-gl';

const mapOptions = ref({
  style: 'https://demotiles.maplibre.org/style.json',
  center: [0, 0],
  zoom: 2,
});

const locationStatus = ref('');

function onGeolocate(event) {
  const { latitude, longitude, accuracy } = event.coords;
  locationStatus.value = `Located at: ${latitude.toFixed(4)}, ${longitude.toFixed(4)} (±${Math.round(accuracy)}m)`;
}

function onGeolocationError(error) {
  locationStatus.value = `Geolocation error: ${error.message}`;
}
</script>

<style scoped>
.status {
  padding: 10px;
  background: #f0f8ff;
  border: 1px solid #007cbf;
  border-radius: 4px;
  margin-bottom: 10px;
  font-family: monospace;
}
</style>

Custom Control

Create a custom control component:

vue
<template>
  <div>
    <Maplibre :options="mapOptions" style="height: 400px;" @load="onMapLoad">
      <!-- Built-in controls -->
      <NavigationControl position="top-right" />

      <!-- Custom control using slot -->
      <div class="custom-control-container">
        <div class="custom-control">
          <button @click="goToRandomLocation" title="Random Location">
            🎲
          </button>
          <button @click="resetView" title="Reset View">🏠</button>
          <button @click="toggleStyle" title="Toggle Style">🎨</button>
        </div>
      </div>
    </Maplibre>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { Maplibre, NavigationControl } from 'vue3-maplibre-gl';

const mapOptions = ref({
  style: 'https://demotiles.maplibre.org/style.json',
  center: [0, 0],
  zoom: 2,
});

const mapInstance = ref(null);
const isDarkStyle = ref(false);

const styles = {
  light: 'https://demotiles.maplibre.org/style.json',
  dark: {
    version: 8,
    sources: {
      'dark-tiles': {
        type: 'raster',
        tiles: [
          'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png',
        ],
        tileSize: 256,
        attribution: '© CartoDB',
      },
    },
    layers: [
      {
        id: 'dark-layer',
        type: 'raster',
        source: 'dark-tiles',
      },
    ],
  },
};

function onMapLoad(map) {
  mapInstance.value = map;
}

function goToRandomLocation() {
  if (!mapInstance.value) return;

  const randomLng = (Math.random() - 0.5) * 360;
  const randomLat = (Math.random() - 0.5) * 180;

  mapInstance.value.flyTo({
    center: [randomLng, randomLat],
    zoom: Math.random() * 10 + 2,
    duration: 2000,
  });
}

function resetView() {
  if (!mapInstance.value) return;

  mapInstance.value.flyTo({
    center: [0, 0],
    zoom: 2,
    duration: 1000,
  });
}

function toggleStyle() {
  if (!mapInstance.value) return;

  isDarkStyle.value = !isDarkStyle.value;
  const newStyle = isDarkStyle.value ? styles.dark : styles.light;
  mapInstance.value.setStyle(newStyle);
}
</script>

<style scoped>
.custom-control-container {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 1000;
}

.custom-control {
  background: white;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  display: flex;
  flex-direction: column;
}

.custom-control button {
  width: 40px;
  height: 40px;
  border: none;
  background: white;
  cursor: pointer;
  font-size: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.2s;
}

.custom-control button:hover {
  background: #f0f0f0;
}

.custom-control button:first-child {
  border-radius: 4px 4px 0 0;
}

.custom-control button:last-child {
  border-radius: 0 0 4px 4px;
}

.custom-control button:not(:last-child) {
  border-bottom: 1px solid #eee;
}
</style>

Layer Control

Create a control to toggle map layers:

vue
<template>
  <div>
    <Maplibre :options="mapOptions" style="height: 400px;">
      <GeoJsonSource :data="layerData" source-id="toggleable-layers">
        <FillLayer v-if="layers.fill.visible" :style="fillStyle" />
        <CircleLayer v-if="layers.circles.visible" :style="circleStyle" />
        <LineLayer v-if="layers.lines.visible" :style="lineStyle" />
      </GeoJsonSource>

      <!-- Layer control panel -->
      <div class="layer-control-panel">
        <h4>Layers</h4>
        <div class="layer-item" v-for="(layer, key) in layers" :key="key">
          <label>
            <input type="checkbox" v-model="layer.visible" />
            {{ layer.name }}
          </label>
          <input
            type="range"
            min="0"
            max="1"
            step="0.1"
            v-model="layer.opacity"
            :disabled="!layer.visible"
          />
          <span class="opacity-value"
            >{{ Math.round(layer.opacity * 100) }}%</span
          >
        </div>
      </div>
    </Maplibre>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';
import {
  Maplibre,
  GeoJsonSource,
  FillLayer,
  CircleLayer,
  LineLayer,
} from 'vue3-maplibre-gl';

const mapOptions = ref({
  style: 'https://demotiles.maplibre.org/style.json',
  center: [-74.006, 40.7128],
  zoom: 11,
});

const layers = ref({
  fill: {
    name: 'Areas',
    visible: true,
    opacity: 0.6,
  },
  circles: {
    name: 'Points',
    visible: true,
    opacity: 0.8,
  },
  lines: {
    name: 'Routes',
    visible: true,
    opacity: 0.7,
  },
});

const layerData = ref({
  type: 'FeatureCollection',
  features: [
    // Polygon
    {
      type: 'Feature',
      geometry: {
        type: 'Polygon',
        coordinates: [
          [
            [-74.0059, 40.7128],
            [-74.0059, 40.7589],
            [-73.9352, 40.7589],
            [-73.9352, 40.7128],
            [-74.0059, 40.7128],
          ],
        ],
      },
      properties: { type: 'area' },
    },
    // Points
    {
      type: 'Feature',
      geometry: { type: 'Point', coordinates: [-74.006, 40.7128] },
      properties: { type: 'point' },
    },
    {
      type: 'Feature',
      geometry: { type: 'Point', coordinates: [-73.9857, 40.7589] },
      properties: { type: 'point' },
    },
    // Line
    {
      type: 'Feature',
      geometry: {
        type: 'LineString',
        coordinates: [
          [-74.006, 40.7128],
          [-73.9857, 40.7589],
          [-73.9665, 40.7812],
        ],
      },
      properties: { type: 'route' },
    },
  ],
});

const fillStyle = computed(() => ({
  'fill-color': '#088',
  'fill-opacity': layers.value.fill.opacity,
}));

const circleStyle = computed(() => ({
  'circle-radius': 8,
  'circle-color': '#ff6b6b',
  'circle-opacity': layers.value.circles.opacity,
  'circle-stroke-width': 2,
  'circle-stroke-color': '#ffffff',
}));

const lineStyle = computed(() => ({
  'line-color': '#007cbf',
  'line-width': 4,
  'line-opacity': layers.value.lines.opacity,
}));
</script>

<style scoped>
.layer-control-panel {
  position: absolute;
  top: 10px;
  right: 10px;
  background: white;
  padding: 15px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  min-width: 200px;
  z-index: 1000;
}

.layer-control-panel h4 {
  margin: 0 0 10px 0;
  color: #333;
  font-size: 14px;
}

.layer-item {
  margin-bottom: 10px;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.layer-item label {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  font-size: 13px;
}

.layer-item input[type='range'] {
  width: 100%;
}

.opacity-value {
  font-size: 11px;
  color: #666;
  text-align: right;
}
</style>

Attribution Control

Add custom attribution:

vue
<template>
  <Maplibre :options="mapOptions" style="height: 400px;">
    <AttributionControl position="bottom-right" :compact="false" />
  </Maplibre>
</template>

<script setup>
import { ref } from 'vue';
import { Maplibre, AttributionControl } from 'vue3-maplibre-gl';

const mapOptions = ref({
  style: {
    version: 8,
    sources: {
      osm: {
        type: 'raster',
        tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
        tileSize: 256,
        attribution:
          '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
      },
    },
    layers: [
      {
        id: 'osm-layer',
        type: 'raster',
        source: 'osm',
      },
    ],
  },
  center: [0, 0],
  zoom: 2,
});
</script>

Key Features

  • Built-in Controls: Navigation, scale, fullscreen, and geolocation
  • Custom Controls: Create your own control components
  • Layer Management: Toggle and control layer visibility
  • Positioning: Flexible control positioning
  • Event Handling: Respond to control interactions

Released under the MIT License.