Why I Built This (Again)
A few weeks ago, I released my Next.js + Leaflet starter kit as the first step in a mapping starter-kit series I’ve been planning. After wrapping that one up, the next logical piece was OpenLayers—especially for developers working on more advanced GIS workflows.
Leaflet is great for lightweight, interactive maps, but many GIS-driven projects need features like vector tiles, custom projections (EPSG:3857 isn’t always enough), high-precision geometry rendering, advanced interactions, and performant handling of large GeoJSON datasets. That’s where OpenLayers really shines.
So I rebuilt the entire starter from the ground up—same UI, same structure, same developer experience—but powered by OpenLayers to support real GIS use cases.
This isn’t a replacement for the Leaflet version.
It’s simply the next step in the series for developers who need a more capable, GIS-focused mapping engine.
And once this OpenLayers kit is out, I’ll be completing the lineup with a Next.js + MapLibre starter kit, designed for modern vector-tile–driven maps and 3D visualizations.
What I Built
A production-ready Next.js 16 starter with vanilla OpenLayers and the same Google Maps-inspired UI from my Leaflet starter.
The Stack
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 16 | App Router, Server Components |
| React | 19 | UI Framework |
| OpenLayers | 10 | Mapping (vanilla, no wrapper) |
| TypeScript | 5 | Type Safety |
| Tailwind CSS | 4 | Styling |
| shadcn/ui | Latest | Accessible UI Components |
What's Included
This isn't a minimal example. It's a full-featured starter with everything I needed when building real map apps.
Core Features
- ✅ Base map setup — OpenLayers Map and View with proper initialization
- ✅ Multiple tile providers — OpenStreetMap, Satellite, Dark mode
- ✅ Theme-aware tiles — Auto-switches based on light/dark mode
- ✅ GeoJSON rendering — With custom styling and fit-to-bounds
- ✅ Country search — Debounced search with keyboard navigation
- ✅ Custom markers — Add markers anywhere with popups
- ✅ Context menu — Right-click to copy coordinates, add markers, measure
- ✅ Measurement tools — Distance and area with interactive drawing
- ✅ POI management — Full CRUD with 14 categories, localStorage persistence
- ✅ Geolocation — Find user location with accuracy circle
- ✅ Responsive layout — Mobile drawer, desktop sidebar
- ✅ Error boundaries — Graceful error handling
- ✅ Dark mode — Full theme support
What Makes It Different
1. Coordinate conversion utilities
OpenLayers uses [lng, lat] with projections. The rest of the world uses [lat, lng]. I built utilities to handle this:
import { latLngToOL, olToLatLng } from "@/lib/utils/coordinates";
// Convert [lat, lng] to OpenLayers coordinate
const olCoord = latLngToOL([51.505, -0.09]);
// Convert back
const latLng = olToLatLng(olCoord);
2. Proper layer management
OpenLayers requires explicit layer add/remove. I built hooks that handle this:
const { tileProvider } = useMapTileProvider();
<OpenLayersTileLayer provider={tileProvider} />;
3. Memory leak prevention
OpenLayers needs manual cleanup. Every component properly disposes of resources:
useEffect(() => {
// ... map initialization
return () => {
map.setTarget(undefined); // Detach from DOM
map.dispose(); // Dispose of resources
};
}, []);
Code Examples
Basic Map Setup
"use client";
import { MapProvider } from "@/contexts/MapContext";
import { OpenLayersMap, OpenLayersTileLayer } from "@/components/map";
export default function MapPage() {
return (
<MapProvider>
<OpenLayersMap center={[51.505, -0.09]} zoom={13}>
<OpenLayersTileLayer provider={osmProvider} />
</OpenLayersMap>
</MapProvider>
);
}
Adding GeoJSON
import { OpenLayersGeoJSON } from "@/components/map";
<OpenLayersGeoJSON
data={countryData}
style={{
color: "#2563eb",
fillColor: "#3b82f6",
fillOpacity: 0.2,
}}
fitBounds={true}
/>;
Using Map Controls
import { useMapControls } from "@/hooks/useMapControls";
function MapControls() {
const { zoomIn, zoomOut, resetView, flyTo } = useMapControls();
return (
<div>
<button onClick={zoomIn}>Zoom In</button>
<button onClick={zoomOut}>Zoom Out</button>
<button onClick={resetView}>Reset</button>
<button onClick={() => flyTo([51.505, -0.09], 13)}>Fly to London</button>
</div>
);
}
Managing POIs
import { usePOIManager } from "@/hooks/usePOIManager";
function POIPanel() {
const { pois, addPOI, deletePOI, exportGeoJSON } = usePOIManager();
const handleAdd = () => {
addPOI("Coffee Shop", 51.505, -0.09, "food-drink", "Great espresso");
};
return (
<div>
<button onClick={handleAdd}>Add POI</button>
<button onClick={exportGeoJSON}>Export</button>
<ul>
{pois.map((poi) => (
<li key={poi.id}>
{poi.title}
<button onClick={() => deletePOI(poi.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
Measurement Tools
import { useMeasurement } from "@/hooks/useMeasurement";
function MeasurementPanel() {
const { startMeasurement, clearMeasurement, distance, area } =
useMeasurement();
return (
<div>
<button onClick={() => startMeasurement("distance")}>
Measure Distance
</button>
<button onClick={() => startMeasurement("area")}>Measure Area</button>
<button onClick={clearMeasurement}>Clear</button>
{distance > 0 && <p>Distance: {(distance / 1000).toFixed(2)} km</p>}
{area > 0 && <p>Area: {(area / 1000000).toFixed(2)} km²</p>}
</div>
);
}
Screenshots
Main Map Interface
POI Management Panel
Measurement Tools
Mobile Responsive
Getting Started
The repo is open source (MIT license):
🔗 GitHub: github.com/wellywahyudi/nextjs-openlayers-starter
Installation
# Clone the repository
git clone https://github.com/wellywahyudi/nextjs-openlayers-starter.git
cd nextjs-openlayers-starter
# Install dependencies
npm install
# Start development server
npm run dev
Open http://localhost:3000/map and you're ready to go.
Customizing the Default View
Edit constants/map-config.ts:
export const DEFAULT_MAP_CONFIG: MapConfig = {
defaultCenter: [51.505, -0.09], // [lat, lng]
defaultZoom: 13,
minZoom: 3,
maxZoom: 18,
};
Adding Custom Tile Providers
Edit constants/tile-providers.ts:
export const TILE_PROVIDERS: TileProvider[] = [
{
id: "custom",
name: "My Custom Tiles",
url: "https://your-tile-server/{z}/{x}/{y}.png",
attribution: "© Your Attribution",
maxZoom: 19,
category: "standard",
},
// ...existing providers
];
Who Should Use This?
This starter is for you if:
- 🗺️ You're building a GIS application — spatial analysis, custom projections, vector tiles
- 📊 You need to render large datasets — 10,000+ features, complex geometries
- 🎯 You want advanced interactions — drawing, editing, snapping, measurement
- 🚀 You're prototyping a map-heavy app — dashboards, analytics, visualization
- 📚 You're learning OpenLayers — and want a clean, modern starting point
If you just need a simple map with markers, stick with the Leaflet version. It's simpler.
If you need power and flexibility, this is for you.
OpenLayers-Specific Notes
Coordinate Systems
OpenLayers uses EPSG:3857 (Web Mercator) by default and expects [lng, lat] order. The starter handles conversion automatically:
// External API uses [lat, lng]
const userInput = [51.505, -0.09];
// Convert to OpenLayers format
const olCoord = latLngToOL(userInput);
// Use with OpenLayers
map.getView().setCenter(olCoord);
Layer Stack
Layers are rendered in order (bottom to top):
- Base Tile Layer
- GeoJSON Vector Layer
- POI Vector Layer
- Marker Vector Layer
- Measurement Vector Layer
Tree-Shaking
OpenLayers is fully tree-shakeable. Import only what you need:
// ✅ Good - tree-shakeable
import Map from "ol/Map";
import View from "ol/View";
import TileLayer from "ol/layer/Tile";
// ❌ Bad - imports everything
import * as ol from "ol";
Bundle Size
- Leaflet version: ~50KB
- OpenLayers version: ~90KB
The increase is worth it for the features you get.
What's Next?
I'm not done. Here's what I'm planning:
Short-term
I'm building a complete series of Next.js mapping starters:
| Starter | Status |
|---|---|
| Next.js + Leaflet | ✅ Available |
| Next.js + OpenLayers | ✅ Available |
| Next.js + MapLibre GL | 🚧 Planned |
Same UI, same DX, different mapping libraries. Pick the one that fits your project.
Contributing
This is open source. If you find bugs, have ideas, or want to contribute:
The codebase is clean, documented, and beginner-friendly.
Final Thoughts
This starter took weeks to build. I migrated every component from Leaflet to OpenLayers, rewrote coordinate handling, fixed memory leaks, and tested on mobile.
It's not perfect. There are edge cases I haven't hit yet. But it's solid, it's fast, and it solves real problems.
If you're building something with OpenLayers and Next.js, this will save you days of setup. If it helps you ship faster, that's all I wanted.
And if you build something cool with it, I'd love to see it. Drop a link in the comments.
A ⭐ on GitHub would mean a lot.
Happy mapping! 🗺️✨
Links
- GitHub Repository https://github.com/wellywahyudi/nextjs-openlayers-starter
- Leaflet Version https://github.com/wellywahyudi/nextjs-leaflet-starter
- My Profile https://github.com/wellywahyudi





Top comments (0)