DEV Community

Cover image for I built a Google Maps Clone using Next.js 16 + Leaflet — now it’s an Open-Source Starter Kit
Welly Wahyudi
Welly Wahyudi

Posted on

I built a Google Maps Clone using Next.js 16 + Leaflet — now it’s an Open-Source Starter Kit

Why I Built This

I've been working in GIS for over 7 years, building everything from interactive map dashboards to spatial analysis tools and land management systems. After doing this for so long, one lesson keeps repeating itself:

You lose more time bootstrapping maps than actually building features.

Most Next.js + Leaflet examples today are either outdated, rely heavily on react-leaflet abstractions, or simply don't work with App Router and React 19.

So I decided to build the mapping starter I always wished existed — and share it with the community.


What I Built

A production-ready Next.js 16 starter template that combines vanilla Leaflet with a modern, Google Maps-inspired UI. It's not just another "hello world" map example—it's a foundation for building real mapping applications.

Map Interface Demo

The Stack

Technology Version Purpose
Next.js 16 App Router, Server Components
React 19 UI Framework
Leaflet 1.9 Mapping (vanilla, no wrapper)
TypeScript 5 Type Safety
Tailwind CSS 4 Styling
shadcn/ui Latest Accessible UI Components

Why Vanilla Leaflet? (The react-leaflet Problem)

"Why not just use react-leaflet? Everyone uses react-leaflet."

react-leaflet is great, but it wasn't designed for Next.js SSR. This leads to common issues:

  • ❌ Hydration mismatches
  • ❌ Broken context
  • ❌ Map initialization failures
  • ❌ SSR compatibility issues

Why I Chose Vanilla Leaflet

1. Direct Control

// With vanilla Leaflet, you get the actual map instance
const map = useLeafletMap();
map.flyTo([51.5, -0.09], 13); // Direct access to all Leaflet methods
Enter fullscreen mode Exit fullscreen mode

2. Better TypeScript Support
No fighting with wrapper types. You're working directly with Leaflet's well-documented types.

3. Easier Debugging
When something breaks, you're debugging Leaflet, not Leaflet + react-leaflet + your code.

4. Smaller Bundle
One less dependency. Every KB counts.

5. Future-Proof
Leaflet updates don't require waiting for react-leaflet to catch up.

The Abstraction I Built Instead

Instead of using react-leaflet, I created a thin, declarative wrapper that feels React-native but gives you full Leaflet power:

<MapProvider>
  <LeafletMap center={[51.5, -0.09]} zoom={13}>
    <LeafletTileLayer url={tileUrl} />
    <LeafletMarker position={[51.5, -0.09]} popup="Hello!" />
    <LeafletGeoJSON data={countryData} />
  </LeafletMap>
</MapProvider>
Enter fullscreen mode Exit fullscreen mode

Clean, declarative, but with direct access to the Leaflet instance whenever you need it.


What This Starter Includes

A fully polished, production-ready mapping foundation inspired by the speed and clarity of Google Maps—built so you can start shipping features on day one instead of battling boilerplate.

1. Expandable Search Bar with Autocomplete

Next Leaflet Expandable Search
Real-time search across 195+ countries with a Google Maps-style expandable interface:

  • ⚡ Debounced input (150ms, no API spam)
  • ⌨️ Full keyboard navigation (↑↓ Enter Esc)
  • 👁️ Visual feedback with highlighted selection
  • 🎯 Auto-zoom to selected country
  • 📜 Auto-scroll selected item into view
<MapSearchBar
  onCountrySelect={handleCountrySelect}
  selectedCountry={country}
  onClearSelection={handleClear}
/>
Enter fullscreen mode Exit fullscreen mode

2. Map Details Sidebar Panel

Map Details Sidebar Panel

A fully responsive details panel that adapts to any device—appearing as a sidebar on desktop and a slide-up drawer on mobile. It’s tightly integrated with the search bar flow to create a smooth, cohesive user experience.

Desktop Experience:

  • 📌 Fixed left-side sidebar for consistent map context
  • 🔎 Seamlessly connected with the search workflow
  • 🖼️ Hero banner with country highlight overlay
  • 📊 Detailed country information with structured layout

Mobile Experience:

  • 📱 Bottom slide-up drawer (Vaul)
  • 👆 Snap points at 30%, 60%, 100%
  • 🎯 Touch-friendly interactions
  • ✨ Smooth animations

What you get in the panel:

  • 🏳️ Country flag
  • 📊 Population, area, capital
  • 💰 Currency information
  • 🗣️ Languages spoken
  • 🧭 Quick action buttons (Directions, Save, Nearby, Share)

