Map
React data-displayA thin, theme-aware Mapbox GL JS wrapper (built on react-map-gl). Pass an access token and drop MapMarker / MapPopup / MapLayer into the children to build any map — fleet boards, journey maps, store locators. The base style follows light/dark automatically, an opt-in `muted` prop desaturates the basemap so overlaid data is the only colour, and `onCreated` hands you the raw map instance for custom layers and fitBounds. Import `mapbox-gl/dist/mapbox-gl.css` once in your app.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://react.uipkge.dev/r/react/map.json$ npx shadcn@latest add https://react.uipkge.dev/r/react/map.json$ yarn dlx shadcn@latest add https://react.uipkge.dev/r/react/map.json$ bunx shadcn@latest add https://react.uipkge.dev/r/react/map.json
Or with the named registry:
npx shadcn@latest add @uipkge-react/map
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
accessToken Mapbox access token. | string | — | required |
mapStyle Override the base style. Defaults to a theme-aware light/dark style. | string | — | optional |
center Initial [lng, lat]. | [number, number] | — | optional |
zoom | number | — | optional |
projection 'mercator' (flat) or 'globe'. | string | — | optional |
navigation Show the zoom/compass control. | boolean | — | optional |
muted Repaint the basemap to a quiet, desaturated canvas. | boolean | — | optional |
onCreated Hands you the raw mapbox-gl Map instance once the style has loaded. | (map: mapboxgl.Map) => void | — | optional |
Dependencies
Used by
Files (3)
-
components/ui/map/map.tsx 6.7 kB
'use client' /** * Map — a thin, theme-aware wrapper around react-map-gl's Mapbox `Map`. Pass an * `accessToken`; drop `<MapMarker>` / `<MapPopup>` / `<MapLayer>` (re-exported * from this package) or any react-map-gl child into the children to build any * kind of map. * * - The base style follows the app theme (light-v11 / dark-v11) unless you pass * an explicit `mapStyle`. * - `muted` repaints the basemap to a quiet, desaturated canvas (graphite in * dark, light-grey in light) so overlaid data is the only colour on the map — * ideal for dashboards, fleet boards and journey maps. * - `onCreated` hands you the raw mapbox-gl Map instance for imperative work * (custom layers, fitBounds, controls). * * Requires a Mapbox access token (https://account.mapbox.com). * * IMPORTANT: consumers must import Mapbox GL's stylesheet once (e.g. in their * root layout / app entry): import 'mapbox-gl/dist/mapbox-gl.css' * It's intentionally not imported here so bundlers that can't resolve CSS from a * library file (some RSC/SSR setups) don't choke — own the import in your app. */ import * as React from 'react' import type mapboxgl from 'mapbox-gl' import Map, { NavigationControl as MapboxNavigationControl, type MapRef, type MapProps as MapboxMapProps, } from 'react-map-gl/mapbox' import { useTheme } from '@/lib/use-theme' import { cn } from '@/lib/utils' export interface MapProps extends React.HTMLAttributes<HTMLDivElement> { /** Mapbox access token. */ accessToken: string /** Override the base style. Defaults to a theme-aware light/dark style. */ mapStyle?: string /** Initial [lng, lat]. */ center?: [number, number] zoom?: number /** 'mercator' (flat) or 'globe'. */ projection?: string /** Show the zoom/compass control. */ navigation?: boolean /** Repaint the basemap to a quiet, desaturated canvas. */ muted?: boolean /** Hands you the raw mapbox-gl Map instance once the style has loaded. */ onCreated?: (map: mapboxgl.Map) => void } /** Desaturate the basemap to a quiet canvas. Each op is guarded — style layer * ids drift between style versions and the wrong property for a layer type * throws. */ function applyMuted(map: mapboxgl.Map, dark: boolean) { let styleLayers: any[] try { styleLayers = map.getStyle()?.layers ?? [] } catch { return // style not loaded yet } const P = dark ? { land: 'rgb(23,24,29)', water: 'rgb(17,18,22)', use: 'rgb(31,32,38)', road: 'rgb(50,52,60)', label: 'rgb(150,152,165)', halo: 'rgb(23,24,29)', admin: 'rgb(50,52,60)' } : { land: 'rgb(246,247,249)', water: 'rgb(226,230,235)', use: 'rgb(238,240,243)', road: 'rgb(221,224,229)', label: 'rgb(120,124,134)', halo: 'rgb(246,247,249)', admin: 'rgb(213,216,221)' } for (const l of styleLayers) { const id = l.id try { if (id === 'background' || id === 'land') map.setPaintProperty(id, 'background-color', P.land) else if (/water/.test(id) && l.type === 'fill') map.setPaintProperty(id, 'fill-color', P.water) else if (/landuse|landcover|national-park/.test(id) && l.type === 'fill') map.setPaintProperty(id, 'fill-color', P.use) else if (/^road-(motorway|trunk|primary)/.test(id) && l.type === 'line') { map.setPaintProperty(id, 'line-color', P.road) map.setPaintProperty(id, 'line-opacity', 0.55) } else if (/^road-(secondary|tertiary|street|minor|service|path|pedestrian)/.test(id)) { map.setLayoutProperty(id, 'visibility', 'none') } else if (/poi|transit|airport|natural-point|water-point|waterway-label|building/.test(id)) { map.setLayoutProperty(id, 'visibility', 'none') } else if (/road-label|settlement-major-label|settlement-minor-label|state-label/.test(id) && l.type === 'symbol') { map.setPaintProperty(id, 'text-color', P.label) map.setPaintProperty(id, 'text-halo-color', P.halo) map.setPaintProperty(id, 'text-opacity', 0.6) } else if (id === 'admin-1-boundary' && l.type === 'line') { map.setPaintProperty(id, 'line-color', P.admin) map.setPaintProperty(id, 'line-opacity', 0.6) } else if (id === 'admin-1-boundary-bg') map.setPaintProperty(id, 'line-opacity', 0) } catch { /* skip */ } } } const MapComponent = React.forwardRef<HTMLDivElement, MapProps>( ( { className, accessToken, mapStyle, center = [0, 20], zoom = 1.4, projection = 'mercator', navigation = true, muted = false, onCreated, children, ...props }, ref, ) => { const { resolvedTheme } = useTheme() const isDark = resolvedTheme === 'dark' const resolvedStyle = mapStyle ?? (isDark ? 'mapbox://styles/mapbox/dark-v11' : 'mapbox://styles/mapbox/light-v11') // Render the map only on the client — Mapbox needs the DOM, and rendering it // on the server would hydrate-mismatch. The bg-muted box is the SSR // placeholder. const [mounted, setMounted] = React.useState(false) React.useEffect(() => { setMounted(true) }, []) const mapRef = React.useRef<MapRef | null>(null) const onLoad = React.useCallback(() => { const map = mapRef.current?.getMap() if (!map) return try { ;(map as any).setProjection(projection) } catch { /* older api */ } if (muted) applyMuted(map, isDark) onCreated?.(map) }, [projection, muted, isDark, onCreated]) const onStyleData = React.useCallback(() => { const map = mapRef.current?.getMap() if (map && muted) applyMuted(map, isDark) }, [muted, isDark]) return ( <div ref={ref} data-uipkge="" data-slot="map" className={cn('bg-muted size-full overflow-hidden', className)} {...props} > {mounted && ( <Map ref={mapRef} mapboxAccessToken={accessToken} mapStyle={resolvedStyle} initialViewState={{ longitude: center[0], latitude: center[1], zoom }} attributionControl={false} style={{ width: '100%', height: '100%' }} onLoad={onLoad} onStyleData={onStyleData} > {navigation && <MapboxNavigationControl position="bottom-right" showCompass={false} />} {children} </Map> )} </div> ) }, ) MapComponent.displayName = 'Map' export { MapComponent as Map } export type { MapboxMapProps } // Re-exported here too (not just index.ts) so the toolkit resolves whether a // consumer imports from the package dir or the file (shadcn rewrites to the // file). Place these inside <Map>'s children. export { Marker as MapMarker, Popup as MapPopup, Layer as MapLayer, Source as MapSource, NavigationControl as MapNavigationControl, } from 'react-map-gl/mapbox' -
components/ui/map/index.ts 0.4 kB
export { Map } from './map' export type { MapProps } from './map' // Re-exported from react-map-gl under Map* names so consumers get the whole map // toolkit from one import. Place these inside <Map>'s children — they read the // map instance from the wrapping react-map-gl context. export { Marker as MapMarker, Popup as MapPopup, Layer as MapLayer, Source as MapSource, NavigationControl as MapNavigationControl, } from 'react-map-gl/mapbox' -
components/ui/map/map.css 1.4 kB
/** * Map chrome — re-skins Mapbox GL's controls + popups to the design-system * tokens so they sit naturally on light and dark surfaces. Global (Mapbox * renders this chrome outside the component subtree). Import once, e.g. in your * root CSS: `@import './components/ui/map/map.css';` */ .mapboxgl-ctrl-group { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-sm, 0 1px 2px rgb(0 0 0 / 0.08)); } .mapboxgl-ctrl-group button { background: transparent; } .mapboxgl-ctrl-group button + button { border-top-color: var(--border); } .mapboxgl-ctrl-group button .mapboxgl-ctrl-icon { filter: invert(var(--map-ctrl-invert, 0)) opacity(0.7); } .dark .mapboxgl-ctrl-group button .mapboxgl-ctrl-icon { filter: invert(1) opacity(0.7); } .mapboxgl-ctrl-attrib { background: color-mix(in oklab, var(--card) 80%, transparent); color: var(--muted-foreground); } .mapboxgl-ctrl-attrib a { color: var(--muted-foreground); } .mapboxgl-popup-content { padding: 8px 12px; background: var(--popover); color: var(--popover-foreground); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-md, 0 4px 12px rgb(0 0 0 / 0.12)); } .mapboxgl-popup-close-button { color: var(--muted-foreground); } .mapboxgl-popup-tip { border-top-color: var(--popover); border-bottom-color: var(--popover); }
Raw manifest: https://react.uipkge.dev/r/react/map.json