mantine-map

Interactive maps for Mantine, powered by MapLibre GL. Markers, controls and polygons with the Mantine styles API.

Installation

yarn add mantine-map maplibre-gl

Import the styles once in your app, after the Mantine core styles. mantine-map/styles.css already includes the required maplibre-gl styles, so you do not need to import them separately:

import "@mantine/core/styles.css";
import "mantine-map/styles.css";

MapLibre GL JS v6 loads its renderer worker from a URL rather than through the bundler's module graph, so the URL must be supplied once before the first map is created. Without it the map mounts but never draws any tiles. setWorkerUrl is re-exported from mantine-map for this:

// Vite. `?worker&url` is required, not plain `?url`: the worker imports a sibling chunk at runtime.
import { setWorkerUrl } from "mantine-map";
import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url";

setWorkerUrl(workerUrl);

With webpack 5, rspack or rsbuild, use setWorkerUrl(new URL("maplibre-gl/dist/maplibre-gl-worker.mjs", import.meta.url).toString()). With Next.js, copy both maplibre-gl-worker.mjs and maplibre-gl-shared.mjs from node_modules/maplibre-gl/dist into public/ and point at them there: they must stay side by side, since the worker imports the shared chunk as a sibling. This site itself avoids the copy by letting Turbopack bundle the worker as a new Worker(new URL(...)) entry, which is tidier to deploy but relies on internals of both Turbopack and maplibre; see docs/maplibre-worker-url.ts in the repository.

Note: mantine-map does not ship any map tiles. Pass a style URL from a tile provider (for example MapTiler or CARTO) via the mapStyle prop. The demos below use the keyless CARTO Voyager and Dark Matter basemaps, swapped automatically with the Mantine color scheme.

Usage

import 'mantine-map/styles.css';
import { Map } from 'mantine-map';

// Keyless CARTO basemaps, swapped with the Mantine color scheme
const basemap = {
  light: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',
  dark: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
};

function Demo() {
  return (
    <Map
      mapStyle={basemap}
      initialViewState={{ longitude: 29.0061, latitude: 41.0422, zoom: 14 }}
      style={{ height: 460 }}
    />
  );
}

Markers

Markers accept custom content and integrate Mantine Tooltip and Popover for interactive overlays.

import 'mantine-map/styles.css';
import { Popover, Stack, Text, Tooltip } from '@mantine/core';
import { Map, MapMarker } from 'mantine-map';

// Keyless CARTO basemaps, swapped with the Mantine color scheme
const basemap = {
  light: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',
  dark: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
};

function Demo() {
  return (
    <Map mapStyle={basemap} initialViewState={{ longitude: 29.0035, latitude: 41.0407, zoom: 14 }} style={{ height: 460 }}>
      {/* Tooltip on hover: wrap the marker */}
      <Tooltip label="Beşiktaş Meydanı">
        <MapMarker longitude={29.0061} latitude={41.0422} />
      </Tooltip>

      {/* Popover on click: wrap the marker as the target */}
      <Popover withArrow shadow="md">
        <Popover.Target>
          <MapMarker longitude={29.0007} latitude={41.0391} color="grape" />
        </Popover.Target>
        <Popover.Dropdown>
          <Stack gap={4}>
            <Text fw={600}>Dolmabahçe Palace</Text>
            <Text size="sm" c="dimmed">Beşiktaş, İstanbul</Text>
          </Stack>
        </Popover.Dropdown>
      </Popover>
    </Map>
  );
}

Adding markers on click

Map reports clicks on the map through onMapClick, which receives the { longitude, latitude } under the pointer, so the coordinate can be handed straight to a Map.Marker. The raw MapLibre MapMouseEvent is passed as the second argument for pixel coordinates and the original DOM event. The name keeps it distinct from onClick, which stays the plain DOM handler of the root element.

Markers are rendered inside the map, so clicking an existing marker fires onMapClick as well. The demo below skips those clicks by checking whether the DOM event originated inside .maplibregl-marker. Clicks on Map.Controls never reach the map and need no such check.

Click the map to drop a marker

import 'mantine-map/styles.css';
import { useState } from 'react';
import { Button, Group, Text } from '@mantine/core';
import { Map, MapCoordinates, MapMarker } from 'mantine-map';

