Skip to content

About

A Next.js playground for MapLibre GL-based map interaction modes. Each mode is a small, independently reusable React component.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

NextJs Maplibre

A Next.js playground for MapLibre GL-based map interaction modes. Each mode is a small, independently reusable React component.

Purpose

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.

Routes

  • / — 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.

Screenshots

/upper-map — oblique pan/rotate view, with the source panel open:

Upper Map Controls

/first-person — eye-level walking view:

First Person Map Control

/routing — turn-by-turn directions for a routed trip:

Turn by turn routing

Setup

npm install
npm run dev

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

Project structure

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)

Notable gotchas (solved here, worth knowing if you build on this)

  • MapLibre + Turbopack renders a blank map. MapLibre resolves its worker script from import.meta.url, which isn't a real http(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 via maplibregl.setWorkerUrl(...) pointing at a real copy of the worker file (see copy:map-worker above, or the CDN URL used in the reusable snippets for zero-setup portability).
  • A tilted map div collapses to 0 height. maplibre-gl.css sets .maplibregl-map { position: relative } and loads after Tailwind, so a Tailwind absolute inset-0 class 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. FirstPersonMapControls clamps the computed zoom to keep buildings/streets visible.
  • Valhalla's polyline precision is 6, not 5. Its route shape field 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.ts decodes at precision 6. (Also: the shape_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.

License

MIT — see LICENSE.

About

A Next.js playground for MapLibre GL-based map interaction modes. Each mode is a small, independently reusable React component.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages