---
title: "Example: Delivery Tracker"
description: "Build a production-shaped delivery tracker with reactive route progress, map fallbacks, and an accessible status alternative."
canonical_url: "https://scripts.nuxt.com/scripts/maplibre/guides/delivery-tracker"
last_updated: "2026-08-11T04:27:01.562Z"
---

This real-world delivery tracker combines a GeoJSON route with a reactive courier marker. Update the marker from a timer or [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/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.

<code-group>
<map-libre-delivery-tracker-demo label="Demo">



</map-libre-delivery-tracker-demo>

```vue [Source]
<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>
```

</code-group>

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.
