Creative Tim UICreative Tim UI

Shadcn Map

PreviousNext

Interactive map components powered by MapLibre GL for displaying locations, markers, routes, and geospatial data with full theme support.

import { Map } from "@/components/ui/map"

export function MapDemo() {
  return (
    <div className="h-[400px] w-full overflow-hidden rounded-lg border">
      <Map center={[-122.4194, 37.7749]} zoom={12} />
    </div>
  )
}

Installation

pnpm dlx @creative-tim/ui@latest add map

Usage

import { Map } from "@/components/ui/map"
<Map center={[-122.4194, 37.7749]} zoom={12} />

Examples

Map with Controls

Interactive map with zoom, compass, locate, and fullscreen controls positioned in the corner.

"use client"

import { Map, MapControls } from "@/components/ui/map"

export function MapWithControls() {
  const handleLocate = (coords: { lng: number; lat: number }) => {
    console.log("User location:", coords)
    // Custom behavior: could show a marker, save to state, etc.
  }

  return (
    <div className="h-[400px] w-full overflow-hidden rounded-lg border">
      <Map center={[-122.4194, 37.7749]} zoom={12}>
        <MapControls
          position="bottom-right"
          showZoom
          showCompass
          showLocate
          showFullscreen
          onLocate={handleLocate}
        />
      </Map>
    </div>
  )
}

Multiple Markers

Display multiple location markers with interactive tooltips on hover and popups on click showing detailed information.

import {
  Map,
  MapMarker,
  MarkerContent,
  MarkerPopup,
  MarkerTooltip,
} from "@/components/ui/map"

export function MapMarkers() {
  // San Francisco landmarks
  const landmarks = [
    {
      id: 1,
      name: "Golden Gate Bridge",
      longitude: -122.4783,
      latitude: 37.8199,
    },
    {
      id: 2,
      name: "Alcatraz Island",
      longitude: -122.423,
      latitude: 37.8267,
    },
    {
      id: 3,
      name: "Ferry Building",
      longitude: -122.3933,
      latitude: 37.7956,
    },
    {
      id: 4,
      name: "Coit Tower",
      longitude: -122.4058,
      latitude: 37.8024,
    },
  ]

  return (
    <div className="h-[400px] w-full overflow-hidden rounded-lg border">
      <Map center={[-122.4194, 37.7749]} zoom={12}>
        {landmarks.map((landmark) => (
          <MapMarker
            key={landmark.id}
            longitude={landmark.longitude}
            latitude={landmark.latitude}
          >
            <MarkerContent>
              <div className="bg-primary border-background ring-primary/20 size-3.5 rounded-full border-2 shadow-md ring-2" />
            </MarkerContent>
            <MarkerTooltip>
              <div className="font-medium">{landmark.name}</div>
            </MarkerTooltip>
            <MarkerPopup>
              <div className="space-y-1.5">
                <p className="text-foreground font-semibold">{landmark.name}</p>
                <p className="text-muted-foreground text-xs">
                  Coordinates: {landmark.latitude.toFixed(4)},{" "}
                  {landmark.longitude.toFixed(4)}
                </p>
              </div>
            </MarkerPopup>
          </MapMarker>
        ))}
      </Map>
    </div>
  )
}

Marker with Popup

Markers with interactive popups that display detailed information when clicked.

import { Button } from "@/components/ui/button"
import { Map, MapMarker, MarkerContent, MarkerPopup } from "@/components/ui/map"

export function MapMarkerPopup() {
  return (
    <div className="h-[400px] w-full overflow-hidden rounded-lg border">
      <Map center={[-122.4783, 37.8199]} zoom={13}>
        <MapMarker longitude={-122.4783} latitude={37.8199}>
          <MarkerContent>
            <div className="bg-primary border-background ring-primary/20 size-3.5 rounded-full border-2 shadow-md ring-2" />
          </MarkerContent>
          <MarkerPopup closeButton>
            <div className="space-y-2">
              <h3 className="font-semibold">Golden Gate Bridge</h3>
              <p className="text-muted-foreground text-xs">
                Iconic suspension bridge spanning the Golden Gate strait
              </p>
              <Button size="sm" className="w-full">
                Get Directions
              </Button>
            </div>
          </MarkerPopup>
        </MapMarker>
      </Map>
    </div>
  )
}

Marker with Tooltip

Markers with tooltips that appear on hover for quick information display.

import {
  Map,
  MapMarker,
  MarkerContent,
  MarkerTooltip,
} from "@/components/ui/map"