3. Basemap Providers

Hover over the layer button, and a panel slides out with visual previews of each map style:

Provider Style Source
🗺️ OpenStreetMap Basic Community
🛰️ Satellite Imagery Esri
🌙 Dark Mode Dark CartoDB

Add your own with a simple config:

{
  id: 'custom',
  name: 'My Custom Tiles',
  url: 'https://my-tile-server/{z}/{x}/{y}.png',
  attribution: '© Me',
  maxZoom: 19,
  category: 'standard',
}
Enter fullscreen mode Exit fullscreen mode

4. Theme-Aware Basemaps

The map tiles automatically switch based on your theme preference:

const { tileProvider } = useMapTileProvider();
// Returns dark tiles in dark mode, light tiles in light mode
Enter fullscreen mode Exit fullscreen mode

Light mode? Bright, colorful tiles. Dark mode? Smooth dark tiles from CartoDB. It's automatic.

5. Geolocation with Reusable Hook

Find user's location with a single hook—used across multiple components following DRY principle:

const { locateUser, isLocating, isAvailable } = useGeolocation();

<button onClick={locateUser} disabled={!isAvailable || isLocating}>
  {isLocating ? "Locating..." : "Find Me"}
</button>;
Enter fullscreen mode Exit fullscreen mode

Features:

  • 📍 Adds blue marker at user's location
  • 🔵 Shows accuracy circle
  • 🎯 Auto-zooms to location
  • ⚠️ Graceful error handling

6. GeoJSON Visualization

Load any GeoJSON data and visualize it with custom styling:

<LeafletGeoJSON
  data={countryData}
  style={{
    fillColor: "#3b82f6",
    fillOpacity: 0.2,
    color: "#2563eb",
    weight: 2,
  }}
/>
Enter fullscreen mode Exit fullscreen mode

The component automatically fits the map bounds to your data with smooth fly-to animations.

7. Custom Map Controls UI

A polished control panel at bottom-right—no default Leaflet controls, fully custom styled:

  • ➕ Zoom in/out buttons
  • 🔄 Reset view to default position
  • 📍 Find my location (uses useGeolocation hook)
  • ⛶ Fullscreen toggle
const { zoomIn, zoomOut, resetView, toggleFullscreen } = useMapControls();
Enter fullscreen mode Exit fullscreen mode

All controls are:

  • 🎨 Theme-aware (light/dark)
  • ♿ Accessible with ARIA labels
  • ✨ Smooth hover animations
  • 📱 Touch-friendly

8. Draggable Markers

<LeafletMarker
  position={position}
  draggable={true}
  onDragEnd={(newPos) => {
    console.log("New position:", newPos);
  }}
/>
Enter fullscreen mode Exit fullscreen mode

9. Error Boundaries & Loading States

Production-ready error handling:

<MapErrorBoundary>
  <MapProvider>
    <LeafletMap>{/* Your map */}</LeafletMap>
    <MapLoadingSpinner />
  </MapProvider>
</MapErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

10. Dark Mode Support

Full dark mode across the entire application:

  • 🌙 Theme toggle in top bar
  • 🎨 All UI components adapt
  • 🗺️ Map tiles switch automatically
  • 💾 Preference persisted

Architecture & Developer Experience

SSR-Safe Leaflet Loading

Leaflet expects window to exist. Next.js SSR doesn't have window. Here's the solution:

"use client";

useEffect(() => {
  // Dynamic import prevents SSR errors
  import("leaflet").then((L) => {
    const map = L.map(containerRef.current, {
      center: [51.5, -0.09],
      zoom: 13,
    });
    setMap(map);
  });
}, []);
Enter fullscreen mode Exit fullscreen mode

All map components are client components ('use client'), but the page itself can be a server component. Best of both worlds.

Context-Based State Management

Instead of prop drilling the map instance through 10 components, I use React Context:

// In MapProvider
const [map, setMap] = useState<LeafletMap | null>(null);

// In any child component
const map = useLeafletMap();
if (map) {
  map.setView([51.5, -0.09], 13);
}
Enter fullscreen mode Exit fullscreen mode

Clean, simple, no Redux needed.

Component Structure

MapProvider (Context)
└── LeafletMap (Initializes Leaflet)
    ├── LeafletTileLayer (Base map)
    ├── LeafletGeoJSON (Data overlay)
    ├── LeafletMarker (Custom markers)
    ├── MapSearchBar (Search UI)
    ├── MapLayersPanel (Layer switcher)
    ├── MapControls (Zoom, fullscreen, geolocation)
    ├── MapDetailsPanel (Country info)
    └── MapTopBar (Categories, theme, user menu)
