High-performance MapLibre primitives. An open source tool by OpenSite AI
pnpm add @page-speed/maps maplibre-gl react-map-glimport { MapLibre } from "@page-speed/maps";
export function Example() {
return (
<div style={{ width: "100%", height: 420 }}>
<MapLibre
stadiaApiKey={process.env.NEXT_PUBLIC_STADIA_API ?? ""}
mapStyle="osm-bright"
viewState={{ latitude: 40.7128, longitude: -74.006, zoom: 12 }}
markers={[
{
id: "nyc",
latitude: 40.7128,
longitude: -74.006,
label: "New York"
}
]}
/>
</div>
);
}- Explicit Stadia auth: no hard-coded keys
- Auto-loads MapLibre CSS: no extra stylesheet import required
- Keyless fallback map style: if no Stadia key is available, roads/landmarks still render via Carto Positron
- Tree-shakable exports: import only what you need
- Auto-centering hooks: compute optimal center and zoom for any set of coordinates
- Drop-in API compatibility: works with the
MapLibrecomponent used indt-cms
The main map component. Renders a MapLibre GL map with markers, controls, and full interactivity.
import { MapLibre } from "@page-speed/maps";
<MapLibre
stadiaApiKey="your-api-key"
mapStyle="osm-bright"
viewState={{ latitude: 33.4484, longitude: -112.074, zoom: 10 }}
markers={[
{ id: "phx", latitude: 33.4484, longitude: -112.074, label: "Phoenix" }
]}
showNavigationControl
showGeolocateControl
onClick={(coord) => console.log("Clicked:", coord)}
onMoveEnd={(center, zoom, bounds) => console.log("Moved:", center, zoom)}
/>| Prop | Type | Default | Description |
|---|---|---|---|
stadiaApiKey |
string |
required | Your Stadia Maps API key |
mapStyle |
string |
"osm-bright" |
Built-in style name or custom style URL |
viewState |
Partial<MapViewState> |
- | Controlled view state (lat, lng, zoom) |
onViewStateChange |
(state) => void |
- | Callback when view state changes |
markers |
Array<MapLibreMarker | BasicMarkerInput> |
[] |
Array of markers to display |
center |
{ lat, lng } |
- | Initial center (alternative to viewState) |
zoom |
number |
14 |
Initial zoom level |
styleUrl |
string |
- | Custom style URL (overrides mapStyle) |
onClick |
(coord) => void |
- | Map click handler |
onMoveEnd |
(center, zoom, bounds) => void |
- | Called when map stops moving |
onMarkerDrag |
(markerId, coord) => void |
- | Called when draggable marker moves |
showNavigationControl |
boolean |
true |
Show zoom/rotation controls |
showGeolocateControl |
boolean |
false |
Show user location button |
navigationControlPosition |
MapControlPosition |
"bottom-right" |
Position of nav controls |
geolocateControlPosition |
MapControlPosition |
"top-left" |
Position of geolocate button |
flyToOptions |
MapLibreFlyToOptions |
{} |
Animation options for flyTo |
className |
string |
- | CSS class for wrapper div |
style |
CSSProperties |
- | Inline styles for wrapper |
children |
ReactNode |
- | Additional map layers/overlays |
mapLibreCssHref |
string |
jsDelivr CDN | Custom MapLibre CSS URL |
Computes the geographic center of an array of coordinates using the Cartesian 3D averaging method. Handles antimeridian crossing and polar coordinates correctly.
import { useGeoCenter } from "@page-speed/maps/hooks/useGeoCenter";
// or
import { useGeoCenter } from "@page-speed/maps";
const markers = [
{ lat: 33.4585, lng: -112.0715 }, // Downtown Phoenix
{ lat: 33.6510, lng: -111.9244 }, // Scottsdale
{ lat: 33.3062, lng: -111.8413 }, // Mesa
];
const center = useGeoCenter(markers);
// Result: { lat: 33.4719, lng: -111.9457 }function useGeoCenter(coordinates: GeoCoordinate[]): GeoCenterResult | null;
function computeGeoCenter(coordinates: GeoCoordinate[]): GeoCenterResult | null;
interface GeoCoordinate {
lat: number;
lng: number;
}
interface GeoCenterResult {
lat: number;
lng: number;
}- Empty array: Returns
null - Single coordinate: Returns that coordinate
- Multiple coordinates: Returns the geographic midpoint
Computes the optimal zoom level to fit all coordinates within a given viewport, using Mercator projection math. Uses MapLibre's native 512px tile size.
import { useDefaultZoom } from "@page-speed/maps/hooks/useDefaultZoom";
// or
import { useDefaultZoom } from "@page-speed/maps";
const markers = [
{ lat: 33.4585, lng: -112.0715 },
{ lat: 33.6510, lng: -111.9244 },
];
const zoom = useDefaultZoom({
coordinates: markers,
mapWidth: 600,
mapHeight: 400,
padding: 50,
maxZoom: 16,
minZoom: 1,
});
// Result: ~10.5 (fits both markers with padding)function useDefaultZoom(options: DefaultZoomOptions): number | null;
function computeDefaultZoom(options: DefaultZoomOptions): number | null;
interface DefaultZoomOptions {
coordinates: GeoCoordinate[];
mapWidth: number;
mapHeight: number;
padding?: number; // default: 50
maxZoom?: number; // default: 18
minZoom?: number; // default: 1
}- Empty array: Returns
null - Single coordinate: Returns
maxZoom - Multiple coordinates: Returns the highest zoom that fits all markers with padding
- Invalid dimensions: Returns
nullorminZoom
import { MapLibre, useGeoCenter, useDefaultZoom } from "@page-speed/maps";
function AutoCenteringMap({ locations }) {
const coordinates = locations.map(loc => ({
lat: loc.latitude,
lng: loc.longitude,
}));
const center = useGeoCenter(coordinates);
const zoom = useDefaultZoom({
coordinates,
mapWidth: 800,
mapHeight: 600,
padding: 60,
});
if (!center) return <div>No locations to display</div>;
return (
<div style={{ width: 800, height: 600 }}>
<MapLibre
stadiaApiKey={process.env.NEXT_PUBLIC_STADIA_API ?? ""}
viewState={{
latitude: center.lat,
longitude: center.lng,
zoom: zoom ?? 10,
}}
markers={locations.map((loc, i) => ({
id: loc.id ?? i,
latitude: loc.latitude,
longitude: loc.longitude,
label: loc.name,
}))}
/>
</div>
);
}Resolves a style name or URL to a fully-qualified MapLibre style URL with authentication.
import { getMapLibreStyleUrl } from "@page-speed/maps/utils/style-url";
const url = getMapLibreStyleUrl("osm-bright", "your-api-key");
// "https://tiles.stadiamaps.com/styles/osm_bright.json?api_key=your-api-key"
const fallback = getMapLibreStyleUrl("osm-bright", "");
// "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json" (keyless fallback)Appends the Stadia API key to a style URL if it's a Stadia Maps URL.
import { appendStadiaApiKey } from "@page-speed/maps/utils/style-url";
const url = appendStadiaApiKey(
"https://tiles.stadiamaps.com/styles/osm_bright.json",
"your-api-key"
);
// "https://tiles.stadiamaps.com/styles/osm_bright.json?api_key=your-api-key"Generates a Google Maps URL for a location.
import { generateGoogleMapLink } from "@page-speed/maps/utils/google-links";
const url = generateGoogleMapLink(33.4484, -112.074, 15);
// "https://www.google.com/maps/@33.4484,-112.074,15z"Generates a Google Maps directions URL to a destination.
import { generateGoogleDirectionsLink } from "@page-speed/maps/utils/google-links";
const url = generateGoogleDirectionsLink(33.4484, -112.074);
// "https://www.google.com/maps/dir/?api=1&destination=33.4484,-112.074"All types are exported from @page-speed/maps/types or the main entry point:
import type {
BasicMarkerInput,
MapControlPosition,
MapCoordinate,
MapLibreFlyToOptions,
MapLibreMarker,
MapLibreProps,
MapViewState,
GeoCoordinate,
GeoCenterResult,
DefaultZoomOptions,
MapLibreBuiltInStyle,
} from "@page-speed/maps";type MapViewState = {
latitude: number;
longitude: number;
zoom: number;
};
type MapCoordinate = {
latitude: number;
longitude: number;
};
type GeoCoordinate = {
lat: number;
lng: number;
};
type BasicMarkerInput = {
id?: string | number;
latitude: number;
longitude: number;
color?: string;
draggable?: boolean;
label?: string;
element?: (() => React.ReactNode) | React.ReactNode;
onClick?: () => void;
};
type MapControlPosition =
| "top-left"
| "top-right"
| "bottom-left"
| "bottom-right";This package supports granular tree-shaking. Import only what you need:
// Full bundle (all exports)
import { MapLibre, useGeoCenter, useDefaultZoom } from "@page-speed/maps";
// Just the component
import { MapLibre } from "@page-speed/maps/core";
// Just hooks (tree-shakable)
import { useGeoCenter, useDefaultZoom } from "@page-speed/maps/hooks";
// Individual hooks (maximum tree-shaking)
import { useGeoCenter } from "@page-speed/maps/hooks/useGeoCenter";
import { useDefaultZoom } from "@page-speed/maps/hooks/useDefaultZoom";
// Just utilities
import { getMapLibreStyleUrl } from "@page-speed/maps/utils/style-url";
import { generateGoogleMapLink } from "@page-speed/maps/utils/google-links";
// Just types
import type { MapLibreProps } from "@page-speed/maps/types";Built-in style presets (requires Stadia API key):
| Style Name | Description |
|---|---|
osm-bright |
Clean, bright OpenStreetMap style (default) |
alidade-smooth |
Modern, smooth cartography |
alidade-smooth-dark |
Dark theme variant |
stadia-outdoors |
Outdoor/terrain focused |
stamen-toner |
High-contrast black & white |
stamen-terrain |
Terrain with hillshading |
stamen-watercolor |
Artistic watercolor style |
maplibre-default |
Carto Positron (no API key required) |
<MapLibre mapStyle="stamen-terrain" stadiaApiKey="..." />Or use a custom style URL:
<MapLibre styleUrl="https://your-tiles.com/style.json" stadiaApiKey="..." />Pass a custom React element for full control over marker rendering:
<MapLibre
stadiaApiKey="..."
markers={[
{
id: "custom",
latitude: 33.4484,
longitude: -112.074,
element: (
<div className="custom-marker">
<img src="/pin.svg" alt="Location" />
<span>Phoenix HQ</span>
</div>
),
},
]}
/><MapLibre
stadiaApiKey="..."
markers={[
{
id: "draggable",
latitude: 33.4484,
longitude: -112.074,
draggable: true,
label: "Drag me!",
},
]}
onMarkerDrag={(markerId, coord) => {
console.log(`Marker ${markerId} moved to:`, coord);
}}
/>function ControlledMap() {
const [viewState, setViewState] = useState({
latitude: 33.4484,
longitude: -112.074,
zoom: 12,
});
return (
<>
<MapLibre
stadiaApiKey="..."
viewState={viewState}
onViewStateChange={setViewState}
/>
<button onClick={() => setViewState(prev => ({ ...prev, zoom: prev.zoom + 1 }))}>
Zoom In
</button>
</>
);
}<MapLibre
stadiaApiKey="..."
viewState={viewState}
flyToOptions={{
speed: 1.2,
curve: 1.5,
easing: (t) => t,
}}
/>This package provides the core map primitives. For feature-rich components with clustering, info windows, and styled markers, see @opensite/ui which builds on top of @page-speed/maps:
// In @opensite/ui (consumer library)
import { useGeoCenter, useDefaultZoom, type GeoCoordinate } from "@page-speed/maps/hooks";
function GeoMap({ markers, clusters, defaultViewState }) {
// Collect all coordinates
const allCoordinates: GeoCoordinate[] = [
...markers.map(m => ({ lat: m.latitude, lng: m.longitude })),
...clusters.map(c => ({ lat: c.latitude, lng: c.longitude })),
];
// Auto-compute center and zoom
const geoCenter = useGeoCenter(allCoordinates);
const defaultZoom = useDefaultZoom({
coordinates: allCoordinates,
mapWidth: 600,
mapHeight: 520,
padding: 60,
});
return (
<MapLibre
viewState={{
latitude: defaultViewState?.latitude ?? geoCenter?.lat ?? 0,
longitude: defaultViewState?.longitude ?? geoCenter?.lng ?? 0,
zoom: defaultViewState?.zoom ?? defaultZoom ?? 10,
}}
// ... clustering, custom markers, info windows, etc.
/>
);
}| Export | Path | Description |
|---|---|---|
MapLibre |
@page-speed/maps |
Main map component |
DTMapLibreMap |
@page-speed/maps |
Alias for MapLibre |
useGeoCenter |
@page-speed/maps/hooks/useGeoCenter |
Geographic center hook |
computeGeoCenter |
@page-speed/maps/hooks/useGeoCenter |
Pure function version |
useDefaultZoom |
@page-speed/maps/hooks/useDefaultZoom |
Auto-zoom hook |
computeDefaultZoom |
@page-speed/maps/hooks/useDefaultZoom |
Pure function version |
getMapLibreStyleUrl |
@page-speed/maps/utils/style-url |
Style URL resolver |
appendStadiaApiKey |
@page-speed/maps/utils/style-url |
API key appender |
generateGoogleMapLink |
@page-speed/maps/utils/google-links |
Google Maps link |
generateGoogleDirectionsLink |
@page-speed/maps/utils/google-links |
Google Directions link |
BSD-3-Clause. See LICENSE for details.
Full-featured map component with markers, clusters, rich media panels, and automatic view calculation.
import { GeoMap, createMapMarkerElement } from "@page-speed/maps";
import type { GeoMapMarker } from "@page-speed/maps";
const markers: GeoMapMarker[] = [
{
id: 'office',
latitude: 40.7128,
longitude: -74.0060,
title: 'New York Office',
summary: 'Our headquarters in downtown Manhattan',
locationLine: '123 Broadway, New York, NY 10001',
hoursLine: 'Mon-Fri: 9:00 AM - 6:00 PM',
mediaItems: [
{ id: '1', src: '/office.jpg', alt: 'Office' },
],
markerElement: createMapMarkerElement({ size: 'lg' }),
actions: [
{
label: 'Get Directions',
href: 'https://maps.app.goo.gl/example',
},
],
},
];
<GeoMap
markers={markers}
stadiaApiKey="your-key"
panelPosition="bottom-left"
showNavigationControl
/>Key Features:
- β Auto-calculated center and zoom from markers
- β Rich media carousels (images/videos)
- β Interactive marker panels
- β Clustering support
- β Custom marker elements
- β Action buttons and links
Beautiful concentric circle markers with hover and selection states.
import { MapMarker, NeutralMapMarker, createMapMarkerElement } from "@page-speed/maps";
// Direct usage
<MapMarker
size="lg"
isSelected
dotColor="#1E40AF"
innerRingColor="#3B82F6"
middleRingColor="#93C5FD"
outerRingColor="#DBEAFE"
/>
// With GeoMap
const markers = [{
id: 'loc-1',
latitude: 40.7128,
longitude: -74.0060,
markerElement: createMapMarkerElement({ size: 'lg' }),
}];Sizes: sm | md | lg
Pre-configured: NeutralMapMarker for neutral gray design
If you're migrating map components from @opensite/ui to @page-speed/maps, see our comprehensive migration guide:
Key Changes:
- β Fixed zoom/centering bugs
- β New MapMarker components
- β Better tree-shakability
- β Optional peer dependencies for icons/images
- Examples - Complete code examples
- Migration Guide - Migrating from @opensite/ui
- API Reference - Full API documentation
- Ecosystem Guidelines - Performance standards
- @page-speed/img - Optimized image component
- @page-speed/video - Performance video component
- @page-speed/icon - Icon system
- @page-speed/forms - Form components
Made with β€οΈ for the DashTrack Platform
