mapglass
v0.1.0
Published
Beautiful React map components built on MapLibre GL for Next.js
Readme
mapglass
Polished React map components built on MapLibre GL. Markers, popups, clustering, and controls — with a glassy UI aesthetic and first-class Next.js support.
Installation
npm i mapglass maplibre-glPeer dependencies:
react >= 18,react-dom >= 18,maplibre-gl >= 4
Quick start
import {
Map,
Marker,
Popup,
ZoomControl,
ResetViewButton,
ViewportBadge,
} from "mapglass";
export function MyMap() {
return (
<Map
initialViewState={{ center: [2.3522, 48.8566], zoom: 11 }}
className="h-[500px]"
>
<Map.Controls>
<ZoomControl />
<ResetViewButton />
</Map.Controls>
<Map.Overlay position="bottom-left">
<ViewportBadge />
</Map.Overlay>
<Marker lngLat={[2.3522, 48.8566]} onClick={({ lngLat }) => console.log(lngLat)}>
<Popup title="Paris" description="Capital of France" />
</Marker>
</Map>
);
}Components
<Map>
The root component. Initializes a MapLibre GL instance and provides context to all child components.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| initialViewState | ViewState | — | Required. Starting position (uncontrolled). |
| viewState | ViewState | — | Controlled view state. |
| onViewStateChange | (v: ViewState) => void | — | Called on every camera move. |
| mapStyle | string | — | Full MapLibre style URL. Overrides mapStylePreset. |
| mapStylePreset | "light" \| "dark" \| "streets" | "light" | Built-in style presets. |
| labelLanguage | "fr" \| "local" | "fr" | Map label language. "local" uses the style's default. |
| theme | "light" \| "dark" | "light" | UI theme for controls and overlays. |
| stylePreset | "glassy" \| "minimal" | "glassy" | Visual style of UI chrome. |
| accentColor | string | "#0ea5e9" | CSS color used as the primary accent. |
| className | string | — | Class applied to the outer wrapper. |
| mapClassName | string | — | Class applied to the map canvas element. |
| onLoad | (map: maplibregl.Map) => void | — | Called once the map style has loaded. |
// Controlled mode
const [view, setView] = useState<ViewState>({ center: [2.35, 48.86], zoom: 11 });
<Map viewState={view} onViewStateChange={setView} initialViewState={view}>
...
</Map><Map.Controls>
Positions a panel of control buttons on the map.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| position | "top-right" \| "top-left" | "top-right" | Corner to anchor the panel. |
| className | string | — | Extra classes on the panel. |
<Map.Controls position="top-right">
<ZoomControl />
<ResetViewButton />
</Map.Controls><Map.Overlay>
Absolutely positions any content at a corner of the map.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| position | "top-left" \| "top-right" \| "bottom-left" \| "bottom-right" | "bottom-left" | Corner to anchor the overlay. |
| className | string | — | Extra classes. |
<Map.Overlay position="bottom-right">
<div>Custom HUD content</div>
</Map.Overlay><Marker>
Renders a positioned dot marker that follows the map camera.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| lngLat | [number, number] | — | Required. [longitude, latitude]. |
| variant | "primary" \| "secondary" \| "danger" | "primary" | Color variant. |
| className | string | — | Extra classes on the marker dot. |
| onClick | (payload: MarkerClickPayload) => void | — | Click handler. Receives { map, lngLat }. |
Place a <Popup> as a child to show a card above the marker.
<Marker lngLat={[2.35, 48.86]} variant="primary" onClick={({ lngLat }) => console.log(lngLat)}>
<Popup open title="Paris" description="48.86, 2.35" />
</Marker><Popup>
A card that renders above its parent <Marker>. Must be a direct child of <Marker>.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | true | Whether the popup is visible. |
| title | string | — | Bold heading. |
| description | string | — | Muted subtext below the title. |
| className | string | — | Extra classes on the card. |
| children | ReactNode | — | Custom content rendered below title/description. |
<Popup open={selected === id} title="Lyon" description="45.76, 4.84">
<button onClick={() => setSelected(null)}>Close</button>
</Popup><ClusteredPointsLayer>
Renders a GeoJSON clustering layer directly on the MapLibre canvas. Clusters collapse/expand on click.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| points | ClusterPoint[] | — | Required. Array of { id, lat, lng, ...extras }. |
| id | string | "points-cluster" | Base ID for the MapLibre source and layers. |
| clusterRadius | number | 50 | Pixel radius for clustering. |
| clusterMaxZoom | number | 13 | Zoom level above which clustering is disabled. |
| clusterColor | string | "#0284c7" | Fill color of cluster circles. |
| pointColor | string | "#0ea5e9" | Fill color of individual point circles. |
| strokeColor | string | "#ffffff" | Stroke color for all circles. |
| onPointClick | (payload: ClusterPointClickPayload) => void | — | Called when an individual point is clicked. Receives { map, lngLat, properties }. |
<ClusteredPointsLayer
points={cities.map(c => ({ id: c.id, lat: c.lat, lng: c.lng, name: c.name }))}
clusterColor="#6366f1"
onPointClick={({ properties }) => setSelected(String(properties.id))}
/><ZoomControl>
Renders zoom-in / zoom-out buttons. No props. Use inside <Map.Controls>.
<ResetViewButton>
Animates the camera back to the map's initialViewState, or to a custom target.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| to | ViewState | initialViewState | Override the reset target. |
<FitAllButton>
Fits the camera to encompass a set of coordinates.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| points | [number, number][] | — | Required. Array of [lng, lat] coordinates. |
| padding | number | 56 | Viewport padding in pixels. |
| maxZoom | number | 12 | Maximum zoom level when fitting. |
const coords = pois.map(p => [p.lng, p.lat] as [number, number]);
<FitAllButton points={coords} maxZoom={10} /><ViewportBadge>
Displays the current latitude, longitude, and zoom level. No props. Use inside <Map.Overlay>.
TypeScript
All types are exported from the package root.
import type {
ViewState, // { center: [lng, lat], zoom, bearing?, pitch? }
LngLat, // [number, number]
MapTheme, // "light" | "dark"
MapStylePreset, // "light" | "dark" | "streets"
UiStylePreset, // "glassy" | "minimal"
OverlayPosition, // "top-left" | "top-right" | "bottom-left" | "bottom-right"
MapProps,
MarkerVariant, // "primary" | "secondary" | "danger"
MarkerClickPayload, // { map: maplibregl.Map | null, lngLat: LngLat }
ClusterPoint, // { id: string, lat: number, lng: number, [key: string]: unknown }
ClusterPointClickPayload, // { map, lngLat, properties }
} from "mapglass";Next.js (App Router)
The map is a client component. Wrap it in a "use client" file and import from a server page:
// components/my-map.tsx
"use client";
import { Map, ZoomControl, ViewportBadge } from "mapglass";
export function MyMap() {
return (
<Map
initialViewState={{ center: [2.3522, 48.8566], zoom: 11 }}
className="h-[500px]"
>
<Map.Controls>
<ZoomControl />
</Map.Controls>
<Map.Overlay position="bottom-left">
<ViewportBadge />
</Map.Overlay>
</Map>
);
}// app/page.tsx (server component)
import { MyMap } from "../components/my-map";
export default function Page() {
return <MyMap />;
}Tailwind CSS
Add the library to your content array so its classes are not purged:
// tailwind.config.ts
content: [
"./app/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./node_modules/mapglass/dist/**/*.{js,mjs,cjs}",
],The library uses CSS custom properties for theming (--map-accent, --map-surface, etc.), automatically applied based on the theme prop.
License
MIT
