import { MapCanvas } from "@elabs-ai/components-maps";npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-mapsPaste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.
Add the brand-ui MapCanvas component (from @elabs-ai/components-maps) to this project. 1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`. 2. Look MapCanvas up before writing code: `npx -y @elabs-ai/components-cli docs MapCanvas` prints its real props, variants, what it pairs with and what to avoid. 3. If @elabs-ai/components-maps is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-maps` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-maps`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`. 4. Use it here: [the screen or file, and what it should show] 5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop. 6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.
Avoid
- Hiding MapLibre's attribution control — the default Carto basemap styles/tiles require it.
- Importing maplibre-gl CSS in the app — MapCanvas imports its own CSS plus the brand popup overrides.
- Rendering it in an RSC/SSR path with no client boundary — MapLibre needs WebGL.
- Mapping plan units into DEGREES to fake a plan coordinate system — degrees are not linear on screen, so a 16:9 plan renders at 2:1. Declare `plan` and let createPlanCrs map into normalized Mercator, where a rectangle stays a rectangle at every zoom.
- Leaving rotation and pitch on for a plan — a tilted floor plan is unreadable. The `plan` prop turns dragRotate, pitchWithRotate and touchPitch off; do not switch them back on.
- Combining `plan` with a globe projection — a plan is flat; the projection is ignored with a one-time warning.
Works with
Examples
Loading
The `loading` prop shows an accessible overlay while the app fetches map data.
Forced Dark Basemap
Pin the basemap flavor regardless of the active brand theme.
Globe
A globe projection — pairs well with `blank` + data layers for data-viz.
Static Locator
An editorial locator: static (no zoom or pan; tooltips still open), two area markers with a stripe pattern and a vignette glow, inline labels, a globe inset, a scale bar and a key. The map sets no height of its own: it takes 1.6 : 1, and turns square at `narrow` so a phone still gets a map worth reading; there the key moves under the map and Rochester's label (`showAt: { narrow: false }`) steps aside.
Static Mode
`interactive={false}`: wheel, drag, double-click and keys leave the viewport alone and the cursor stays an arrow, but hovering the marker still opens its tooltip. The readout only changes when the viewport does.
Responsive Height
A map in a parent with no height of its own: `height` resolves per tier — here 320 px, and a square at `narrow`.
API
MapCanvas
| Prop | Type | Description |
|---|---|---|
children | ReactNode | |
className | string | Additional CSS classes for the map container. |
theme | BasemapTheme | Basemap flavor. If not provided, it is derived from the active brand theme (`data-theme` + that theme's own `color-scheme`), then a `dark`/`light` root class, then the OS preference. |
styles | { light?: MapStyleOption; dark?: MapStyleOption; } | Custom map styles for light and dark themes. Overrides the default Carto styles. |
blank | boolean | Use a transparent, tile-less basemap instead of the default Carto street basemap — a blank canvas. Used alone it renders nothing; add your own layers on top (`<MapGeoJSON>`, `<MapArc>`, markers, …). Ideal for data visualizations. Ignored when an explicit `styles` prop is provided. |
basemapLabels | boolean | Draw the basemap's own place labels. `false` leaves the ground and hides every text label the basemap style carries, so the map's own labels (`MapMarker`'s `label`, `MapAnnotation`) are the only naming on it — the locator recipe, and the default inside `MapInset`, where a full-size label is sliced by the frame. Default `true`. |
projection | MapProjectionOption | Map projection: `"mercator"` (MapLibre's default) or `"globe"` for a 3D globe view, or a full MapLibre projection spec. Feature-detected: a MapLibre build that cannot switch projections ignores it (with a console note). |
interactive | boolean | `false` makes the map STATIC — the editorial / locator default: no zoom, pan, rotate or keyboard handlers, the default cursor and no tab stop (leave `<MapControls>` out of a static map). Hover and click still reach layers and markers, so tooltips keep working. The viewport can still be set in code (`viewport`, the ref). Default `true`. |
height | MapResponsive<MapHeight> | The map's height: CSS px, or `{ aspect }` (width ÷ height), optionally per tier — `{ base: { aspect: 1.6 }, narrow: { aspect: 1 } }`. Tiers are measured on the map's own width (`narrow` < 480 px, `medium` < 768 px). Unset, the map fills its parent as before; a parent with no height of its own gets `DEFAULT_MAP_HEIGHT` (1.6 : 1, square at `narrow`) instead of 0. |
viewport | Partial<MapViewport> | Controlled viewport. When provided together with `onViewportChange`, the map becomes controlled and the viewport is driven by this prop. |
onViewportChange | (viewport: MapViewport) => void | Callback fired continuously as the viewport changes (pan, zoom, rotate, pitch). Use standalone to observe changes, or with `viewport` for controlled mode. |
loading | boolean | Show a loading overlay on the map (e.g. while the app fetches map data). |
plan | PlanExtent | PlanCrs | Turn the canvas into a CUSTOM (non-geographic) plan map: a floor plan, a factory layout, a train carriage, a rack elevation. Pass the plan's extent (`{ width, height }` in the plan's own units) or a `createPlanCrs(…)` result, and every layer inside then speaks PLAN coordinates instead of lng/lat — shapes, routes, markers and popups alike. The plan is fitted on mount, pan and zoom are clamped to its extent, and rotation and pitch are off (a rotated floor plan is unreadable). Pair it with `blank` and, for a picture under the shapes, `<MapPlanImage>`. The plan coordinate system is synthesized on Web Mercator, so it is not a georeference: distances belong to the plan, and a scale bar would lie. |
fitPlan | boolean | Fit a plan map to its extent on mount and whenever the extent changes (default: true). Ignored on a geographic map, and skipped when the camera is driven through `viewport`. |
MapMarker
| Prop | Type | Description |
|---|---|---|
children | ReactNode | Marker sub-components (MapMarkerContent, MapMarkerPopup, MapMarkerTooltip, MapMarkerLabel). Optional when `label` alone marks the place — an inline area label, for example. |
showAt | MapResponsive<boolean> | Whether the marker shows at a tier (default `true` everywhere): `{ base: true, narrow: false }` drops it from a phone-width map. The The editorial advice applies — duplicate a marker with a shorter label and the opposite `showAt` rather than cramming one label into both. |
label | MapMarkerLabelSpec | A text label drawn with the marker (see {@link MapMarkerLabelSpec}). |
onClick | (e: MouseEvent) => void | Callback when the marker is clicked. |
onMouseEnter | (e: MouseEvent) => void | Callback when the mouse enters the marker. |
onMouseLeave | (e: MouseEvent) => void | Callback when the mouse leaves the marker. |
onDragStart | (lngLat: { lng: number; lat: number }, plan: PlanPoint | null) => void | Callback when a drag starts (requires `draggable`). The plan point is `null` off a plan. |
onDrag | (lngLat: { lng: number; lat: number }, plan: PlanPoint | null) => void | Callback during a drag (requires `draggable`). The plan point is `null` off a plan. |
onDragEnd | (lngLat: { lng: number; lat: number }, plan: PlanPoint | null) => void | Callback when a drag ends (requires `draggable`). The plan point is `null` off a plan. |
dragLabel | string | The accessible name of a `draggable` marker — say what it moves ("Depot location"). Defaults to the locale seam's generic name. Ignored when the marker is not draggable. |
MapControls
| Prop | Type | Description |
|---|---|---|
position | "top-left" | "top-right" | "bottom-left" | "bottom-right" | Position of the controls on the map (default: "bottom-right"). |
showZoom | boolean | Show zoom in/out buttons. Default: true on an interactive map, false on a static one (`<MapCanvas interactive={false}>`), whose whole point is a fixed view — set `showZoom` explicitly to override either way. |
showCompass | boolean | Show a compass button to reset bearing/pitch (default: false). |
showLocate | boolean | Show a locate button to fly to the user's location (default: false). |
showFullscreen | boolean | Show a fullscreen toggle button (default: false). |
showFit | boolean | Show a button that frames the whole plan again — a plan map has an edge to come back to, so this defaults to ON there and off on a geographic map. |
fitBounds | [[number, number], [number, number]] | What the fit button frames. Defaults to the plan's own extent; pass bounds to frame something else (one wing, the selected cluster). |
onFit | () => void | Called after the camera has been asked to frame the plan. |
className | string | Additional CSS classes for the controls container. |
onLocate | (coords: { longitude: number; latitude: number }) => void | Callback with user coordinates when located. |
onLocateError | (error: GeolocationPositionError) => void | Callback when geolocation fails or is denied. |
MapClusterLayer
| Prop | Type | Description |
|---|---|---|
datarequired | string | GeoJSON.FeatureCollection<GeoJSON.Point, P> | GeoJSON FeatureCollection data or a URL to fetch GeoJSON from. |
clusterMaxZoom | number | Maximum zoom level to cluster points on (default: 14). |
clusterRadius | number | Radius of each cluster when clustering points, in pixels (default: 50). |
clusterColors | [string, string, string] | Colors for cluster circles: [small, medium, large] based on point count. Defaults to the theme's `--success`/`--warning`/`--destructive` tokens. |
clusterThresholds | [number, number] | Point-count thresholds for the color/size steps: [medium, large] (default: [100, 750]). |
pointColor | string | Color for unclustered individual points. Defaults to the theme's `--primary` token. |
onPointClick | ( feature: GeoJSON.Feature<GeoJSON.Point, P>, coordinates: [number, number], ) => void | Callback when an unclustered point is clicked. |
onClusterClick | (clusterId: number, coordinates: [number, number], pointCount: number) => void | Callback when a cluster is clicked. If not provided, zooms into the cluster. |
MapGeoJSON
| Prop | Type | Description |
|---|---|---|
datarequired | MapGeoJSONData<P> | GeoJSON data (FeatureCollection, Feature, Geometry) or a URL to fetch it from. On a canvas with a `plan` extent the coordinates are PLAN units — `[x, y]` in the plan's own drawing units — and are converted for you. |
id | string | Optional unique identifier prefix for the source/layers. Auto-generated if not provided. |
promoteId | string | Feature property to promote to the feature `id`. Required for hover and selected feature-state (`fillHoverPaint`, `selectedId`) and stable `onHover`/`onClick` payloads. |
fillPaint | MapFillPaint | false | Paint for the polygon fill layer. Merged on top of a theme-aware neutral default (the `--border` token — a mid neutral that reads on the page surface in every theme). Pass `false` to omit the fill layer entirely (e.g. outlines only). |
linePaint | MapLinePaint | false | Paint for the outline layer. Merged on top of a hairline default (`line-color` = the `--background` token, `line-width` = 0.5) for thin separators. Override `line-color` if your container differs, or pass `false` to omit the layer. |
fillHoverPaint | MapFillPaint | Paint merged onto the fill layer for the feature under the cursor, applied as a `case` expression keyed on hover feature-state. Requires `promoteId`. |
fillOpacity | number | The fill layer's opacity, 0–1 — shorthand for `fillPaint["fill-opacity"]` (which wins when both are set). Default 1, or 0.25 with a `pattern`, so the stripes read over a tint of the same colour. |
pattern | MapGeoJSONPattern | Hatch the areas with stripes (see {@link MapGeoJSONPattern}). |
vignette | MapGeoJSONVignette | Glow around the areas' edges (see {@link MapGeoJSONVignette}). |
lineHoverPaint | MapLinePaint | The outline equivalent of `fillHoverPaint`. Requires `promoteId`. |
fillSelectedPaint | MapFillPaint | Paint merged onto the fill layer for the feature(s) named by `selectedId`. Applied outside hover, so a selected feature still looks selected while the cursor is over it. Requires `promoteId`. |
lineSelectedPaint | MapLinePaint | The outline equivalent of `fillSelectedPaint`. Requires `promoteId`. |
selectedId | string | number | readonly (string | number)[] | null | The selected feature id(s) — the promoted `promoteId` values. Selection is the consumer's state; this only paints it. |
hoveredId | string | number | null | Take over the highlight: when set, the hover paint follows THIS id instead of the pointer. That is how `MapPlanOverlay` makes a keyboard-focused room light up exactly as a hovered one does — one channel, one appearance. |
onClick | (e: MapGeoJSONEvent<P>) => void | Callback when a feature is clicked. |
onHover | (e: MapGeoJSONEvent<P> | null) => void | Callback fired when the hovered feature changes; `null` when the cursor leaves. |
interactive | boolean | Whether features respond to mouse events. Defaults to `false` on a geographic canvas and `true` on a plan canvas, where the shapes ARE the subject rather than a backdrop. |
featureLabel | (feature: MapGeoJSONFeature<P>, index: number) => string | The text of a feature in the keyboard list an `interactive` map renders — one visually-hidden button per feature, so the values a pointer reveals on hover are reachable by Tab and readable by a screen reader (WCAG 2.1.1 and 1.3.1). Say what the region is AND what it is worth — the same sentence a hover readout prints. Defaults to the promoted id. |
pickPadding | number | How far, in px, a click may miss a shape and still land on it. Defaults to 8 px on a plan (so a 20 px seat is a 36 px touch target) and 0 elsewhere. An exact hit always wins over a padded one. |
hitWidth | number | Width, in px, of the invisible line that makes an outline-only shape touchable (default 16). Only used when `fillPaint` is `false`. |
beforeId | string | Optional MapLibre layer id to insert the layers before (z-order control). |
MapRoute
| Prop | Type | Description |
|---|---|---|
id | string | Optional unique identifier for the route layer. |
coordinatesrequired | [number, number][] | Array of [longitude, latitude] coordinate pairs defining the route — or, in a `<MapCanvas plan>`, [x, y] pairs in plan units. |
color | string | Line color as a CSS color value. Defaults to the theme's `--primary` token. |
width | number | Line width in pixels (default: 3). |
opacity | number | Line opacity from 0 to 1 (default: 0.8). |
dashArray | [number, number] | Dash pattern [dash length, gap length] for dashed lines. |
direction | MapRouteDirection | Draw chevrons along the line to show which way it runs (default: "none"). A conveyor, a one-way aisle and a walking route all need this, and an arrow is a second channel that greyscale keeps. |
directionSpacing | number | Distance between chevrons in px (default: 80). |
onClick | () => void | Callback when the route line is clicked. |
onMouseEnter | () => void | Callback when the mouse enters the route line. |
onMouseLeave | () => void | Callback when the mouse leaves the route line. |
interactive | boolean | Whether the route is interactive — shows a pointer cursor on hover (default: true). |
MapPlanImage
| Prop | Type | Description |
|---|---|---|
srcrequired | MapPlanImageSource | { light: MapPlanImageSource; dark: MapPlanImageSource } | The plan picture. Pass `{ light, dark }` to swap it with the theme. |
alt | string | What the picture shows, for anyone who cannot see it ("Level 3 floor plan, 14 rooms around a central core"). A plan drawn into WebGL has no accessible name of its own, so this is the only description of it. |
id | string | Optional identifier for the source/layer. Auto-generated if not provided. |
extent | { x: number; y: number; width: number; height: number } | Where the picture sits, in PLAN units. Defaults to the whole plan extent, so a single full-bleed plan needs nothing here. Use it to place one wing, one machine cell or one carriage inside a larger plan. |
coordinates | MapLibreGL.Coordinates | The four corners in lng/lat (top-left, top-right, bottom-right, bottom-left) — an escape hatch that also works on an ordinary geographic canvas, like Leaflet's `imageOverlay`. Wins over `extent`. |
opacity | number | Opacity, 0–1 (default 1). A photographic or ink-heavy plan usually reads best held back around 0.35 so the data drawn on top stays the focus. |
visible | boolean | Hide the picture without tearing the source down — a multi-floor switch (default true). |
resampling | "linear" | "nearest" | `"nearest"` keeps CAD line art crisp when zoomed past its native resolution. |
beforeId | string | MapLibre layer id to insert this layer before (z-order control). |
onLoad | () => void | Fired once the picture has been fetched and decoded. |
onError | (error: Error) => void | Fired when the picture cannot be loaded. The plan itself keeps working without it. |
MapPlanOverlay
| Prop | Type | Description |
|---|---|---|
regionsrequired | readonly MapPlanRegion[] | The interactive regions, in reading order. Keep the array MEMOIZED — it drives the projection pass. |
labelrequired | string | Accessible name for the set of regions ("Rooms on level 3"). Supplied by the consumer so it can be localized. |
selectedId | string | readonly string[] | null | The selected region id(s) — the same value handed to `<MapGeoJSON selectedId>`. |
onSelect | (id: string | null, region: MapPlanRegion | null) => void | Fired when a region is activated by click, `Enter` or `Space`; `null` on `Escape`. |
onActiveChange | (id: string | null, region: MapPlanRegion | null) => void | Fired when the pointer or keyboard focus moves to another region. Feed it to `<MapGeoJSON hoveredId>` so the shape and the proxy highlight together. |
mode | MapPlanOverlayMode | `"regions"` (default) puts one button on every region. `"groups"` puts one button on each `group` instead, and `Enter` descends into its members — the answer for a plan with more regions than a keyboard can reasonably walk (a train with 192 seats), where the first question is always "which coach?". |
describeGroup | (group: MapPlanGroup) => string | The accessible description of a group's button — "64 seats", localized by the consumer. Group mode only. |
showLabels | boolean | Draw each region's label in the overlay (default true). |
labelMinZoom | number | Hide the labels below this zoom, when a fitted plan is too small to read. |
capturePointer | boolean | Let the regions take the pointer as well as the keyboard (default true). With `false` the overlay is keyboard-only and clicks reach the canvas, where `<MapGeoJSON onClick>` picks the shape itself — the right choice for interlocking, non-rectangular shapes whose boxes overlap. |
className | string | Extra classes for the overlay root. |
Theming
The tokens each state resolves to. Change the token and every theme follows.
| State | Token |
|---|---|
| loading | loading → Spinner overlay (role="status") |