export function MapMarkerTooltip() {
  const landmarks = [
    {
      name: "Golden Gate Bridge",
      longitude: -122.4783,
      latitude: 37.8199,
    },
    {
      name: "Alcatraz Island",
      longitude: -122.423,
      latitude: 37.8267,
    },
    {
      name: "Ferry Building",
      longitude: -122.3933,
      latitude: 37.7956,
    },
  ]

  return (
    <div className="h-[400px] w-full overflow-hidden rounded-lg border">
      <Map center={[-122.4194, 37.7749]} zoom={12}>
        {landmarks.map((landmark) => (
          <MapMarker
            key={landmark.name}
            longitude={landmark.longitude}
            latitude={landmark.latitude}
          >
            <MarkerContent>
              <div className="bg-primary border-background ring-primary/20 size-5 rounded-full border-2 shadow-md ring-2" />
            </MarkerContent>
            <MarkerTooltip>{landmark.name}</MarkerTooltip>
          </MapMarker>
        ))}
      </Map>
    </div>
  )
}

Custom Map Style

Customize the visual appearance of the map with your own color scheme and branding using custom style JSON files.

import type { StyleSpecification } from "maplibre-gl"

import { Map } from "@/components/ui/map"

// Custom light style with soft teal/mint theme using OpenFreeMap tiles
// OpenFreeMap: MIT License - https://github.com/hyperknot/openfreemap/blob/main/LICENSE.md
const customLightStyle: StyleSpecification = {
  version: 8,
  name: "Custom Light Style",
  sources: {
    openmaptiles: {
      type: "vector",
      url: "https://tiles.openfreemap.org/planet",
      attribution:
        '&copy; <a href="https://openfreemap.org">OpenFreeMap</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
    },
  },
  glyphs: "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf",
  layers: [
    {
      id: "background",
      type: "background",
      paint: { "background-color": "#f5f9f8" },
    },
    {
      id: "water",
      type: "fill",
      source: "openmaptiles",
      "source-layer": "water",
      paint: { "fill-color": "#c5e3df" },
    },
    {
      id: "landcover",
      type: "fill",
      source: "openmaptiles",
      "source-layer": "landcover",
      paint: { "fill-color": "#e8f5e9", "fill-opacity": 0.5 },
    },
    {
      id: "roads-minor",
      type: "line",
      source: "openmaptiles",
      "source-layer": "transportation",
      filter: ["in", "class", "minor", "service", "street"],
      paint: {
        "line-color": "#90a4ae",
        "line-width": ["interpolate", ["linear"], ["zoom"], 10, 0.5, 18, 4],
      },
    },
    {
      id: "roads-major",
      type: "line",
      source: "openmaptiles",
      "source-layer": "transportation",
      filter: ["in", "class", "primary", "secondary", "tertiary", "trunk"],
      paint: {
        "line-color": "#4db6ac",
        "line-width": ["interpolate", ["linear"], ["zoom"], 8, 1, 18, 8],
      },
    },
    {
      id: "roads-highway",
      type: "line",
      source: "openmaptiles",
      "source-layer": "transportation",
      filter: ["==", "class", "motorway"],
      paint: {
        "line-color": "#26a69a",
        "line-width": ["interpolate", ["linear"], ["zoom"], 6, 1, 18, 12],
      },
    },
    {
      id: "buildings",
      type: "fill",
      source: "openmaptiles",
      "source-layer": "building",
      paint: { "fill-color": "#cfd8dc", "fill-opacity": 0.6 },
    },
  ],
}

// Custom dark style with soft teal/slate theme
const customDarkStyle: StyleSpecification = {
  version: 8,
  name: "Custom Dark Style",
  sources: {
    openmaptiles: {
      type: "vector",
      url: "https://tiles.openfreemap.org/planet",
      attribution:
        '&copy; <a href="https://openfreemap.org">OpenFreeMap</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
    },
  },
  glyphs: "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf",
  layers: [
    {
      id: "background",
      type: "background",
      paint: { "background-color": "#1e272e" },
    },
    {
      id: "water",
      type: "fill",
      source: "openmaptiles",
      "source-layer": "water",
      paint: { "fill-color": "#2c3e50" },
    },
    {
      id: "landcover",
      type: "fill",
      source: "openmaptiles",
      "source-layer": "landcover",
      paint: { "fill-color": "#2d3e36", "fill-opacity": 0.5 },
    },
    {
      id: "roads-minor",
      type: "line",
      source: "openmaptiles",
      "source-layer": "transportation",
      filter: ["in", "class", "minor", "service", "street"],
      paint: {
        "line-color": "#455a64",
        "line-width": ["interpolate", ["linear"], ["zoom"], 10, 0.5, 18, 4],
      },
    },
    {
      id: "roads-major",
      type: "line",
      source: "openmaptiles",
      "source-layer": "transportation",
      filter: ["in", "class", "primary", "secondary", "tertiary", "trunk"],
      paint: {
        "line-color": "#4db6ac",
        "line-width": ["interpolate", ["linear"], ["zoom"], 8, 1, 18, 8],
      },
    },
    {
      id: "roads-highway",
      type: "line",
      source: "openmaptiles",
      "source-layer": "transportation",
      filter: ["==", "class", "motorway"],
      paint: {
        "line-color": "#80cbc4",
        "line-width": ["interpolate", ["linear"], ["zoom"], 6, 1, 18, 12],
      },
    },
    {
      id: "buildings",
      type: "fill",
      source: "openmaptiles",
      "source-layer": "building",
      paint: { "fill-color": "#37474f", "fill-opacity": 0.7 },
    },
  ],
}

