# Map

> A city map with glass controls floating over it: search and filters on top, map layers on the start side, the selected place with directions on the end side, zoom buttons and a dock.

Pattern · Pro kit · [Live page](https://ui.twentyfirst.media/pro/patterns/MapScreen/)

A city map with glass controls floating over it: search and filters on top, map layers on the start side, the selected place with directions on the end side, zoom buttons and a dock.

- Built from `FloatingLayout` and `MapView`; the map is drawn from the kit's colours, so it follows the kit and the theme. Put your own map (MapLibre, Mapbox, Cesium, three.js) into `background` instead.
- Everything floating is glass: translucent, blurred and readable over any backdrop.

## Code

```tsx
import * as React from "react";
import {
  Badge,
  Button,
  ButtonGroup,
  Chip,
  Dock,
  FloatingLayout,
  IconButton,
  KeyValue,
  MapView,
  Panel,
  SearchField,
  Segmented,
  Switch,
} from "@twentyfirstmedia/pro";
import type { MapPoint } from "@twentyfirstmedia/pro";

/** A city drawn from the kit's colours, so it follows the kit and the theme. */
function CityMap() {
  const streets = [80, 190, 300, 410, 520, 630, 740];
  const avenues = [90, 230, 370, 510, 650, 790, 930, 1070];
  return (
    <svg viewBox="0 0 1200 800" preserveAspectRatio="xMidYMid slice" aria-hidden>
      <rect width="1200" height="800" fill="var(--background)" />
      {avenues.flatMap((x, i) =>
        streets.map((y, j) => (
          <rect
            key={`${i}-${j}`}
            x={x + 12}
            y={y + 12}
            width={116}
            height={86}
            rx={6}
            fill="var(--muted)"
          />
        )),
      )}
      <rect x="652" y="82" width="256" height="196" rx="18" fill="var(--mint)" />
      <rect x="92" y="522" width="256" height="196" rx="18" fill="var(--mint)" />
      <circle cx="790" cy="180" r="34" fill="var(--sky)" />
      <path
        d="M-40 470 C 180 400, 320 560, 520 470 S 860 330, 1240 430 L 1240 560 C 900 470, 720 610, 520 600 S 160 540, -40 610 Z"
        fill="var(--sky)"
      />
      <g stroke="var(--card)" strokeWidth="10" strokeLinecap="round">
        {streets.map((y) => (
          <line key={y} x1="0" x2="1200" y1={y + 6} y2={y + 6} />
        ))}
        {avenues.map((x) => (
          <line key={x} x1={x + 6} x2={x + 6} y1="0" y2="800" />
        ))}
      </g>
      <g stroke="var(--butter)" strokeWidth="16" strokeLinecap="round" fill="none">
        <path d="M0 196 L 1200 196" />
        <path d="M516 0 L 516 800" />
        <path d="M150 800 L 660 330 L 1200 120" />
      </g>
    </svg>
  );
}

const places: MapPoint[] = [
  {
    id: "cafe",
    label: "Kaffeerösterei",
    sub: "Café · open until 18:00",
    lat: 72,
    lng: 44,
    text: "Café",
    tint: "butter",
  },
  {
    id: "museum",
    label: "Harbour Museum",
    sub: "Museum · €12",
    lat: 50,
    lng: 54,
    text: "Museum",
    tint: "lilac",
  },
  {
    id: "market",
    label: "Fish Market",
    sub: "Market · Sundays 5–9:30",
    lat: 24,
    lng: 40,
    text: "Market",
    tint: "blush",
  },
  {
    id: "park",
    label: "Planten un Blomen",
    sub: "Park",
    lat: 32,
    lng: 60,
    text: "Park",
    tint: "mint",
  },
];

export default function Preview() {
  const [selected, setSelected] = React.useState<MapPoint["id"] | null>("museum");
  const [mode, setMode] = React.useState("Explore");
  const place = places.find((p) => p.id === selected);
  return (
    <FloatingLayout
      label="City map"
      height={720}
      background={
        <MapView
          label="Map of the harbour district"
          background={<CityMap />}
          bounds={{ n: 100, s: 0, w: 0, e: 100 }}
          points={places}
          selected={selected}
          onSelect={setSelected}
          height="100%"
        />
      }
      top={
        <>
          <SearchField
            placeholder="Search places and addresses"
            style={{ flex: "1 1 260px", maxWidth: 420 }}
          />
          <Chip>Coffee</Chip>
          <Chip>Museums</Chip>
          <Chip selected>Open now</Chip>
        </>
      }
      start={
        <Panel title="Layers" subtitle="What the map shows">
          <Segmented
            label="Map style"
            options={["Explore", "Transit", "Satellite"]}
            value={mode}
            onChange={setMode}
            block
          />
          <Switch defaultChecked>Traffic</Switch>
          <Switch defaultChecked>Parks and water</Switch>
          <Switch>Bike lanes</Switch>
        </Panel>
      }
      controls={
        <ButtonGroup orientation="vertical" label="Zoom">
          <IconButton icon="plus" label="Zoom in" variant="outline" />
          <IconButton icon="minus" label="Zoom out" variant="outline" />
        </ButtonGroup>
      }
      attribution="Map data © OpenStreetMap contributors"
      end={
        place && (
          <Panel
            title={place.label}
            subtitle={typeof place.sub === "string" ? place.sub : undefined}
            action={
              <Badge tone="success" dot>
                Open
              </Badge>
            }
          >
            <KeyValue
              items={[
                ["Walk", "12 min"],
                ["Distance", "850 m"],
                ["Rating", "4.7 of 5"],
              ]}
            />
            <div className="tf-row" style={{ gap: 8 }}>
              <Button variant="primary" icon="send">
                Directions
              </Button>
              <Button variant="secondary" icon="star">
                Save
              </Button>
            </div>
          </Panel>
        )
      }
      bottom={
        <Dock
          label="Map tools"
          items={[
            { icon: "search", label: "Search", tint: "sky" },
            { icon: "star", label: "Saved", tint: "butter" },
            { icon: "clock", label: "Recent", tint: "mint" },
            { icon: "share", label: "Share", tint: "lilac" },
          ]}
        />
      }
    />
  );
}
```

## Props

### Badge

| Prop | Type | Notes |
| --- | --- | --- |
| `tone?` | `Tone` |  |
| `dot?` | `boolean` |  |
| `live?` | `boolean` |  |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Button

| Prop | Type | Notes |
| --- | --- | --- |
| `variant?` | `"primary" \| "secondary" \| "soft" \| "ghost" \| "danger" \| "danger-solid"` |  |
| `size?` | `"md" \| "sm"` |  |
| `icon?` | `IconName` |  |
| `iconRight?` | `IconName` |  |
| `disabled?` | `boolean` |  |
| `type?` | `"button" \| "submit" \| "reset"` |  |
| `onClick?` | `(e: MouseEvent<HTMLButtonElement>) => void` |  |
| `children?` | `ReactNode` |  |
| `asChild?` | `boolean` | Render the single child element (a link, a router Link) with the button's look instead of a <button>. |
| `id?` | `string` |  |
| `name?` | `string` | Form field name and value, sent with the form when this button submits it. |
| `value?` | `string` |  |
| `form?` | `string` | The id of the form this button belongs to, when it sits outside it. |
| `title?` | `string` |  |
| `tabIndex?` | `number` |  |
| `autoFocus?` | `boolean` |  |
| `aria-label?` | `string` |  |
| `aria-labelledby?` | `string` |  |
| `aria-describedby?` | `string` |  |
| `aria-expanded?` | `boolean \| "true" \| "false"` | For disclosure and menu triggers: whether what the button controls is open. |
| `aria-controls?` | `string` |  |
| `aria-haspopup?` | `boolean \| "true" \| "false" \| "menu" \| "listbox" \| "tree" \| "grid" \| "dialog"` |  |
| `aria-pressed?` | `boolean \| "true" \| "false" \| "mixed"` | For toggle buttons. |
| `aria-current?` | `boolean \| "true" \| "false" \| "page" \| "step" \| "location" \| "date" \| "time"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### ButtonGroup

| Prop | Type | Notes |
| --- | --- | --- |
| `label?` | `string` | Accessible name of the group. |
| `orientation?` | `"horizontal" \| "vertical"` |  |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Chip

| Prop | Type | Notes |
| --- | --- | --- |
| `selected?` | `boolean` |  |
| `icon?` | `IconName` |  |
| `count?` | `number` |  |
| `onClick?` | `(e: MouseEvent<HTMLButtonElement>) => void` |  |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Dock

| Prop | Type | Notes |
| --- | --- | --- |
| `items` | `DockItem[]` | Required |
| `label?` | `string` |  |
| `surface?` | `Surface` | `"glass"`: translucent glass over what is behind (maps, 3D, video, photos); `"solid"` opts out inside a glass `UIProvider`. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### FloatingLayout

| Prop | Type | Notes |
| --- | --- | --- |
| `background?` | `ReactNode` | What the UI floats over, filling the layout: a map, a 3D canvas, a video, a photo. |
| `top?` | `ReactNode` | Bar along the top edge (search, filters, title). |
| `start?` | `ReactNode` | Column on the start side (layers, lists, settings). |
| `end?` | `ReactNode` | Column on the end side (details of the selected thing). |
| `bottom?` | `ReactNode` | Bar along the bottom edge (dock, playback, a summary). |
| `controls?` | `ReactNode` | Controls of the background (zoom, compass, locate) in the free corner above the bottom bar. |
| `attribution?` | `ReactNode` | Source notes of the background (map attribution), under `controls`, clear of every slot. |
| `startWidth?` | `number \| string` | Width of the start column (default 320px) and of the end column (default 340px). |
| `endWidth?` | `number \| string` |  |
| `endSheet?` | `boolean` | Below 720px of width the end slot becomes a bottom sheet over the background, with a handle that expands and collapses it (default true). `false` stacks it with the others. |
| `height?` | `number \| string` | Height of the layout (default 640). Below 720px of width the slots stack: top, start, end, bottom. The layout sets `--floating-safe-top/-start/-end/-bottom` (px) to the space its slots take, so map libraries can keep their own controls clear of them. |
| `surface?` | `Surface` | Glass for every surface in the slots (default) or solid surfaces. |
| `label?` | `string` | Accessible name of the region. |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### IconButton

| Prop | Type | Notes |
| --- | --- | --- |
| `icon` | `IconName` | Required |
| `label` | `string` | Accessible name and tooltip (`aria-label` and `title`); `aria-label` or `title` override them. |
| `variant?` | `"soft" \| "solid" \| "outline" \| "ghost"` |  |
| `size?` | `"sm" \| "md" \| "lg"` |  |
| `type?` | `"button" \| "submit" \| "reset"` |  |
| `onClick?` | `(e: MouseEvent<HTMLButtonElement>) => void` |  |
| `disabled?` | `boolean` |  |
| `asChild?` | `boolean` | Render the single child element (a link) with the button's look; the icon is placed inside it. |
| `children?` | `ReactNode` |  |
| `id?` | `string` |  |
| `name?` | `string` | Form field name and value, sent with the form when this button submits it. |
| `value?` | `string` |  |
| `form?` | `string` | The id of the form this button belongs to, when it sits outside it. |
| `title?` | `string` |  |
| `tabIndex?` | `number` |  |
| `autoFocus?` | `boolean` |  |
| `aria-label?` | `string` |  |
| `aria-labelledby?` | `string` |  |
| `aria-describedby?` | `string` |  |
| `aria-expanded?` | `boolean \| "true" \| "false"` | For disclosure and menu triggers: whether what the button controls is open. |
| `aria-controls?` | `string` |  |
| `aria-haspopup?` | `boolean \| "true" \| "false" \| "menu" \| "listbox" \| "tree" \| "grid" \| "dialog"` |  |
| `aria-pressed?` | `boolean \| "true" \| "false" \| "mixed"` | For toggle buttons. |
| `aria-current?` | `boolean \| "true" \| "false" \| "page" \| "step" \| "location" \| "date" \| "time"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### KeyValue

| Prop | Type | Notes |
| --- | --- | --- |
| `items` | `KeyValueItem[]` | Required |
| `dense?` | `boolean` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### MapView

| Prop | Type | Notes |
| --- | --- | --- |
| `points?` | `MapPoint[]` |  |
| `bounds?` | `{ n: number; s: number; w: number; e: number }` |  |
| `background?` | `ReactNode` |  |
| `bubbles?` | `boolean` |  |
| `selected?` | `MapPoint["id"] \| null` |  |
| `onSelect?` | `(id: MapPoint["id"] \| null) => void` |  |
| `renderPopup?` | `(p: MapPoint) => ReactNode` |  |
| `legend?` | `ReactNode` |  |
| `controls?` | `ReactNode` |  |
| `height?` | `number \| string` | Height in px (default 320) or any CSS length, e.g. "100%" to fill a FloatingLayout. |
| `label?` | `string` |  |
| `surface?` | `Surface` | `"glass"`: the legend, controls and popup float as glass over the map (for a live map in `background`). |
| `className?` | `string` |  |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Panel

| Prop | Type | Notes |
| --- | --- | --- |
| `side?` | `boolean` |  |
| `title?` | `string` |  |
| `subtitle?` | `string` |  |
| `action?` | `ReactNode` |  |
| `children?` | `ReactNode` |  |
| `surface?` | `Surface` | `"glass"`: translucent glass over what is behind (maps, 3D, video, photos); `"solid"` opts out inside a glass `UIProvider`. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### SearchField

| Prop | Type | Notes |
| --- | --- | --- |
| `placeholder?` | `string` |  |
| `label?` | `string` | Accessible name (default: the placeholder, or "Search"). |
| `shortcut?` | `string` | The key hint, e.g. "⌘K" (hidden on touch screens). |
| `bindShortcut?` | `boolean` | ⌘/Ctrl + the hint's last key (K for "⌘K") focuses the field, except while typing elsewhere. |
| `value?` | `string` |  |
| `defaultValue?` | `string` |  |
| `onChange?` | `(e: ChangeEvent<HTMLInputElement>) => void` |  |
| `onKeyDown?` | `(e: KeyboardEvent<HTMLInputElement>) => void` |  |
| `onFocus?` | `(e: FocusEvent<HTMLInputElement>) => void` |  |
| `onBlur?` | `(e: FocusEvent<HTMLInputElement>) => void` |  |
| `id?` | `string` |  |
| `name?` | `string` |  |
| `autoFocus?` | `boolean` |  |
| `inputProps?` | `InputHTMLAttributes<HTMLInputElement>` | For a search with suggestions (combobox): role, aria-controls, aria-activedescendant … |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Segmented

| Prop | Type | Notes |
| --- | --- | --- |
| `options` | `(string \| Option)[]` | Required |
| `value?` | `string` |  |
| `defaultValue?` | `string` |  |
| `onChange?` | `(v: string) => void` |  |
| `size?` | `"sm"` |  |
| `block?` | `boolean` |  |
| `label?` | `string` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Switch

| Prop | Type | Notes |
| --- | --- | --- |
| `name?` | `string` |  |
| `checked?` | `boolean` |  |
| `defaultChecked?` | `boolean` |  |
| `onChange?` | `(e: ChangeEvent<HTMLInputElement>) => void` |  |
| `children?` | `ReactNode` |  |
| `spread?` | `boolean` |  |
| `disabled?` | `boolean` |  |
| `label?` | `string` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
