Skip to main content

MapPlanOverlay

Plansoverlay

The keyboard and label layer of a plan: one real <button aria-pressed> per region over the WebGL canvas, with spatial arrow-key travel and a group mode for very large plans.

import { MapPlanOverlay } from "@elabs-ai/components-maps";
Install
npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-maps

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.

Add the brand-ui MapPlanOverlay 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 MapPlanOverlay up before writing code: `npx -y @elabs-ai/components-cli docs MapPlanOverlay` 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

  • Relying on the canvas alone for interaction — a room drawn in WebGL is not focusable, has no name and no pressed state. Without this overlay a plan is mouse-only.
  • Nesting the visible label inside the button — it is an aria-hidden SIBLING, and the button's accessible name already starts with the label verbatim (WCAG 2.5.3).
  • Walking 200+ regions with Tab and the arrows — set mode="groups" so the first question is 'which coach?' and Enter goes in.
  • Unmounting off-screen regions to save DOM — focus would vanish mid-walk; they stay mounted and the camera eases to them instead.

Works with

Used inside

Pairs with

Examples

5 live examples

Groups

Group mode: one button per group, `Enter` to go in, `Escape` to come back out, `PageUp`/`PageDown` to change group at either level.

As Words

The plan as words only — what a printer gets, since a WebGL canvas prints blank.

Loading

Nothing on the plan yet: the canvas keeps its shape and says so once.

No Regions

A plan with no regions on it — a panel, not an empty grid.

API

MapPlanOverlay

PropTypeDescription
regionsrequiredreadonly MapPlanRegion[]The interactive regions, in reading order. Keep the array MEMOIZED — it drives the projection pass.
labelrequiredstringAccessible name for the set of regions ("Rooms on level 3"). Supplied by the consumer so it can be localized.
selectedIdstring | readonly string[] | nullThe selected region id(s) — the same value handed to `<MapGeoJSON selectedId>`.
onSelect(id: string | null, region: MapPlanRegion | null) => voidFired when a region is activated by click, `Enter` or `Space`; `null` on `Escape`.
onActiveChange(id: string | null, region: MapPlanRegion | null) => voidFired when the pointer or keyboard focus moves to another region. Feed it to `<MapGeoJSON hoveredId>` so the shape and the proxy highlight together.
modeMapPlanOverlayMode`"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) => stringThe accessible description of a group's button — "64 seats", localized by the consumer. Group mode only.
showLabelsbooleanDraw each region's label in the overlay (default true).
labelMinZoomnumberHide the labels below this zoom, when a fitted plan is too small to read.
capturePointerbooleanLet 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.
classNamestringExtra classes for the overlay root.

Theming

The tokens each state resolves to. Change the token and every theme follows.

StateToken
focusevery region button carries the shared focus-ring — the canvas cannot show one
selectedaria-pressed plus data-selected, mirrored into the shape layer via selectedId
activeone channel for pointer hover AND keyboard focus, fed to MapGeoJSON hoveredId so the shape and the proxy light up together