Nuxt 4 Interactive Map Tutorial: Leaflet, Marker Clustering, Highcharts

By Lufti Nurfahmi

  • Playground / Experiment

Nuxt 4 Interactive Map Tutorial: Leaflet, Marker Clustering, Highcharts

 

Lately, I’ve been experimenting with Nuxt 4, trying to explore its capabilities for building interactive web applications. One of my experiments led me to create a fully interactive map of Jakarta, complete with clustered markers, custom popups, and embedded charts. This tutorial walks through how I built it using Nuxt 4, Leaflet, Leaflet.markercluster, and Highcharts.

Live Demo: interactive-map-nuxt.vercel.app
Source Code: GitHub Repository

Tech Stack

  • Nuxt 4 – modern Vue framework with TypeScript support.
  • Tailwind CSS – for fast, responsive styling.
  • Leaflet – library for interactive maps.
  • Leaflet.markercluster – for automatic clustering of markers.
  • Highcharts – for charts inside popups/offcanvas.

Project Setup

Start by creating a new Nuxt 4 project:

npx nuxi init interactive-map-nuxt
cd interactive-map-nuxt
npm install

Install the required dependencies:

npm install leaflet leaflet.markercluster highcharts @nuxtjs/tailwindcss

Configure nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['@nuxtjs/tailwindcss'],
  css: [
    'leaflet/dist/leaflet.css',
    '@/assets/css/main.css'
  ],
  devtools: { enabled: true },
})

Folder Structure

interactive-map-nuxt/
├─ app/
│  ├─ layouts/
│  │  └─ default.vue
│  ├─ pages/
│  │  └─ index.vue
│  └─ composables/
│     └─ useJakartaMap.ts
├─ assets/css/main.css
└─ public/jakarta.json

Folder Structure

Creating the Map

<template>
  <div ref="mapContainer" class="fixed inset-0 z-0"></div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';

const mapContainer = ref<HTMLDivElement | null>(null);

onMounted(async () => {
  if (!mapContainer.value) return;

  const L = await import('leaflet');

  const map = L.map(mapContainer.value, {
    center: [-6.2088, 106.8456], // Jakarta
    zoom: 12,
    minZoom: 11,
    maxZoom: 18,
    scrollWheelZoom: true,
  });

  L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
    attribution: '© Carto © OpenStreetMap',
    subdomains: 'abcd',
    maxZoom: 19,
  }).addTo(map);
});
</script>

Basic Map

Adding Markers & Clustering

import 'leaflet.markercluster';

const markers = L.markerClusterGroup();

data.features.forEach((feature) => {
  const [lng, lat] = feature.geometry.coordinates;
  const marker = L.marker([lat, lng]);
  marker.bindPopup(`<b>${feature.properties.name}</b><br>${feature.properties.category}`);
  markers.addLayer(marker);
});

map.addLayer(markers);

Custom Popups / Offcanvas

<div v-if="dialog.visible"
     class="absolute bg-white shadow-lg rounded-lg p-4 w-64 transition-all duration-200"
     :style="{ top: dialog.y + 'px', left: dialog.x + 'px' }">
  <h3 class="font-bold text-lg">{{ dialog.data?.name }}</h3>
  <p class="text-sm text-gray-600">Category: {{ dialog.data?.category }}</p>
  <ul class="list-disc ml-4 text-sm">
    <li v-for="(val, year) in dialog.data?.visitors" :key="year">
      {{ year }}: {{ val.toLocaleString() }}
    </li>
  </ul>
  <button @click="dialog.visible = false" class="mt-3 text-sm text-blue-600 hover:underline">
    Close
  </button>
</div>

Integrating Highcharts

import Highcharts from 'highcharts';

Highcharts.chart('chart-container', {
  chart: { type: 'pie', height: 200 },
  title: { text: 'Visitors Breakdown' },
  series: [{
    name: 'Visitors',
    data: [
      { name: 'Domestic', y: feature.properties.visitors['2023'] * 0.7 },
      { name: 'International', y: feature.properties.visitors['2023'] * 0.3 }
    ]
  }]
});

Custom Marker Icons

const icon = L.icon({
  iconUrl: '/marker-icon.png',
  iconSize: [25, 41],
  iconAnchor: [12, 41]
});

L.marker([lat, lng], { icon }).addTo(map);

Deployment

npm run build
npm run preview

Then deploy to Vercel. Preview: https://interactive-map-nuxt.vercel.app/

Conclusion

This experiment taught me a lot about Nuxt 4, Leaflet, and integrating charts in an interactive map. You now have a fully functional map with:

  • Marker clustering
  • Tailwind-styled custom popups
  • Highcharts visualizations
  • Responsive and interactive map layout

You can expand this tutorial to other cities, datasets, or even real-time data visualization.