Enter fullscreen mode Exit fullscreen mode

Each component is focused, testable, and reusable.

TypeScript All The Way

Every component, hook, and utility is fully typed:

interface LeafletMarkerProps {
  position: [number, number];
  popup?: string;
  draggable?: boolean;
  onDragEnd?: (position: [number, number]) => void;
}
Enter fullscreen mode Exit fullscreen mode

Your IDE will love you. Future you will love you.

Custom Hooks for Everything

// Access the map instance
const map = useLeafletMap();

// Control the map
const { zoomIn, zoomOut, resetView, toggleFullscreen } = useMapControls();

// Theme-aware tiles
const { tileProvider, currentProviderId, setProviderId } = useMapTileProvider();

// Geolocation
const { locateUser, isLocating, isAvailable } = useGeolocation();

// Theme toggle
const { theme, setTheme, toggleTheme } = useTheme();
Enter fullscreen mode Exit fullscreen mode

Project Structure

├── app/
│   ├── api/countries/     # Country search API
│   ├── map/               # Map page (Server Component)
│   └── page.tsx           # Landing page
├── components/
│   ├── landing/           # Hero, navigation, tech stack
│   ├── map/               # 14 map components
│   └── ui/                # shadcn/ui components
├── contexts/              # MapContext, ThemeContext
├── hooks/                 # 5 custom hooks
├── constants/             # Map config, tile providers
└── types/                 # TypeScript definitions
Enter fullscreen mode Exit fullscreen mode

Real-World Usage Example

Here's how you'd build a country explorer (like the demo):

"use client";

import { useState } from "react";
import {
  MapProvider,
  LeafletMap,
  LeafletTileLayer,
  LeafletGeoJSON,
  MapSearchBar,
} from "@/components/map";

export default function CountryExplorer() {
  const [country, setCountry] = useState(null);

  const handleCountrySelect = async (countryId: string) => {
    const res = await fetch(`/api/countries/${countryId}`);
    const data = await res.json();
    setCountry(data);
  };

  return (
    <div className="h-screen">
      <MapProvider>
        <LeafletMap>
          <LeafletTileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
          <LeafletGeoJSON data={country} />
        </LeafletMap>
        <MapSearchBar
          onCountrySelect={handleCountrySelect}
          selectedCountry={country}
        />
      </MapProvider>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

That's it. ~25 lines of code for a fully functional country explorer with search, visualization, and smooth animations.


What's Next? (The Roadmap)

v1.1.0 (Coming Soon)

  • 📐 Drawing tools (polygons, circles, polylines)
  • 📏 Measurement tools (distance, area)
  • 📸 Export map as image
  • 🔢 Marker clustering for large datasets

The Big Picture: A Complete Starter Kit Series

This is just the beginning. I'm building a complete series of web mapping starters:

Starter Status
Next.js + Leaflet ✅ Available
Next.js + OpenLayers 🚧 Coming Soon
Next.js + MapLibre GL 🚧 Coming Soon

Each will have the same polished UI and developer experience, but with different mapping libraries.


Try It Yourself

The repo is open source (MIT license):

🔗 GitHub: github.com/wellywahyudi/nextjs-leaflet-starter

Quick Start

# Clone the repository
git clone https://github.com/wellywahyudi/nextjs-leaflet-starter.git
cd nextjs-leaflet-starter

# Install dependencies
npm install

# Start development server
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000/map and you're mapping.


Who Is This For?

This starter is designed for developers and teams who want a fast, modern foundation for building map-based applications. It’s a perfect fit if you're:

  • 🏢 Building a location-based product — delivery apps, real-estate platforms, mobility tools, etc.
  • 📊 Creating a geospatial or data-visualization dashboard
  • 🗺️ Developing a GIS application using GeoJSON, layers, and spatial analysis
  • 📚 Learning web mapping with Next.js 16 and Leaflet
  • ❤️ Someone who simply loves maps

Contributing

I'd love your help making this better:

The codebase is clean, well-documented, and beginner-friendly.


Final Thoughts

What started as “I just need a simple map” quickly turned into a mission to build the Next.js mapping starter I always wished existed. It’s not perfect—nothing ever is—but it’s fast, modern, open source, and solves real-world problems developers hit every day.

If you end up building something cool with it, I’d genuinely love to see it. Drop a link in the comments or reach out anytime.

And if this starter saves you even a few hours of setup, a ⭐ on GitHub would mean a lot.

Happy mapping! 🗺️✨


Links

Top comments (0)