maplibre-layers

discountedcookie's avatarfrom discountedcookie

Use when adding map layers, managing GeoJSON sources, styling markers, or handling layer interactions. Load for layer registration patterns, markRaw usage, MglGeoJsonSource components, data-driven styling, and layer event handling. Covers the mapLayers store and dynamic layer composition.

⭐0stars🔀0forks📁View on GitHub🕐Updated Dec 15, 2025

When & Why to Use This Skill

This Claude skill provides standardized patterns and best practices for managing MapLibre GL layers within Vue.js applications. It solves critical architectural challenges such as reactive performance optimization using markRaw, dynamic GeoJSON source management, and scalable layer registration patterns, ensuring robust and performant map-based visualizations.

Use Cases

  • Case 1: Implementing a centralized map layer management system using Vue stores to dynamically compose and toggle layers across different application views.
  • Case 2: Optimizing map performance by correctly utilizing native GPU-accelerated layers for large datasets while using HTML markers for complex, interactive UI labels.
  • Case 3: Developing data-driven styling for geographic features, such as interpolating colors and sizes based on real-time data properties or zoom levels.
  • Case 4: Managing complex map interactions and event listeners (click, hover) within the Vue lifecycle to prevent memory leaks and ensure clean component unmounting.
namemaplibre-layers
description>-

MapLibre Layers

Layer management patterns for map visualization.

Announce: "I'm using maplibre-layers to implement map layers correctly."

Layer Registration Pattern

Views register layers via the mapLayers store:

// src/stores/mapLayers.ts
import { markRaw } from 'vue'

export const useMapLayersStore = defineStore('mapLayers', () => {
  const layers = ref<MapLayer[]>([])

  function setLayers(newLayers: MapLayer[]) {
    // CRITICAL: markRaw prevents Vue from making components reactive
    layers.value = newLayers.map(l => ({
      ...l,
      component: markRaw(l.component)
    }))
  }

  function clearLayers() {
    layers.value = []
  }

  return { layers, setLayers, clearLayers }
})

In a view:

// GameView.vue
import CandidatesLayer from '@/components/map/CandidatesLayer.vue'
import { MAP_KEY } from '@/composables/map/useMapCamera'

const mapLayersStore = useMapLayersStore()

onMounted(() => {
  mapLayersStore.setLayers([{
    key: 'candidates',
    component: CandidatesLayer,
    props: { 
      candidates: candidates,
      mapKey: MAP_KEY
    }
  }])
})

onUnmounted(() => {
  mapLayersStore.clearLayers()
})

In BaseMap.vue:

<template>
  <MglMap ...>
    <component
      v-for="layer in layers"
      :key="layer.key"
      :is="layer.component"
      v-bind="layer.props"
    />
  </MglMap>
</template>

GeoJSON Layer Pattern

Use MglGeoJsonSource with layer components:

<!-- CandidatesLayer.vue -->
<template>
  <MglGeoJsonSource 
    source-id="candidates" 
    :data="candidatesGeoJson"
  >
    <!-- Circle markers -->
    <MglCircleLayer
      layer-id="candidates-circles"
      :paint="{
        'circle-radius': ['get', 'radius'],
        'circle-color': ['get', 'color'],
        'circle-opacity': 0.8
      }"
    />
  </MglGeoJsonSource>
</template>

<script setup lang="ts">
const props = defineProps<{
  candidates: Candidate[]
}>()

const candidatesGeoJson = computed(() => ({
  type: 'FeatureCollection' as const,
  features: props.candidates.map(c => ({
    type: 'Feature' as const,
    geometry: {
      type: 'Point' as const,
      coordinates: [c.lng, c.lat]
    },
    properties: {
      id: c.id,
      name: c.name,
      radius: Math.max(5, c.confidence * 20),
      color: getColorForConfidence(c.confidence)
    }
  }))
}))
</script>

Data-Driven Styling

Use MapLibre expressions for dynamic styling:

// Interpolate color based on confidence
const paintConfig = {
  'circle-color': [
    'interpolate',
    ['linear'],
    ['get', 'confidence'],
    0, '#gray',
    0.5, '#yellow', 
    1.0, '#green'
  ],
  'circle-radius': [
    'interpolate',
    ['linear'],
    ['zoom'],
    2, 3,   // At zoom 2, radius 3
    10, 15  // At zoom 10, radius 15
  ]
}

Layer Event Handling

Handle clicks and hovers on layers:

<script setup lang="ts">
import { inject, onMounted, onUnmounted } from 'vue'
import type { Map as MapLibreMap } from 'maplibre-gl'

const props = defineProps<{ mapKey: symbol }>()
const map = inject<MapLibreMap>(props.mapKey)

function handleClick(e: any) {
  const feature = e.features?.[0]
  if (feature) {
    emit('placeClick', feature.properties.id)
  }
}

function handleMouseEnter() {
  if (map) map.getCanvas().style.cursor = 'pointer'
}

function handleMouseLeave() {
  if (map) map.getCanvas().style.cursor = ''
}

onMounted(() => {
  if (!map) return
  map.on('click', 'candidates-circles', handleClick)
  map.on('mouseenter', 'candidates-circles', handleMouseEnter)
  map.on('mouseleave', 'candidates-circles', handleMouseLeave)
})

onUnmounted(() => {
  if (!map) return
  map.off('click', 'candidates-circles', handleClick)
  map.off('mouseenter', 'candidates-circles', handleMouseEnter)
  map.off('mouseleave', 'candidates-circles', handleMouseLeave)
})
</script>

HTML Markers for Labels

Use HTML markers for complex labels (Vue components):

<template>
  <!-- Native layer for performance -->
  <MglGeoJsonSource ...>
    <MglCircleLayer ... />
  </MglGeoJsonSource>
  
  <!-- HTML markers for labels -->
  <MglMarker
    v-for="candidate in topCandidates"
    :key="candidate.id"
    :lng-lat="[candidate.lng, candidate.lat]"
    :offset="[0, -20]"
  >
    <div class="candidate-label">
      {{ candidate.name }}
    </div>
  </MglMarker>
</template>

When to use what:

  • Native layers (MglCircleLayer, MglFillLayer): Thousands of points, GPU-accelerated
  • HTML markers (MglMarker): Complex styling, Vue components, limited count

Anti-Patterns

DON'T: Skip markRaw

// WRONG: Vue makes component reactive (bad performance)
layers.value = [{ component: MyLayer }]

// CORRECT: markRaw prevents reactivity
layers.value = [{ component: markRaw(MyLayer) }]

DON'T: Forget Cleanup

// WRONG: Event listeners leak
onMounted(() => {
  map.on('click', 'layer', handler)
})

// CORRECT: Clean up on unmount
onUnmounted(() => {
  map.off('click', 'layer', handler)
})

DON'T: Mutate GeoJSON Directly

// WRONG: Mutating doesn't trigger reactivity
candidatesGeoJson.value.features.push(newFeature)

// CORRECT: Create new object
candidatesGeoJson.value = {
  ...candidatesGeoJson.value,
  features: [...candidatesGeoJson.value.features, newFeature]
}

Layer Ordering

Layers render in order added. Control z-order with beforeId:

<MglFillLayer
  layer-id="regions"
  :beforeId="'candidates-circles'"  <!-- Render below circles -->
/>

References

See references/layer-examples.md for more patterns.