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.
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
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>
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

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}
/>
2. 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',
}
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
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>;
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,
}}
/>
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
useGeolocationhook) - ⛶ Fullscreen toggle
const { zoomIn, zoomOut, resetView, toggleFullscreen } = useMapControls();
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);
}}
/>
9. Error Boundaries & Loading States
Production-ready error handling:
<MapErrorBoundary>
<MapProvider>
<LeafletMap>{/* Your map */}</LeafletMap>
<MapLoadingSpinner />
</MapProvider>
</MapErrorBoundary>
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);
});
}, []);
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);
}
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)
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;
}
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();
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
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>
);
}
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
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:
- 🐛 Found a bug? Open an issue
- 💡 Have an idea? Start a discussion
- 🔧 Want to contribute? Submit a PR
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
- Full Demo https://nextjs-leaflet-starter.vercel.app
- GitHub Repository https://github.com/wellywahyudi/nextjs-leaflet-starter
- My Profile https://github.com/wellywahyudi


Top comments (0)