Raw Map Instance
The components declare map resources. Interacting with them is MapLibre's job. Get the Map and call it directly for layer events, setFilter, feature state, and camera animation.
Get the Instance
ScriptMapLibreMap emits ready with its expose object. The map field is a shallow ref holding the MapLibre Map.
<script setup lang="ts">
import type { Map as MapLibreMap } from 'maplibre-gl'
import type { ShallowRef } from 'vue'
function onMapReady({ map }: { map: ShallowRef<MapLibreMap | undefined> }) {
const instance = map.value
if (!instance)
return
instance.setMaxZoom(18)
}
</script>
<template>
<ScriptMapLibreMap
:center="[144.9631, -37.8136]"
map-style="https://tiles.openfreemap.org/styles/liberty"
@ready="onMapReady"
/>
</template>A template ref works the same way. ScriptMapLibreMap exposes maplibre, map, and load.
ready fires inside MapLibre's load event. MapLibre loads the style first, then fires load. So addSource and addLayer are safe in the handler. You do not need to wait for anything.Re-add Resources After a Style Change
MapLibre drops every source and layer when setStyle runs. Changing the map-style prop calls setStyle, so anything you added on the raw instance disappears. Listen to style.load and add it again.
function onMapReady({ map }: { map: ShallowRef<MapLibreMap | undefined> }) {
const instance = map.value
if (!instance)
return
function addRouteLayer() {
instance!.addSource('route', { type: 'geojson', data: route })
instance!.addLayer({
id: 'route-line',
type: 'line',
source: 'route',
paint: { 'line-color': '#2563eb', 'line-width': 4 },
})
}
addRouteLayer()
instance.on('style.load', addRouteLayer)
}ScriptMapLibreGeoJson restores its own source and layers already. Only the resources you add yourself need this.
Filter a Layer Without Rebuilding It
Changing the layers prop rebuilds the source and its layers. To filter a rendered layer instead, call setFilter on the map.
watch(selectedCategory, (category) => {
const instance = map.value
if (!instance || !instance.getLayer('depot-point'))
return
instance.setFilter('depot-point', category
? ['==', ['get', 'category'], category]
: null)
})Pass null to clear the filter.
Hover and Selection With Feature State
Feature state needs a feature ID. GeoJSON features have no ID by default, so set promoteId or generateId in sourceOptions.
<script setup lang="ts">
const depotSourceOptions = { promoteId: 'depotId' }
</script>
<template>
<ScriptMapLibreGeoJson
source-id="depots"
:data="depots"
:source-options="depotSourceOptions"
:layers="depotLayers"
/>
</template>Write the state on hover, and clear it on leave. <ScriptMapLibreGeoJson> emits mousemove and mouseleave for its own layers, so you can also bind these in the template. See Pointer Events.
let hoveredId: string | number | undefined
instance.on('mousemove', 'depot-point', (event) => {
const id = event.features?.[0]?.id
if (id === undefined)
return
if (hoveredId !== undefined)
instance.setFeatureState({ source: 'depots', id: hoveredId }, { hover: false })
hoveredId = id
instance.setFeatureState({ source: 'depots', id }, { hover: true })
instance.getCanvas().style.cursor = 'pointer'
})
instance.on('mouseleave', 'depot-point', () => {
if (hoveredId !== undefined)
instance.setFeatureState({ source: 'depots', id: hoveredId }, { hover: false })
hoveredId = undefined
instance.getCanvas().style.cursor = ''
})The layer reads it back with a feature-state expression:
const hoverPaint: CircleLayerSpecification['paint'] = {
'circle-radius': ['case', ['boolean', ['feature-state', 'hover'], false], 11, 7],
}0 and not ''. MapLibre paints a falsy ID correctly, but queryRenderedFeatures reports its state as empty. loadMatchingFeature guards that lookup with a truthiness check. So any handler reading event.features[0].state sees nothing, and state-dependent hit testing misses. If you promote a zero-based index, offset it by one.Read the Features in View
queryRenderedFeatures returns what MapLibre currently draws. Use it to keep a side list in sync with the viewport.
instance.on('moveend', () => {
const visible = instance.queryRenderedFeatures({ layers: ['depot-point'] })
visibleDepots.value = visible.map(feature => feature.properties.depotId as string)
})queryRenderedFeatures reads rendered tiles. A feature clipped at a tile edge can come back more than once.
Animate the Camera
The center, zoom, bearing, pitch, and bounds props jump the camera. If you want an animation, call the map. To frame the data without animation, use the bounds prop instead.
instance.fitBounds([[144.94, -37.83], [144.97, -37.81]], {
padding: 48,
duration: 600,
})
instance.easeTo({ center: [144.9631, -37.8136], zoom: 15, duration: 400 })fitBounds takes [[west, south], [east, north]]. Use flyTo for a long move that should arc out and back in.
Clean Up Listeners
map.on() returns a subscription in MapLibre v6. Store it and unsubscribe when the component unmounts.
const subscription = instance.on('click', 'depot-point', onDepotClick)
onBeforeUnmount(() => subscription.unsubscribe())Performance, CSP & Accessibility
ScriptMapLibreMap loads near the viewport by default and reserves its configured dimensions during SSR. The SDK, stylesheet, Web Worker, style, and tiles stay off the network until the trigger runs.
<ScriptMapLibreMap>
The map facade reserves layout space during SSR, loads MapLibre when triggered, creates the Map, and provides it to child components.