export function MapCustomStyle() {
  return (
    <div className="h-[400px] w-full overflow-hidden rounded-lg border">
      <Map
        center={[-122.4194, 37.7749]}
        zoom={12}
        styles={{
          light: customLightStyle,
          dark: customDarkStyle,
        }}
      />
    </div>
  )
}

Draggable Marker

Interactive marker that users can drag to update location with custom callbacks.

Current Position

Lng: -122.4194, Lat: 37.7749

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Map, MapMarker, MarkerContent } from "@/components/ui/map"

export function MapMarkerDraggable() {
  const [coords, setCoords] = React.useState<[number, number]>([
    -122.4194, 37.7749,
  ])
  const [isDragging, setIsDragging] = React.useState(false)

  const handleDragEnd = (lngLat: { lng: number; lat: number }) => {
    setCoords([lngLat.lng, lngLat.lat])
    setIsDragging(false)
    console.log("Marker moved to:", lngLat)
  }

  const resetPosition = () => {
    setCoords([-122.4194, 37.7749])
  }

  return (
    <div className="space-y-3">
      <div className="h-[400px] w-full overflow-hidden rounded-lg border">
        <Map center={coords} zoom={12}>
          <MapMarker
            longitude={coords[0]}
            latitude={coords[1]}
            draggable
            onDragStart={() => setIsDragging(true)}
            onDragEnd={handleDragEnd}
          >
            <MarkerContent>
              <div
                className={`bg-primary size-5 rounded-lg border-2 border-white shadow-lg transition-transform ${
                  isDragging ? "scale-125" : ""
                }`}
              />
            </MarkerContent>
          </MapMarker>
        </Map>
      </div>

      <div className="flex items-center justify-between rounded-lg border p-4">
        <div className="space-y-1">
          <p className="text-sm font-medium">Current Position</p>
          <p className="text-muted-foreground font-mono text-xs">
            Lng: {coords[0].toFixed(4)}, Lat: {coords[1].toFixed(4)}
          </p>
        </div>
        <Button size="sm" variant="outline" onClick={resetPosition}>
          Reset Position
        </Button>
      </div>
    </div>
  )
}

Props

Map (Root Component)

PropTypeDefaultDescription
center[number, number][-122.4194, 37.7749] (SF)Map center coordinates [longitude, latitude]
zoomnumber12Initial zoom level (0-22)
minZoomnumber0Minimum zoom level allowed
maxZoomnumber22Maximum zoom level allowed
stylesobjectOpenFreeMapCustom tile styles for light/dark themes
classNamestring-Additional CSS classes
childrenReactNode-Child components (markers, controls, etc.)

MapMarker

PropTypeDefaultDescription
longitudenumber-Marker longitude coordinate (required)
latitudenumber-Marker latitude coordinate (required)
draggablebooleanfalseWhether the marker can be dragged
onClick() => void-Called when marker is clicked
onDragStart(lngLat: LngLat) => void-Called when drag starts
onDrag(lngLat: LngLat) => void-Called continuously during drag
onDragEnd(lngLat: LngLat) => void-Called when drag ends
childrenReactNode-Marker content components

MarkerContent

PropTypeDefaultDescription
childrenReactNodeBlue dotCustom marker appearance
classNamestring-Additional CSS classes

MarkerPopup

PropTypeDefaultDescription
childrenReactNode-Popup content
classNamestring-Additional CSS classes
closeButtonbooleanfalseShow close button in popup
closeOnClickbooleantrueClose popup when clicking outside

MarkerTooltip

PropTypeDefaultDescription
childrenReactNode-Tooltip content
classNamestring-Additional CSS classes

MarkerLabel

PropTypeDefaultDescription
childrenReactNode-Label content
classNamestring-Additional CSS classes
position"top" | "bottom" | "left" | "right""top"Label position relative to marker

MapControls

