Guides
Example: Delivery Tracker
This real-world delivery tracker combines a GeoJSON route with a reactive courier marker. Update the marker from a timer or WebSocket handler in production; MapLibre moves it without recreating it.
It demonstrates reactive route progress, a live courier marker, navigation controls, error fallback content, and an accessible status alternative outside the WebGL canvas.
Delivery route from West Melbourne depot to Flinders Lane. The courier is currently at West Melbourne depot.
Loading delivery map
Delivery tracker
Parcel collected. Stop 1 of 3: West Melbourne depot
<script setup lang="ts">
import type { FeatureCollection, LineString } from 'geojson'
import type { LngLatLike } from 'maplibre-gl'
interface Checkpoint {
label: string
detail: string
position: [number, number]
}
const checkpoints: Checkpoint[] = [
{
label: 'West Melbourne depot',
detail: 'Parcel collected',
position: [144.9495, -37.8101],
},
{
label: 'Docklands checkpoint',
detail: 'Courier heading east',
position: [144.9538, -37.8151],
},
{
label: 'Southbank checkpoint',
detail: 'Final approach',
position: [144.9632, -37.8227],
},
{
label: 'Flinders Lane delivery',
detail: 'Customer destination',
position: [144.9687, -37.8154],
},
]
const currentIndex = ref(0)
const center = ref<LngLatLike>(checkpoints[0]!.position)
const current = computed(() => checkpoints[currentIndex.value]!)
const route = computed<FeatureCollection<LineString>>(() => {
const completed = checkpoints
.slice(0, currentIndex.value + 1)
.map(checkpoint => checkpoint.position)
if (completed.length === 1)
completed.push(completed[0]!)
return {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: { progress: 'remaining' },
geometry: {
type: 'LineString',
coordinates: checkpoints.map(checkpoint => checkpoint.position),
},
},
{
type: 'Feature',
properties: { progress: 'completed' },
geometry: { type: 'LineString', coordinates: completed },
},
],
}
})
const routeLayers = [
{
id: 'delivery-route',
type: 'line' as const,
filter: ['==', ['get', 'progress'], 'remaining'],
paint: {
'line-color': '#64748b',
'line-opacity': 0.65,
'line-width': 5,
},
},
{
id: 'delivery-progress',
type: 'line' as const,
filter: ['==', ['get', 'progress'], 'completed'],
paint: { 'line-color': '#2563eb', 'line-width': 6 },
},
]
function advanceCourier() {
if (currentIndex.value === checkpoints.length - 1)
return
currentIndex.value += 1
center.value = checkpoints[currentIndex.value]!.position
}
</script>
<template>
<div>
<p aria-live="polite">
Current status: <strong>{{ current.detail }}</strong>
</p>
<ScriptMapLibreMap
v-model:center="center"
map-style="https://tiles.openfreemap.org/styles/liberty"
:zoom="14"
width="100%"
:height="520"
aria-label="Delivery route from West Melbourne to Flinders Lane"
>
<template #description>
Delivery route from West Melbourne depot to Flinders Lane via Docklands and Southbank.
The courier is currently at {{ current.label }}.
</template>
<template #error>
<p role="alert">
Live map unavailable. Follow the delivery status above instead.
</p>
</template>
<ScriptMapLibreNavigationControl position="top-right" />
<ScriptMapLibreGeoJson
source-id="delivery-route"
:data="route"
:layers="routeLayers"
/>
<ScriptMapLibreMarker
:position="current.position"
:aria-label="`Courier at ${current.label}`"
title="Current courier position"
:options="{ color: '#2563eb' }"
/>
</ScriptMapLibreMap>
<button
type="button"
:disabled="currentIndex === checkpoints.length - 1"
@click="advanceCourier"
>
Advance courier
</button>
</div>
</template>Keep order status, checkpoint names, and estimated times in normal HTML outside the canvas. The description slot connects the route summary to the map for assistive technology.
MapLibre coordinates use [longitude, latitude] order.