Map View
- working
- RTL
A real, interactive map for the map markers: MapLibre on OpenFreeMap vector tiles (no API key), painted with the
Nexera map tokens, so it follows light, dark and custom themes, with labels in the app's own font. Coordinates are
[lng, lat]. The map is created when its box comes near the viewport (maplibre-gl is loaded then, not with the page) and removed
on unmount. Before that, and when the map can't start, the box keeps its size in the land colour. Consumer duties: give the same information next to the map as text (a list of people or places), because a map is
hard or impossible to use for many people; show a user's position only with their consent; keep the credit visible.
import { MapView } from "@nexera-ui/react";- 7
- examples
- 21
- props
- 3
- live controls
- 1
- platform
- 0
- WCAG criteria
- 4
- blocks use it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { MapView } from "@nexera-ui/react";
<MapView />Examples 6
The same examples as Storybook, rendered live. Open Code to copy one.
Clustered
300 people: nearby markers merge into clusters with status segments; press one to zoom in until it splits.
import { MapMarker, MapMarkerPerson, MapMarkerPlace, MapMarkers, MapView } from "@nexera-ui/react";
interface Person {
id: string;
name: string;
initials: string;
status: MapMarkerWorkStatus;
lngLat: readonly [number, number];
}
const HQ = [-0.0877, 51.5265] as const;
const PEOPLE: Person[] = [
{
id: "ayesha",
name: "Ayesha Khan",
initials: "AK",
status: "working",
lngLat: [-0.0869, 51.5259],
},
{
id: "bilal",
name: "Bilal Ahmed",
initials: "BA",
status: "working",
lngLat: [-0.0841, 51.5248],
},
{ id: "omar", name: "Omar Farooq", initials: "OF", status: "remote", lngLat: [-0.0712, 51.4985] },
{ id: "hina", name: "Hina Raza", initials: "HR", status: "on-leave", lngLat: [-0.1031, 51.5362] },
{ id: "sara", name: "Sara Malik", initials: "SM", status: "remote", lngLat: [-0.0991, 51.5035] },
{ id: "mia", name: "Mia Jensen", initials: "MJ", status: "working", lngLat: [-0.124, 51.5308] },
{
id: "luis",
name: "Luis Ortega",
initials: "LO",
status: "offline",
lngLat: [-0.1357, 51.4975],
},
{
id: "yuki",
name: "Yuki Tanaka",
initials: "YT",
status: "working",
lngLat: [-0.0898, 51.5279],
},
];
const STATUSES: MapMarkerWorkStatus[] = ["working", "remote", "on-leave", "offline"];
const MANY: Person[] = Array.from({ length: 300 }, (_, i) => ({
id: `p${String(i)}`,
name: `Person ${String(i + 1)}`,
initials: "NW",
status: STATUSES[i % 4] ?? "working",
lngLat: [-0.16 + ((i * 37) % 100) / 1000, 51.49 + ((i * 53) % 45) / 1000],
}));
function Team({
people = PEOPLE,
selectedId = null,
onSelect,
}: {
people?: Person[];
selectedId?: string | null;
onSelect?: (id: string) => void;
}) {
return (
<MapView
aria-label="Where the Northwind team works"
defaultCenter={[-0.1, 51.515]}
defaultZoom={12.5}
selectedId={selectedId}
locate
height={460}
>
<MapMarker id="hq" lngLat={HQ}>
<MapMarkerPlace name="Northwind HQ" headcount={24} onPress={() => onSelect?.("hq")} />
</MapMarker>
<MapMarkers
items={people}
getId={(p) => p.id}
getLngLat={(p) => p.lngLat}
getCategory={(p) => p.status}
categories={STATUSES}
clusterLabel={(n) => `${String(n)} people`}
renderMarker={(p, { isSelected }) => (
<MapMarkerPerson
name={p.name}
initials={p.initials}
workStatus={p.status}
isSelected={isSelected}
onPress={() => onSelect?.(p.id)}
/>
)}
/>
</MapView>
);
}
const Clustered = { render: () => <Team people={MANY} /> };
List drives selection
The list beside the map drives selection: the map flies to the person you pick, and their marker shows it.
import { useState } from "react";
import {
ListItem,
MapMarker,
MapMarkerPerson,
MapMarkerPlace,
MapMarkers,
MapView,
} from "@nexera-ui/react";
interface Person {
id: string;
name: string;
initials: string;
status: MapMarkerWorkStatus;
lngLat: readonly [number, number];
}
const HQ = [-0.0877, 51.5265] as const;
const PEOPLE: Person[] = [
{
id: "ayesha",
name: "Ayesha Khan",
initials: "AK",
status: "working",
lngLat: [-0.0869, 51.5259],
},
{
id: "bilal",
name: "Bilal Ahmed",
initials: "BA",
status: "working",
lngLat: [-0.0841, 51.5248],
},
{ id: "omar", name: "Omar Farooq", initials: "OF", status: "remote", lngLat: [-0.0712, 51.4985] },
{ id: "hina", name: "Hina Raza", initials: "HR", status: "on-leave", lngLat: [-0.1031, 51.5362] },
{ id: "sara", name: "Sara Malik", initials: "SM", status: "remote", lngLat: [-0.0991, 51.5035] },
{ id: "mia", name: "Mia Jensen", initials: "MJ", status: "working", lngLat: [-0.124, 51.5308] },
{
id: "luis",
name: "Luis Ortega",
initials: "LO",
status: "offline",
lngLat: [-0.1357, 51.4975],
},
{
id: "yuki",
name: "Yuki Tanaka",
initials: "YT",
status: "working",
lngLat: [-0.0898, 51.5279],
},
];
const STATUSES: MapMarkerWorkStatus[] = ["working", "remote", "on-leave", "offline"];
const STATUS_NAMES: Record<MapMarkerWorkStatus, string> = {
working: "In the office",
remote: "Remote",
"on-leave": "On leave",
offline: "Offline",
};
function Team({
people = PEOPLE,
selectedId = null,
onSelect,
}: {
people?: Person[];
selectedId?: string | null;
onSelect?: (id: string) => void;
}) {
return (
<MapView
aria-label="Where the Northwind team works"
defaultCenter={[-0.1, 51.515]}
defaultZoom={12.5}
selectedId={selectedId}
locate
height={460}
>
<MapMarker id="hq" lngLat={HQ}>
<MapMarkerPlace name="Northwind HQ" headcount={24} onPress={() => onSelect?.("hq")} />
</MapMarker>
<MapMarkers
items={people}
getId={(p) => p.id}
getLngLat={(p) => p.lngLat}
getCategory={(p) => p.status}
categories={STATUSES}
clusterLabel={(n) => `${String(n)} people`}
renderMarker={(p, { isSelected }) => (
<MapMarkerPerson
name={p.name}
initials={p.initials}
workStatus={p.status}
isSelected={isSelected}
onPress={() => onSelect?.(p.id)}
/>
)}
/>
</MapView>
);
}
export function ListDrivesSelection() {
const [selected, setSelected] = useState<string | null>(null);
return (
<div className="grid gap-4 md:grid-cols-[260px_minmax(0,1fr)]">
<ul className="m-0 grid list-none content-start gap-1 p-0" aria-label="People">
{PEOPLE.map((p) => (
<li key={p.id}>
<ListItem
title={p.name}
subtitle={STATUS_NAMES[p.status]}
onPress={() => {
setSelected(p.id);
}}
/>
</li>
))}
</ul>
<Team selectedId={selected} onSelect={setSelected} />
</div>
);
}
Location
"Centre on me" with a known position (no browser request), for demos or when the app already knows where you are.
import { MapView } from "@nexera-ui/react";
export function Location() {
return (
<MapView
aria-label="Your location"
defaultCenter={[-0.0901, 51.5155]}
defaultZoom={14}
locate
userLocation={[-0.0901, 51.5155]}
height={360}
/>
);
}
Dark
Dark theme: the same tokens, their dark values.
import { MapMarker, MapMarkerPerson, MapMarkerPlace, MapMarkers, MapView } from "@nexera-ui/react";
interface Person {
id: string;
name: string;
initials: string;
status: MapMarkerWorkStatus;
lngLat: readonly [number, number];
}
const HQ = [-0.0877, 51.5265] as const;
const PEOPLE: Person[] = [
{
id: "ayesha",
name: "Ayesha Khan",
initials: "AK",
status: "working",
lngLat: [-0.0869, 51.5259],
},
{
id: "bilal",
name: "Bilal Ahmed",
initials: "BA",
status: "working",
lngLat: [-0.0841, 51.5248],
},
{ id: "omar", name: "Omar Farooq", initials: "OF", status: "remote", lngLat: [-0.0712, 51.4985] },
{ id: "hina", name: "Hina Raza", initials: "HR", status: "on-leave", lngLat: [-0.1031, 51.5362] },
{ id: "sara", name: "Sara Malik", initials: "SM", status: "remote", lngLat: [-0.0991, 51.5035] },
{ id: "mia", name: "Mia Jensen", initials: "MJ", status: "working", lngLat: [-0.124, 51.5308] },
{
id: "luis",
name: "Luis Ortega",
initials: "LO",
status: "offline",
lngLat: [-0.1357, 51.4975],
},
{
id: "yuki",
name: "Yuki Tanaka",
initials: "YT",
status: "working",
lngLat: [-0.0898, 51.5279],
},
];
const STATUSES: MapMarkerWorkStatus[] = ["working", "remote", "on-leave", "offline"];
function Team({
people = PEOPLE,
selectedId = null,
onSelect,
}: {
people?: Person[];
selectedId?: string | null;
onSelect?: (id: string) => void;
}) {
return (
<MapView
aria-label="Where the Northwind team works"
defaultCenter={[-0.1, 51.515]}
defaultZoom={12.5}
selectedId={selectedId}
locate
height={460}
>
<MapMarker id="hq" lngLat={HQ}>
<MapMarkerPlace name="Northwind HQ" headcount={24} onPress={() => onSelect?.("hq")} />
</MapMarker>
<MapMarkers
items={people}
getId={(p) => p.id}
getLngLat={(p) => p.lngLat}
getCategory={(p) => p.status}
categories={STATUSES}
clusterLabel={(n) => `${String(n)} people`}
renderMarker={(p, { isSelected }) => (
<MapMarkerPerson
name={p.name}
initials={p.initials}
workStatus={p.status}
isSelected={isSelected}
onPress={() => onSelect?.(p.id)}
/>
)}
/>
</MapView>
);
}
const Dark = { globals: { theme: "dark" }, render: () => <Team /> };
Right to left
Right to left: the controls and the credit move to the left; the map itself never mirrors.
import { MapMarker, MapMarkerPlace, MapView } from "@nexera-ui/react";
const HQ = [-0.0877, 51.5265] as const;
export function RightToLeft() {
return (
<MapView
aria-label="خريطة الفريق"
defaultCenter={[-0.1, 51.515]}
defaultZoom={12.5}
locate
height={420}
labels={{
zoomIn: "تكبير",
zoomOut: "تصغير",
locate: "مركز على موقعي",
keyboardHint: "خريطة. الأسهم للتحريك، زائد وناقص للتكبير.",
}}
>
<MapMarker id="hq" lngLat={HQ}>
<MapMarkerPlace name="المقر الرئيسي" headcount={24} headcountLabel="شخصًا" />
</MapMarker>
</MapView>
);
}
Custom style
Your own style or tiles: Nexera colours and the default credit are then yours to provide.
import { MapView } from "@nexera-ui/react";
export function CustomStyle() {
return (
<MapView
aria-label="Map"
mapStyle="https://tiles.openfreemap.org/styles/positron"
defaultCenter={[-0.1, 51.515]}
defaultZoom={12}
height={360}
/>
);
}
Props 21
Press "Try it" on a card to load that prop into the playground.
21 props shown
aria-labelNexerastringaria-labelledbyNexerastringdefaultCenterNexeraLngLatWhere the map opens, as [lng, lat] (uncontrolled).
centerNexeraLngLatControlled centre, as [lng, lat].
defaultZoomNexeranumberOpening zoom, 0 (world) to 20 (uncontrolled).
zoomNexeranumberControlled zoom.
onViewChangeNexera(view: { center: LngLat; zoom: number; }) => voidCalled after the map stops moving.
selectedIdNexerastring | nullFlies to the MapMarker or MapMarkers item with this id, zooming past its cluster.
controlsNexerabooleanZoom in, zoom out and (with locate) Centre on me.
locateNexerabooleanAdds "Centre on me", which asks for the browser's location when pressed.
userLocationNexeraLngLat | nullYour own position for the user (no browser request): demos, or when the app already knows it.
onLocationErrorNexera(reason: "denied" | "unavailable") => voidCalled when the location can't be found, besides the built-in message.
mapStyleNexerastring | StyleSpecificationYour own MapLibre style (URL or object). Nexera colours and the default credit are then yours to provide.
minZoomNexeranumbermaxZoomNexeranumberworkerUrlNexerastringWhere MapLibre's web worker script is loaded from. Bundlers do not copy it from the maplibre-gl package, so by
default it comes from jsDelivr at the installed version; to avoid that request, serve
maplibre-gl/dist/maplibre-gl-worker.mjs yourself and pass its URL.
heightNexerastring | numberHeight of the map box, held before the map loads.
labelsNexeraPartial<MapViewLabels>Visible and announced strings. Translate them.
classNameNexerastringExtra classes, merged last.
styleNexeraCSSPropertieschildrenNexeraReactNodeMapMarker and MapMarkers.
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
This component relies on the shared primitives' accessibility tests; no criteria are tagged to it directly yet.
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
<MapView className="shadow-sm" />Used in blocks
- Team mapMaps and places
- Office finderMaps and places
- People nearbyMaps and places
- Check in hereMaps and places
Related components
- AccordionItemOne collapsible section of an FAQ or settings page.
- AreaChartA stacked area chart: layers piled up over time with the title, range switch, actions and legend of the card.
- AvailabilityRowOne person's availability across a window of the day: an avatar with initials and the name, and a timeline with the busy periods drawn as bars.
- AvatarA person or organisation shown as a photo, initials or an icon.
- AvatarGroupA row of overlapping avatars with a "+N" chip for the rest.
- AvatarLabelAn avatar with a name and an optional subtitle beside it, for a person in a list, a header or a mention.
- BadgeShort status or category label: `Tone` x `Variant` x `Size`, with an optional dot, leading icon and remove button.
- BadgeGroupA pill that pairs a small badge with a short message, for an announcement or a status line.