maplibre-layers
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.
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.
| name | maplibre-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.