Guides
Example: Pickup Locator
This real-world pickup locator pairs a location list with the map. The list keeps addresses and opening hours available when tiles fail, and it gives keyboard users a direct way to choose a location.
It demonstrates reactive selection, accessible location buttons, explicit OpenStreetMap attribution, marker popups, error fallback content, and a GeoJSON delivery zone.
Loading pickup map
Flinders Lane
Centre Place, Melbourne
Centre Place, Melbourne
Queen Victoria Market
Queen Street, Melbourne
Queen Street, Melbourne
Pickup locator
Selected: Flinders Lane, Centre Place, Melbourne
<script setup lang="ts">
import type { Feature, Polygon } from 'geojson'
import type { LatLngExpression, LatLngTuple } from 'leaflet'
interface PickupLocation {
id: string
name: string
address: string
hours: string
position: LatLngTuple
}
const locations: PickupLocation[] = [
{
id: 'flinders-lane',
name: 'Flinders Lane',
address: 'Centre Place, Melbourne VIC',
hours: 'Open today until 6 pm',
position: [-37.8164, 144.9656],
},
{
id: 'queen-victoria-market',
name: 'Queen Victoria Market',
address: 'Queen Street, Melbourne VIC',
hours: 'Open today until 3 pm',
position: [-37.8076, 144.9568],
},
{
id: 'southbank',
name: 'Southbank',
address: 'Southbank Promenade, Southbank VIC',
hours: 'Open today until 8 pm',
position: [-37.8202, 144.9655],
},
]
const selectedId = ref(locations[0]!.id)
const center = ref<LatLngExpression>(locations[0]!.position)
const zoom = ref(14)
const deliveryZone: Feature<Polygon> = {
type: 'Feature',
properties: { name: 'Same-day delivery zone' },
geometry: {
type: 'Polygon',
coordinates: [[
[144.946, -37.802],
[144.982, -37.802],
[144.986, -37.828],
[144.951, -37.832],
[144.946, -37.802],
]],
},
}
function selectLocation(location: PickupLocation) {
selectedId.value = location.id
center.value = location.position
zoom.value = 16
}
</script>
<template>
<div class="pickup-locator">
<aside aria-labelledby="pickup-title">
<h2 id="pickup-title">
Pickup in Melbourne
</h2>
<ol>
<li v-for="location in locations" :key="location.id">
<button
type="button"
:aria-pressed="selectedId === location.id"
@click="selectLocation(location)"
>
<strong>{{ location.name }}</strong>
<span>{{ location.address }}</span>
<small>{{ location.hours }}</small>
</button>
</li>
</ol>
</aside>
<ScriptLeafletMap
v-model:center="center"
v-model:zoom="zoom"
width="100%"
:height="520"
aria-label="Pickup locations and delivery zone in central Melbourne"
>
<template #error>
<p role="alert">
The map is unavailable. Choose a pickup location from the list.
</p>
</template>
<ScriptLeafletTileLayer
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
:options="{
maxZoom: 19,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>',
}"
/>
<ScriptLeafletGeoJson
:data="deliveryZone"
:options="{
style: {
color: '#15803d',
fillColor: '#86efac',
fillOpacity: 0.18,
weight: 2,
},
}"
/>
<ScriptLeafletMarker
v-for="location in locations"
:key="location.id"
:position="location.position"
:alt="`${location.name} pickup location`"
:title="location.name"
@click="selectLocation(location)"
>
<ScriptLeafletPopup
:open="selectedId === location.id"
:options="{ minWidth: 180 }"
>
<strong>{{ location.name }}</strong><br>
{{ location.address }}<br>
{{ location.hours }}
</ScriptLeafletPopup>
</ScriptLeafletMarker>
</ScriptLeafletMap>
</div>
</template>Use CSS Grid to place the list beside the map on wide screens and above it on small screens. Keep the list in the document even when the map is the main visual.
GeoJSON coordinates use [longitude, latitude]; Leaflet marker positions use [latitude, longitude].