Map

free

Map wraps MapLibre with markers, popups, routes, arcs, and point clustering, themed to match the rest of the UI.

maplibre-gl
Preview
Dark
import Map from '@/components/composites/Map';

export default function UsageDemo() {
  return (
    <div className="h-80 w-full">
      <Map center={[-122.4194, 37.7749]} zoom={11}>
        <Map.Marker longitude={-122.3937} latitude={37.7955}>
          <Map.MarkerContent />
          <Map.MarkerPopup>
            <div className="font-semibold text-foreground">
              Ferry Building
            </div>
            <p className="text-muted-foreground">
              Historic marketplace and transit hub on the Embarcadero.
            </p>
          </Map.MarkerPopup>
        </Map.Marker>
        <Map.Marker longitude={-122.4058} latitude={37.8024}>
          <Map.MarkerContent />
          <Map.MarkerPopup>
            <div className="font-semibold text-foreground">Coit Tower</div>
            <p className="text-muted-foreground">
              Art Deco tower with panoramic views of the bay.
            </p>
          </Map.MarkerPopup>
        </Map.Marker>
      </Map>
    </div>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add Map

Examples

Static Preview

Preview
Dark
import Map from '@/components/composites/Map';

export default function StaticPreviewDemo() {
  return (
    <div className="h-36 w-full">
      <Map
        center={[-97.7431, 30.2672]}
        zoom={14}
        interactive={false}
        attributionControl={false}
        className="h-full min-h-0 shadow-none"
      >
        <Map.Marker longitude={-97.7431} latitude={30.2672}>
          <Map.MarkerContent />
        </Map.Marker>
      </Map>
    </div>
  );
}

Popups & Labels

Preview
Dark
import Map from '@/components/composites/Map';

export default function PopupsLabelsDemo() {
  return (
    <div className="h-80 w-full">
      <Map center={[-122.42, 37.778]} zoom={13}>
        <Map.Marker longitude={-122.43} latitude={37.78}>
          <Map.MarkerContent />
          <Map.MarkerLabel>HQ</Map.MarkerLabel>
        </Map.Marker>
        <Map.Marker longitude={-122.41} latitude={37.77}>
          <Map.MarkerContent />
          <Map.MarkerTooltip>Warehouse — 12 units in stock</Map.MarkerTooltip>
        </Map.Marker>
        <Map.Popup longitude={-122.4} latitude={37.785}>
          <div className="font-semibold text-foreground">Zone closed</div>
          <p className="text-muted-foreground">
            Scheduled maintenance until Friday.
          </p>
        </Map.Popup>
      </Map>
    </div>
  );
}

Controls

Preview
Dark
import Map from '@/components/composites/Map';

export default function ControlsDemo() {
  return (
    <div className="h-80 w-full">
      <Map center={[-122.3321, 47.6062]} zoom={11}>
        <Map.Controls
          position="top-right"
          showZoom
          showCompass
          showLocate
          showFullscreen
        />
      </Map>
    </div>
  );
}

Route

Preview
Dark
import Map from '@/components/composites/Map';

const routeCoordinates: [number, number][] = [
  [-122.4194, 37.7749],
  [-122.4094, 37.7799],
  [-122.4008, 37.7852],
  [-122.3937, 37.7955],
];

export default function RouteDemo() {
  const [start, ...rest] = routeCoordinates;
  const end = rest[rest.length - 1];

  return (
    <div className="h-80 w-full">
      <Map center={[-122.406, 37.783]} zoom={10}>
        <Map.Route coordinates={routeCoordinates} interactive width={4} />
        <Map.Marker longitude={start[0]} latitude={start[1]}>
          <Map.MarkerContent />
        </Map.Marker>
        <Map.Marker longitude={end[0]} latitude={end[1]}>
          <Map.MarkerContent className="bg-success after:bg-success-foreground" />
        </Map.Marker>
      </Map>
    </div>
  );
}

Arc

Preview
Dark
import Map from '@/components/composites/Map';

const hubs = {
  sf: [-122.4194, 37.7749] as [number, number],
  newYork: [-74.006, 40.7128] as [number, number],
  london: [-0.1276, 51.5074] as [number, number],
  tokyo: [139.6917, 35.6895] as [number, number],
  sydney: [151.2093, -33.8688] as [number, number],
};

const arcData = [
  { id: 'sf-london', from: hubs.sf, to: hubs.london },
  { id: 'sf-tokyo', from: hubs.sf, to: hubs.tokyo },
  { id: 'sf-sydney', from: hubs.sf, to: hubs.sydney },
  { id: 'newyork-london', from: hubs.newYork, to: hubs.london },
];

export default function ArcDemo() {
  return (
    <div className="h-80 w-full">
      <Map center={[0, 20]} zoom={1}>
        <Map.Arc data={arcData} interactive />
      </Map>
    </div>
  );
}

Cluster Layer

Preview
Dark
import Map from '@/components/composites/Map';

const storeCoordinates: [number, number][] = [
  [-122.42, 37.77], [-122.41, 37.78], [-122.44, 37.76],
  [-122.39, 37.75], [-122.43, 37.8], [-122.4, 37.73],
  [-118.24, 34.05], [-118.29, 34.02], [-118.2, 34.08],
  [-118.33, 34.06], [-118.22, 34.01], [-118.27, 34.09],
  [-117.16, 32.72], [-117.2, 32.75], [-117.12, 32.7],
  [-117.18, 32.68], [-117.1, 32.74], [-117.22, 32.71],
  [-121.49, 38.58], [-121.45, 38.55], [-121.52, 38.6],
  [-121.47, 38.62], [-121.53, 38.56], [-121.44, 38.59],
];

const storeLocations: GeoJSON.FeatureCollection<GeoJSON.Point> = {
  type: 'FeatureCollection',
  features: storeCoordinates.map((coordinates, index) => ({
    type: 'Feature',
    properties: { storeId: index },
    geometry: { type: 'Point', coordinates },
  })),
};

export default function ClusterLayerDemo() {
  return (
    <div className="h-80 w-full">
      <Map center={[-119.4179, 36.7783]} zoom={5}>
        <Map.ClusterLayer data={storeLocations} />
      </Map>
    </div>
  );
}

Controlled Viewport

Preview
Dark
40.71, -74.01 · zoom 11.0
import { useState } from 'react';
import Segment from '@/components/ui/Segment';
import Map from '@/components/composites/Map';

const cities = [
  { label: 'New York', center: [-74.006, 40.7128] as [number, number] },
  { label: 'London', center: [-0.1276, 51.5074] as [number, number] },
  { label: 'Tokyo', center: [139.6917, 35.6895] as [number, number] },
  { label: 'Sydney', center: [151.2093, -33.8688] as [number, number] },
];

export default function ControlledViewportDemo() {
  const [active, setActive] = useState(cities[0].label);
  const [viewport, setViewport] = useState({
    center: cities[0].center,
    zoom: 11,
    bearing: 0,
    pitch: 0,
  });

  const handleChange = (label: string) => {
    const city = cities.find((c) => c.label === label);
    if (!city) return;
    setActive(label);
    setViewport({ center: city.center, zoom: 11, bearing: 0, pitch: 0 });
  };

  return (
    <div className="w-full space-y-4">
      <div className="flex justify-center">
        <Segment value={active} onChange={handleChange}>
          {cities.map((city) => (
            <Segment.Item key={city.label} value={city.label}>
              {city.label}
            </Segment.Item>
          ))}
        </Segment>
      </div>
      <div className="h-72">
        <Map viewport={viewport} onViewportChange={setViewport} className="h-full" />
      </div>
      <div className="text-xs text-muted-foreground">
        {viewport.center[1].toFixed(2)}, {viewport.center[0].toFixed(2)} ·
        zoom {viewport.zoom.toFixed(1)}
      </div>
    </div>
  );
}

API

Map

PropDescriptionTypeDefault
childrenMarkers, popups, routes, arcs, and cluster layers.ReactNode
themeTile style to use.'light' | 'dark'app's ConfigProvider mode
stylesOverride the default tile style URL per theme.{ light?: MapStyleOption; dark?: MapStyleOption }OpenFreeMap positron/dark
viewportControlled camera. Pair with onViewportChange (see Controlled Viewport).Partial<MapViewport>
onViewportChangeCalled on pan/zoom/rotate with the new camera state.(viewport: MapViewport) => void
loadingShows a centered spinner overlay.booleanfalse
refExposes the underlying maplibregl.Map instance.Ref<MapRef | null>
classNameClass names applied to the map's root element.string
centerInitial camera center (uncontrolled).LngLatTuple
zoomInitial zoom level (uncontrolled).number
bearingInitial map rotation, in degrees (uncontrolled).number0
pitchInitial camera tilt, in degrees (uncontrolled).number0
interactiveDisables all pan/zoom/rotate gestures when false.booleantrue
attributionControlShows the map data attribution badge.booleantrue
...mapOptionsThe rest of MapLibre's own MapOptions — see MapLibre's docs.Omit<maplibregl.MapOptions, 'container' | 'style'>

Map.Marker

PropDescriptionTypeDefault
longitudeMarker position.number
latitudeMarker position.number
childrenMarker visual and any attached Map.MarkerPopup / Map.MarkerTooltip / Map.MarkerLabel.ReactNode
onClickFired on marker click.(event: MouseEvent) => void
onMouseEnter / onMouseLeaveFired on hover.(event: MouseEvent) => void
onDragStart / onDrag / onDragEndFired while dragging, when draggable is set.(lngLat: { lng: number; lat: number }) => void
...markerOptionsThe rest of MapLibre's own MarkerOptions (draggable, offset, rotation, anchor, color, …).Omit<maplibregl.MarkerOptions, 'element'>

Map.MarkerContent

The default pin visual — a small primary-colored dot. Not required; Map.Marker's children accepts any node.

PropDescriptionTypeDefault
childrenContent rendered inside the dot.ReactNode
classNameClass names applied to the dot.string

Map.MarkerPopup

Click-to-open popup attached to a marker. Must be rendered inside Map.Marker.

PropDescriptionTypeDefault
childrenPopup content.ReactNode
closeButtonShows a close button in the corner.booleanfalse
classNameClass names applied to the popup body.string
...popupOptionsThe rest of MapLibre's own PopupOptions (offset, maxWidth, anchor, …).Omit<maplibregl.PopupOptions, 'closeButton'>

Map.MarkerTooltip

Hover-to-show tooltip attached to a marker. Must be rendered inside Map.Marker.

PropDescriptionTypeDefault
childrenTooltip content.ReactNode
classNameClass names applied to the tooltip body.string
...popupOptionsThe rest of MapLibre's own PopupOptions, minus closeButton and closeOnClick.Omit<maplibregl.PopupOptions, 'closeButton' | 'closeOnClick'>

Map.MarkerLabel

Small always-visible caption positioned above or below a marker.

PropDescriptionTypeDefault
childrenLabel content.ReactNode
positionSide of the marker the label sits on.'top' | 'bottom''top'
classNameClass names applied to the label.string

Map.Popup

A standalone popup at its own coordinate, not attached to a marker.

PropDescriptionTypeDefault
longitudePopup position.number
latitudePopup position.number
childrenPopup content.ReactNode
closeButtonShows a close button in the corner.booleanfalse
onCloseCalled when the popup closes.() => void
classNameClass names applied to the popup body.string
...popupOptionsThe rest of MapLibre's own PopupOptions, minus closeButton.Omit<maplibregl.PopupOptions, 'closeButton'>

Map.Controls

Floating zoom / compass / locate / fullscreen button cluster.

PropDescriptionTypeDefault
positionCorner of the map the cluster docks to.'top-left' | 'top-right' | 'bottom-left' | 'bottom-right''bottom-right'
showZoomShows zoom in/out buttons.booleantrue
showCompassShows a button that resets bearing and pitch.booleanfalse
showLocateShows a button that flies to the user's current location.booleanfalse
showFullscreenShows a button that toggles fullscreen.booleanfalse
onLocateCalled with the resolved coordinates after a successful locate.(coords: { longitude: number; latitude: number }) => void
classNameClass names applied to the control cluster.string

Map.Route

A plain polyline between coordinates.

PropDescriptionTypeDefault
coordinatesLine waypoints, in order. Needs at least 2 points.LngLatTuple[]
colorLine color.stringtheme --nui-primary
widthLine width, in pixels.number3
opacityLine opacity.number1
dashArrayDash pattern as [dash, gap].[number, number]
interactiveAttaches onClick / onMouseEnter / onMouseLeave.booleanfalse
onClick / onMouseEnter / onMouseLeaveFired when interactive is true.() => void
beforeIdInsert this layer before an existing layer id.string

Map.Arc

Curved lines between point pairs, for flight-path / network-style visualizations.

PropDescriptionTypeDefault
dataArc endpoints.MapArcDatum[]
curvatureHow much the arc bows away from a straight line.number0.2
samplesPoints used to draw the curve — higher is smoother.number64
paintMapLibre line paint override.LinePainttheme --nui-primary
layoutMapLibre line layout override.LineLayout
hoverPaintPaint applied to an arc while hovered (interactive only).LinePaint
interactiveAttaches hover state and onClick / onHover.booleanfalse
onClick / onHoverFired when interactive is true.(event: MapArcEvent | null) => void
beforeIdInsert this layer before an existing layer id.string

Map.ClusterLayer

Groups nearby GeoJSON points into clusters that expand as you zoom in.

PropDescriptionTypeDefault
dataPoint features to cluster, or a URL to load them from.string | GeoJSON.FeatureCollection<GeoJSON.Point>
clusterMaxZoomZoom level above which points no longer cluster.number14
clusterRadiusCluster grouping radius, in pixels.number50
clusterPaintMapLibre paint override for cluster circles.CirclePainttheme --nui-primary
clusterCountPaintMapLibre paint override for the cluster count label.SymbolPaint
pointPaintMapLibre paint override for individual (non-clustered) points.CirclePainttheme --nui-primary
onClusterClickFired on cluster click. Also auto-zooms to expand the cluster.(event: { clusterId: number; coordinates: LngLatTuple; originalEvent }) => void
onPointClickFired on an individual point click.(event: { feature; coordinates: LngLatTuple; originalEvent }) => void
beforeIdInsert these layers before an existing layer id.string

useMap

Returns the shared map context. Must be called within Map.

function useMap(): {
  map: maplibregl.Map | null;
  isLoaded: boolean;
  isStyleLoaded: boolean;
}

Types

type MapViewport = {
  center: [longitude: number, latitude: number];
  zoom: number;
  bearing: number;
  pitch: number;
}
 
type MapStyleOption = string | maplibregl.StyleSpecification
 
type LngLatTuple = [longitude: number, latitude: number]
 
type MapArcDatum = {
  id: string | number;
  from: LngLatTuple;
  to: LngLatTuple;
}
 
type MapArcEvent<T = MapArcDatum> = {
  arc: T;
  longitude: number;
  latitude: number;
  originalEvent: maplibregl.MapMouseEvent;
}

LinePaint, LineLayout, CirclePaint, SymbolPaint, and SymbolLayout are MapLibre's own style-spec paint/layout types, re-exported as-is — see MapLibre's style spec rather than a NateUI-defined shape.