That's the whole thing explained. Here's the same component as a single, self-contained file -- copy it into any React/Next.js project and it works as-is.
"use client";
/**
* Drop-in MapLibre "upper" map view -- pan and rotate a tilted 3D map with
* WASD (keyboard) or the on-screen buttons.
*
* Setup: npm install maplibre-gl react react-dom
*
* The worker script below loads from a CDN so this file needs zero extra
* setup. Pin the version in the URL to whatever maplibre-gl version you
* installed. (Why a CDN worker at all? MapLibre resolves its worker URL from
* import.meta.url, which isn't a real http(s) URL under bundlers like
* Turbopack/Webpack -- without this, it silently fails and no tiles load.)
*/
import * as maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { useEffect, useRef } from "react";
const KEYS = ["KeyW", "KeyA", "KeyS", "KeyD"] as const;
type Key = (typeof KEYS)[number];
export interface UpperMapControlsProps {
/** [lng, lat] to center the map on. */
origin?: [number, number];
/** Any MapLibre style URL. */
mapStyle?: string;
initialZoom?: number;
pitch?: number;
bearing?: number;
/** Pixels panned per movement tick. */
panDistance?: number;
/** Degrees turned per movement tick. */
turnDegrees?: number;
}
export default function UpperMapControls({
origin = [10.1815, 36.7992],
mapStyle = "https://tiles.openfreemap.org/styles/liberty",
initialZoom = 17,
pitch = 65,
bearing = -20,
panDistance = 60,
turnDegrees = 15,
}: UpperMapControlsProps) {
const mapContainerRef = useRef<HTMLDivElement | null>(null);
const keysDownRef = useRef<Set<Key>>(new Set());
useEffect(() => {
if (!mapContainerRef.current) return;
maplibregl.setWorkerUrl(
"https://unpkg.com/maplibre-gl@6.6.0/dist/maplibre-gl-worker.mjs",
);
const easing = (t: number) => t * (2 - t);
const map = new maplibregl.Map({
container: mapContainerRef.current,
style: mapStyle,
center: origin,
zoom: initialZoom,
pitch,
bearing,
interactive: false,
canvasContextAttributes: { antialias: true },
attributionControl: false,
});
map.addControl(new maplibregl.AttributionControl({ compact: true }), "top-right");
mapContainerRef.current
.querySelector(".maplibregl-ctrl-attrib")
?.classList.remove("maplibregl-compact-show");
const keysDown = keysDownRef.current;
const onKeyDown = (e: KeyboardEvent) => {
if ((KEYS as readonly string[]).includes(e.code)) keysDown.add(e.code as Key);
};
const onKeyUp = (e: KeyboardEvent) => {
keysDown.delete(e.code as Key);
};
window.addEventListener("keydown", onKeyDown);
window.addEventListener("keyup", onKeyUp);
const interval = window.setInterval(() => {
if (keysDown.has("KeyW")) map.panBy([0, -panDistance], { easing });
if (keysDown.has("KeyS")) map.panBy([0, panDistance], { easing });
if (keysDown.has("KeyA")) {
map.easeTo({ bearing: map.getBearing() - turnDegrees, duration: 120 });
}
if (keysDown.has("KeyD")) {
map.easeTo({ bearing: map.getBearing() + turnDegrees, duration: 120 });
}
}, 120);
return () => {
window.clearInterval(interval);
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("keyup", onKeyUp);
keysDown.clear();
map.remove();
};
}, [origin, mapStyle, initialZoom, pitch, bearing, panDistance, turnDegrees]);
const buttonStyle: React.CSSProperties = {
font: "700 18px system-ui, sans-serif",
color: "#e6edf3",
background: "linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0.03))",
border: "1px solid rgba(255,255,255,0.14)",
borderRadius: 10,
width: 52,
height: 52,
boxShadow: "0 2px 5px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.18)",
touchAction: "none",
transition: "transform 0.08s ease, filter 0.08s ease",
};
return (
<>
<div ref={mapContainerRef} style={{ position: "absolute", inset: 0 }} />
{/* Plain CSS for press feedback -- simplest way to add a ":active" rule
from inside a single self-contained component file. */}
<style>{`.touch-dpad-btn:active { transform: scale(0.93); filter: brightness(1.2); }`}</style>
<div
style={{
position: "absolute",
bottom: 16,
left: "50%",
transform: "translateX(-50%)",
display: "grid",
gridTemplateColumns: "repeat(3, 52px)",
gridTemplateRows: "repeat(2, 52px)",
gap: 8,
padding: 10,
borderRadius: 16,
background: "rgba(13, 17, 23, 0.55)",
backdropFilter: "blur(8px)",
WebkitBackdropFilter: "blur(8px)",
border: "1px solid rgba(255,255,255,0.08)",
boxShadow: "0 8px 24px rgba(0,0,0,0.35)",
}}
>
<button
type="button"
className="touch-dpad-btn"
style={{ ...buttonStyle, gridArea: "1 / 2" }}
onPointerDown={(e) => {
e.preventDefault();
e.currentTarget.setPointerCapture(e.pointerId);
keysDownRef.current.add("KeyW");
}}
onLostPointerCapture={() => keysDownRef.current.delete("KeyW")}
>
W
</button>
<button
type="button"
className="touch-dpad-btn"
style={{ ...buttonStyle, gridArea: "2 / 1" }}
onPointerDown={(e) => {
e.preventDefault();
e.currentTarget.setPointerCapture(e.pointerId);
keysDownRef.current.add("KeyA");
}}
onLostPointerCapture={() => keysDownRef.current.delete("KeyA")}
>
A
</button>
<button
type="button"
className="touch-dpad-btn"
style={{ ...buttonStyle, gridArea: "2 / 2" }}
onPointerDown={(e) => {
e.preventDefault();
e.currentTarget.setPointerCapture(e.pointerId);
keysDownRef.current.add("KeyS");
}}
onLostPointerCapture={() => keysDownRef.current.delete("KeyS")}
>
S
</button>
<button
type="button"
className="touch-dpad-btn"
style={{ ...buttonStyle, gridArea: "2 / 3" }}
onPointerDown={(e) => {
e.preventDefault();
e.currentTarget.setPointerCapture(e.pointerId);
keysDownRef.current.add("KeyD");
}}
onLostPointerCapture={() => keysDownRef.current.delete("KeyD")}
>
D
</button>
</div>
</>
);
}