PropTypeDefaultDescription
position"top-left" | "top-right" | "bottom-left" | "bottom-right""bottom-right"Controls position on map
showZoombooleantrueShow zoom in/out buttons
showCompassbooleanfalseShow compass/reset north button
showLocatebooleanfalseShow geolocation button
showFullscreenbooleanfalseShow fullscreen toggle button
onLocate(coords: { lng: number; lat: number }) => void-Called when user location is found
classNamestring-Additional CSS classes

MapPopup (Standalone)

PropTypeDefaultDescription
longitudenumber-Popup longitude (required)
latitudenumber-Popup latitude (required)
childrenReactNode-Popup content
classNamestring-Additional CSS classes
closeButtonbooleantrueShow close button
onClose() => void-Called when popup is closed

MapRoute

PropTypeDefaultDescription
idstring-Unique route identifier (required)
coordinates[number, number][]-Array of [lng, lat] coordinates
colorstring"#3b82f6"Route line color
widthnumber3Route line width in pixels
opacitynumber1Route line opacity (0-1)
dashArraynumber[]-Dash pattern for dashed lines
interactivebooleantrueEnable click/hover interactions
onClick() => void-Called when route is clicked

useMap Hook

Returns the map instance and loading state for advanced use cases.

const { map, isLoaded } = useMap()
Return ValueTypeDescription
mapmaplibregl.Map | nullMapLibre GL map instance
isLoadedbooleanWhether the map has finished loading

Features

  • Zero Configuration: Works immediately with free OpenFreeMap tiles, no API keys required
  • Theme Support: Automatically switches between light and dark map tiles based on your theme
  • Composable: Build complex map interfaces by combining simple components
  • TypeScript: Full type safety with comprehensive TypeScript definitions
  • Accessible: ARIA labels, keyboard navigation, and screen reader support
  • Performant: Efficient rendering with MapLibre GL's WebGL-based engine
  • Customizable: Easily customize colors, styles, and behavior to match your design

Best Practices

  • Use San Francisco coordinates [-122.4194, 37.7749] as the default or choose appropriate defaults for your use case
  • Keep zoom levels between 10-15 for city-level views, 15-18 for neighborhood views
  • Use MarkerTooltip for quick information, MarkerPopup for detailed interactions
  • Limit the number of markers rendered at once (use clustering for large datasets)
  • Provide custom onLocate handlers to save user location or show notifications
  • Use draggable markers for location pickers and address selection
  • Add MapControls to improve user navigation and orientation
  • Consider loading states while map initializes
  • Test map behavior on both desktop and mobile devices
  • Ensure map containers have explicit height (h-[400px] or similar)

Advanced Usage

Custom Map Styles

Provide custom map tile styles for complete visual control over your maps. Supports separate styles for light and dark themes.

<Map
  center={[-122.4194, 37.7749]}
  zoom={12}
  styles={{
    light: "/map-styles/custom-light-style.json",
    dark: "/map-styles/custom-dark-style.json",
  }}
/>

The map will automatically switch between light and dark styles based on your theme. You can use:

  • Free OpenFreeMap styles: positron, liberty, bright, dark
  • Your own custom style JSON files

Custom Marker Styling

<MapMarker longitude={lng} latitude={lat}>
  <MarkerContent>
    <div className="bg-primary text-primary-foreground rounded-full p-2">
      <RestaurantIcon className="h-4 w-4" />
    </div>
  </MarkerContent>
</MapMarker>

Programmatic Map Control

const { map } = useMap()
 
useEffect(() => {
  if (map) {
    map.flyTo({ center: [lng, lat], zoom: 14 })
  }
}, [map, lng, lat])

Drawing Routes

<MapRoute
  id="route-1"
  coordinates={[
    [-122.4194, 37.7749],
    [-122.4783, 37.8199],
    [-122.4230, 37.8267],
  ]}
  color="hsl(var(--primary))"
  width={4}
/>

The map component provides a powerful and flexible way to integrate interactive maps into your application. It's perfect for store locators, delivery tracking, real estate listings, travel planning, and any interface that requires geospatial visualization.

Copyrights and Licenses

This component is built with open-source technologies that require attribution:

  • MapLibre GL JS - The rendering engine powering these maps. Licensed under BSD-3-Clause, a permissive open-source license that allows free use in commercial projects.
  • OpenFreeMap - Provides the free vector map tiles used by default. OpenFreeMap is released under the MIT License and serves map tiles without requiring API keys or registration.
  • OpenMapTiles - The vector tile schema used by OpenFreeMap. Code is licensed under BSD-3-Clause, design under CC-BY 4.0.
  • OpenStreetMap - The underlying map data is sourced from OpenStreetMap contributors, licensed under ODbL. Attribution to OpenStreetMap is required when using their data.

The map component automatically displays the required attributions in the bottom corner of the map. If you create custom styles, ensure you include proper attribution for your tile and data sources.