A Next.js playground for MapLibre GL-based map interaction modes. Each mode is a small, independently reusable React component.
Help integrate MapLibre GL in Next.js. Getting a MapLibre map working correctly under Next.js/Turbopack has a handful of non-obvious gotchas (see below) — this repo works through them for three common map interaction patterns, with a live demo of each, a chunk-by-chunk explanation of the code next to it, and a fully self-contained, copy-paste-ready version of the component to drop straight into another project.
/— Dashboard. Lists the available map modes, each with a live Three.js 3D preview, and links to them./simple-map— The minimal setup: just a MapLibre map, no extra controls beyond MapLibre's own built-in drag/scroll/zoom-buttons./upper-map— Pan and rotate a tilted 3D view over the city from above, using WASD (or the on-screen buttons)./first-person— Walk the streets at eye level. W/S move forward and back, A/D strafe sideways, and the Left/Right arrow keys turn the view./routing— Click the map to add two or more stops and get a real route between them by car, bike, or on foot (via the Valhalla routing engine), including turn-by-turn directions.
Each map page also opens with a terminal-styled side panel that walks through the component's source chunk by chunk with plain-English explanations, and includes a second, fully self-contained version of the component you can copy-paste directly into another React/Next.js project.
/upper-map — oblique pan/rotate view, with the source panel open:
/first-person — eye-level walking view:
/routing — turn-by-turn directions for a routed trip:
npm install
npm run devOpen http://localhost:3000.
npm run dev and npm run build both run a predev/prebuild step
(copy:map-worker) that copies MapLibre's web worker files into public/.
This is required — see "Notable gotchas" below.
app/
page.tsx -- dashboard
simple-map/page.tsx -- minimal, no-frills map
upper-map/page.tsx -- oblique pan/rotate map
first-person/page.tsx -- eye-level walking map
routing/page.tsx -- Valhalla routing map
components/
SimpleMap.tsx -- the minimal map mode
UpperMapControls.tsx -- the oblique map mode
FirstPersonMapControls.tsx -- the first-person walking mode
RoutingMap.tsx -- the Valhalla routing mode
MapsDashboard.tsx -- dashboard grid
MapPreview3D.tsx -- Three.js preview used on dashboard cards
CodePanel.tsx -- the terminal-styled source/explanation panel
BackButton.tsx -- link back to the dashboard
lib/
code-explainers.ts -- annotated code chunks shown in CodePanel
reusable-snippets.ts -- the portable, drop-in version of each map component
valhalla.ts -- Valhalla /route client (fetch + polyline decoding)
- MapLibre + Turbopack renders a blank map. MapLibre resolves its worker
script from
import.meta.url, which isn't a realhttp(s)URL under Turbopack — it silently falls back to"", the worker loads the page's own HTML instead of the worker script, and zero tiles ever load, with no console error. Fixed viamaplibregl.setWorkerUrl(...)pointing at a real copy of the worker file (seecopy:map-workerabove, or the CDN URL used in the reusable snippets for zero-setup portability). - A tilted map div collapses to 0 height.
maplibre-gl.csssets.maplibregl-map { position: relative }and loads after Tailwind, so a Tailwindabsolute inset-0class loses that specificity fight. Fixed with inline styles, which always win. - True eye-level first-person needs a very high zoom (~21+ for a
reasonable look-ahead distance), which is well past where many vector
styles' tile data exists natively — past a certain amount of overzoom, this
particular style stops rendering entirely.
FirstPersonMapControlsclamps the computed zoom to keep buildings/streets visible. - Valhalla's polyline precision is 6, not 5. Its route
shapefield uses the same encoding as Google's polyline format, but at 6 decimal places of precision instead of the usual 5 — decoding with the standard precision silently produces garbage coordinates.lib/valhalla.tsdecodes at precision 6. (Also: theshape_format: "geojson"request param does not change the response format on the public demo server — manual decoding is required regardless.)
The in-app source panels on /upper-map, /first-person, and /routing go
through each of these in more detail, chunk by chunk, right next to the code
that fixes it.
Note: /routing uses the free public valhalla1.openstreetmap.de demo
server by default (no API key, but rate-limited and occasionally down). Pass
your own valhallaUrl prop to point at a self-hosted or commercial instance.
MIT — see LICENSE.