// Keyless CARTO basemaps, swapped with the Mantine color scheme
const basemap = {
  light: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',
  dark: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
};

function Demo() {
  const [markers, setMarkers] = useState<MapCoordinates[]>([]);

  return (
    <>
      <Group justify="space-between" mb="sm">
        <Text size="sm" c="dimmed">Click the map to drop a marker</Text>
        <Button size="xs" variant="default" onClick={() => setMarkers([])} disabled={markers.length === 0}>
          Clear
        </Button>
      </Group>

      <Map
        mapStyle={basemap}
        initialViewState={{ longitude: 29.0061, latitude: 41.0422, zoom: 14 }}
        style={{ height: 460 }}
        onMapClick={(coordinates, event) => {
          // Markers are rendered inside the map, so clicking one lands here too. Ignore those.
          if ((event.originalEvent.target as HTMLElement).closest('.maplibregl-marker')) {
            return;
          }
          setMarkers((current) => [...current, coordinates]);
        }}
      >
        {markers.map((marker) => (
          <MapMarker
            key={`${marker.longitude},${marker.latitude}`}
            longitude={marker.longitude}
            latitude={marker.latitude}
          />
        ))}
      </Map>
    </>
  );
}

Controls

Controls are built from Mantine ActionIcon and inherit the active theme. Compose them inside Map.Controls and anchor them to any corner.

The library ships no icons, so each control takes its icon as children and you can use any icon set (the demo below uses Tabler icons). Map.FullscreenControl also accepts a function, so a different icon can be rendered per state. Wrap adjacent controls in Map.ControlGroup to join them into a single segmented block.

Map keeps the live bearing on its root element as the --map-bearing CSS variable. Map.CompassControl uses it to rotate its needle without re-rendering, and your own overlays can consume it the same way: transform: rotate(calc(var(--map-bearing) * -1)).

import 'mantine-map/styles.css';
import {
  IconCurrentLocation,
  IconMaximize,
  IconMinimize,
  IconMinus,
  IconNavigation,
  IconPlus,
} from '@tabler/icons-react';
import {
  Map,
  MapControls,
  MapControlGroup,
  MapZoomInControl,
  MapZoomOutControl,
  MapCompassControl,
  MapFullscreenControl,
  MapGeolocateControl,
} from 'mantine-map';

// Keyless CARTO basemaps, swapped with the Mantine color scheme
const basemap = {
  light: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',
  dark: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
};

function Demo() {
  return (
    <Map mapStyle={basemap} initialViewState={{ longitude: 29.0061, latitude: 41.0422, zoom: 14 }} style={{ height: 460 }}>
      <MapControls position="top-right">
        {/* Controls ship no icons, pass your own as children */}
        <MapControlGroup>
          <MapZoomInControl>
            <IconPlus size={18} />
          </MapZoomInControl>
          <MapZoomOutControl>
            <IconMinus size={18} />
          </MapZoomOutControl>
        </MapControlGroup>
        <MapCompassControl>
          <IconNavigation size={18} />
        </MapCompassControl>
        <MapFullscreenControl>
          {(isFullscreen) => (isFullscreen ? <IconMinimize size={18} /> : <IconMaximize size={18} />)}
        </MapFullscreenControl>
        <MapGeolocateControl>
          <IconCurrentLocation size={18} />
        </MapGeolocateControl>
      </MapControls>
    </Map>
  );
}

Polygons

Use Map.Polygon to highlight an area. Colors accept Mantine color keys and are resolved to concrete values for the WebGL renderer.

import 'mantine-map/styles.css';
import { Map, MapPolygon } from 'mantine-map';

// Keyless CARTO basemaps, swapped with the Mantine color scheme
const basemap = {
  light: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',
  dark: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
};

const area = [
  [29.0, 41.048],
  [29.016, 41.048],
  [29.016, 41.036],
  [29.0, 41.036],
];

function Demo() {
  return (
    <Map mapStyle={basemap} initialViewState={{ longitude: 29.008, latitude: 41.042, zoom: 13 }} style={{ height: 460 }}>
      <MapPolygon id="besiktas" coordinates={area} fillColor="blue" />
    </Map>
  );
}