Map blocks for your application
Pre-built, ready-to-use map blocks. Browse, preview, and copy them into your app with one command.
Analytics Map
Real-time analytics overview with a world map, breakdown cards, and device stats.
components/ui/blocks/analytics-map/AnalyticsMap.vue
<script setup lang="ts">
import {
Map,
MapControls,
MapMarker,
MarkerContent,
MarkerTooltip,
} from "@/components/ui/map";
import OverviewCard from "./OverviewCard.vue";
import BreakdownCard from "./BreakdownCard.vue";
import {
browsersRows,
countriesRows,
locations,
referrersRows,
visitedPagesRows,
} from "./data";
const MAP_HEIGHT = "38rem";
</script>
<template>
<div
class="bg-background relative min-h-screen"
:style="{ '--map-height': MAP_HEIGHT }"
>
<div class="relative h-(--map-height)">
<ClientOnly>
<Map
:center="[-2, 16]"
:zoom="1.5"
:scroll-zoom="false"
:render-world-copies="true"
>
<MapControls show-fullscreen />
<MapMarker
v-for="location in locations"
:key="location.city"
:longitude="location.lng"
:latitude="location.lat"
>
<MarkerContent>
<div
class="rounded-full bg-blue-500/70"
:style="{
width: location.size * 3 + 'px',
height: location.size * 3 + 'px',
}"
/>
</MarkerContent>
<MarkerTooltip
:offset="20"
class="bg-background text-foreground border"
>
<p class="text-muted-foreground font-medium">
{{ location.city }}
</p>
<p class="mt-0.5">{{ location.size }} active users</p>
</MarkerTooltip>
</MapMarker>
</Map>
</ClientOnly>
<div
class="via-background/30 to-background pointer-events-none absolute inset-x-0 bottom-0 h-40 bg-linear-to-b from-transparent"
aria-hidden
/>
<OverviewCard />
</div>
<div class="grid gap-4 p-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<BreakdownCard title="Visited pages" :rows="visitedPagesRows" />
<BreakdownCard title="Referrers" :rows="referrersRows" />
<BreakdownCard title="Countries" :rows="countriesRows" />
<BreakdownCard title="Browsers" :rows="browsersRows" />
</div>
</div>
</template>
Logistics Network
Domestic logistics map with a sidebar of stats and filters.
components/ui/blocks/logistics-network/LogisticsNetwork.vue
<script setup lang="ts">
import { hubs, routes } from "./data";
import FilterSidebar from "./FilterSidebar.vue";
import NetworkMap from "./NetworkMap.vue";
</script>
<template>
<div class="flex h-screen w-full">
<FilterSidebar :hubs="hubs" :routes="routes" />
<main class="flex-1 overflow-hidden">
<NetworkMap :hubs="hubs" :routes="routes" />
</main>
</div>
</template>
Heatmap
Globe-projected heatmap visualizing earthquake density with zoom-dependent styling.
components/ui/blocks/heatmap/Heatmap.vue
<script setup lang="ts">
import { Map } from "@/components/ui/map";
import Card from "@/components/ui/Card.vue";
import CardHeader from "@/components/ui/CardHeader.vue";
import CardTitle from "@/components/ui/CardTitle.vue";
import CardContent from "@/components/ui/CardContent.vue";
import GlobeHeatmapLayers from "./GlobeHeatmapLayers.vue";
const EARTHQUAKE_GEOJSON_URL =
"https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson";
const HEATMAP_GRADIENT_COLORS = [
"#fff7bc",
"#fee391",
"#fec44f",
"#fe9929",
"#d7301f",
];
const HEATMAP_COLOR_STOPS: [number, string][] = [
[0.15, HEATMAP_GRADIENT_COLORS[0]!],
[0.35, HEATMAP_GRADIENT_COLORS[1]!],
[0.55, HEATMAP_GRADIENT_COLORS[2]!],
[0.75, HEATMAP_GRADIENT_COLORS[3]!],
[1, HEATMAP_GRADIENT_COLORS[4]!],
];
</script>
<template>
<div class="bg-muted/50 relative h-screen">
<div class="relative h-full">
<ClientOnly>
<Map
:center="[-113, 43]"
:zoom="3.2"
:projection="{ type: 'globe' }"
:pitch="24"
:min-zoom="1.2"
:max-zoom="8"
>
<GlobeHeatmapLayers />
</Map>
</ClientOnly>
</div>
<Card class="absolute top-4 left-4 z-10 w-72">
<CardHeader>
<CardTitle>Global Earthquakes Heatmap</CardTitle>
</CardHeader>
<CardContent>
<div class="grid grid-cols-5 gap-1.5">
<span
v-for="color in HEATMAP_GRADIENT_COLORS"
:key="color"
class="h-2.5 rounded-full"
:style="{ backgroundColor: color }"
/>
</div>
<div
class="text-muted-foreground flex items-center justify-between pt-3 text-xs"
>
<span>Low</span>
<span>High</span>
</div>
<p class="text-muted-foreground pt-2 text-xs">
Data source:
<a
:href="EARTHQUAKE_GEOJSON_URL"
target="_blank"
rel="noopener noreferrer"
class="hover:text-foreground underline underline-offset-4 transition-colors"
>
MapLibre earthquakes.geojson
</a>
</p>
</CardContent>
</Card>
</div>
</template>
Delivery Tracker
Live order tracking with route progress, courier position, and order details.
components/ui/blocks/delivery-tracker/DeliveryTracker.vue
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { Clock3, Truck, UserRound, Utensils } from "lucide-vue-next";
import {
Map,
MapMarker,
MapRoute,
MarkerContent,
MarkerTooltip,
} from "@/components/ui/map";
import Card from "@/components/ui/Card.vue";
import CardHeader from "@/components/ui/CardHeader.vue";
import CardTitle from "@/components/ui/CardTitle.vue";
import CardContent from "@/components/ui/CardContent.vue";
import Badge from "@/components/ui/Badge.vue";
import { Button } from "@/components/ui/button";
interface DeliveryMeal {
name: string;
price: string;
quantity: number;
}
interface OsrmRouteData {
coordinates: [number, number][];
duration: number;
distance: number;
}
const deliveryMeals: DeliveryMeal[] = [
{ name: "Spicy Tofu Grain Bowl", price: "$44.00", quantity: 1 },
{ name: "Herb Chicken Rice Box", price: "$58.00", quantity: 2 },
{ name: "Roasted Veggie Wrap", price: "$29.00", quantity: 1 },
];
const pickup = { lng: -122.466, lat: 37.716 };
const dropoff = { lng: -122.399, lat: 37.683 };
function formatDistance(meters?: number) {
if (!meters) return "--";
if (meters < 1000) return `${Math.round(meters)} m`;
return `${(meters / 1000).toFixed(1)} km`;
}
function formatDuration(seconds?: number) {
if (!seconds) return "--";
const minutes = Math.round(seconds / 60);
if (minutes < 60) return `${minutes} min`;
const hours = Math.floor(minutes / 60);
return `${hours}h ${minutes % 60}m`;
}
const routeData = ref<OsrmRouteData | null>(null);
const loading = ref(true);
onMounted(async () => {
try {
const response = await fetch(
`https://router.project-osrm.org/route/v1/driving/${pickup.lng},${pickup.lat};${dropoff.lng},${dropoff.lat}?overview=full&geometries=geojson`,
);
const data = await response.json();
const route = data?.routes?.[0];
if (!route?.geometry?.coordinates) return;
routeData.value = {
coordinates: route.geometry.coordinates as [number, number][],
duration: route.duration as number,
distance: route.distance as number,
};
} catch (err) {
console.error("Failed to fetch route:", err);
} finally {
loading.value = false;
}
});
const progressCoordinates = computed<[number, number][]>(() => {
const coords = routeData.value?.coordinates ?? [];
const factor = routeData.value ? 0.62 : 0.66;
const progressCount = Math.max(2, Math.floor(coords.length * factor));
return coords.slice(0, progressCount);
});
const courierPosition = computed<[number, number] | undefined>(
() => progressCoordinates.value[progressCoordinates.value.length - 1],
);
</script>
<template>
<div class="p-8">
<div
class="bg-sidebar mx-auto grid max-w-7xl rounded-xl border md:h-[600px] md:grid-cols-[1.05fr_1fr]"
>
<!-- Left: order details -->
<div class="flex flex-col p-5 md:p-6">
<div class="space-y-1">
<h3 class="text-2xl font-semibold tracking-tight">Track Delivery</h3>
<p class="text-muted-foreground text-sm">Mon Feb 10 — 2-3 PM</p>
</div>
<Card class="mt-5">
<CardHeader>
<CardTitle class="font-medium">
Order items ({{ deliveryMeals.length }})
</CardTitle>
</CardHeader>
<CardContent class="space-y-5">
<div
v-for="meal in deliveryMeals"
:key="meal.name"
class="flex items-center gap-3"
>
<div
class="bg-muted grid size-8 place-items-center rounded-full text-xs font-medium"
>
<Utensils class="text-muted-foreground size-4" />
</div>
<div class="min-w-4 flex-1">
<p class="truncate pb-1 text-sm font-medium">{{ meal.name }}</p>
<p class="text-muted-foreground text-xs">{{ meal.price }}</p>
</div>
<Badge variant="secondary" class="h-6 rounded-full px-2.5">
x{{ meal.quantity }}
</Badge>
</div>
<div
class="border-border/60 flex items-center justify-between border-t pt-3 text-sm"
>
<span class="text-muted-foreground">Bundle total</span>
<span class="font-medium">$189.00</span>
</div>
</CardContent>
</Card>
<div class="mt-4 grid gap-3 sm:grid-cols-2">
<Card>
<CardContent class="space-y-2">
<p class="text-muted-foreground text-sm font-medium">
Pickup confirmed
</p>
<p class="text-sm font-medium">Mon, Feb 10 at 1:48 PM</p>
</CardContent>
</Card>
<Card>
<CardContent class="space-y-2">
<p class="text-muted-foreground text-sm font-medium">
Remaining travel
</p>
<p class="text-sm font-medium">
{{ formatDuration(routeData?.duration) }}
</p>
</CardContent>
</Card>
</div>
<div class="mt-6 flex flex-wrap items-center gap-2">
<Button size="sm">
<Clock3 class="size-4" />
View timeline
</Button>
<Button variant="outline" size="sm">
<UserRound class="size-4" />
Contact courier
</Button>
</div>
</div>
<!-- Right: map -->
<div
class="relative h-[400px] overflow-hidden rounded-xl shadow-sm md:h-full"
>
<ClientOnly>
<Map
:loading="loading"
:center="[-122.435, 37.696]"
:zoom="12"
:min-zoom="10"
:max-zoom="16"
:styles="{
light: 'https://tiles.openfreemap.org/styles/bright',
dark: 'https://tiles.openfreemap.org/styles/dark',
}"
>
<MapRoute
id="delivery-full-route"
:coordinates="routeData?.coordinates ?? []"
color="#5b6572"
:width="5.2"
:opacity="0.3"
:interactive="false"
/>
<MapRoute
id="delivery-progress-route"
:coordinates="progressCoordinates"
color="#3b82f6"
:width="6"
:opacity="0.95"
:interactive="false"
/>
<MapMarker
v-if="courierPosition"
:longitude="courierPosition[0]"
:latitude="courierPosition[1]"
:offset="[0, 10]"
>
<MarkerContent>
<div
class="relative grid size-9 place-items-center rounded-full bg-emerald-500 dark:bg-emerald-600"
>
<Truck class="size-4 text-white" />
</div>
</MarkerContent>
<MarkerTooltip>
<div class="space-y-0.5 text-xs">
<p class="font-medium">
Order {{ formatDuration(routeData?.duration) }} away
</p>
<p class="text-background/70">
Route {{ formatDistance(routeData?.distance) }}
</p>
</div>
</MarkerTooltip>
</MapMarker>
<MapMarker :longitude="pickup.lng" :latitude="pickup.lat">
<MarkerContent>
<div
class="size-4 rounded-full border-2 border-white bg-emerald-500 shadow-sm"
/>
</MarkerContent>
<MarkerTooltip>Origin</MarkerTooltip>
</MapMarker>
<MapMarker :longitude="dropoff.lng" :latitude="dropoff.lat">
<MarkerContent>
<div
class="size-4 rounded-full border-2 border-white bg-rose-500 shadow-sm"
/>
</MarkerContent>
<MarkerTooltip>Destination</MarkerTooltip>
</MapMarker>
</Map>
</ClientOnly>
</div>
</div>
</div>
</template>