---
title: "Raw Map Instance"
description: "Call the MapLibre Map directly for layer events, filtering, hover state, and camera animation."
canonical_url: "https://scripts.nuxt.com/scripts/maplibre/guides/raw-map-instance"
last_updated: "2026-09-21T15:42:48.976Z"
---

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`.

```vue
<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`.

::callout{icon="i-heroicons-information-circle"}
`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.

```ts
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.

```ts
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`.

```vue
<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>`{lang="html"} emits `mousemove` and `mouseleave` for its own layers, so you can also bind these in the template. See [Pointer Events](/scripts/maplibre/api/geojson#pointer-events).

```ts
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:

```ts
const hoverPaint: CircleLayerSpecification['paint'] = {
  'circle-radius': ['case', ['boolean', ['feature-state', 'hover'], false], 11, 7],
}
```

::callout{icon="i-heroicons-exclamation-triangle" color="amber"}
Give every feature an ID that is not `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.

```ts
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](/scripts/maplibre/api/script-maplibre-map#frame-the-data) instead.

```ts
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()`{lang="ts"} returns a subscription in MapLibre v6. Store it and unsubscribe when the component unmounts.

```ts
const subscription = instance.on('click', 'depot-point', onDepotClick)

onBeforeUnmount(() => subscription.unsubscribe())
```

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
