# Floating Layout

> UI floating over a full-bleed background: a map, a 3D scene, a video or a photo.

Component · Navigation · Halo kit · [Live page](https://ui.twentyfirst.media/halo/components/FloatingLayout/)

UI floating over a full-bleed background: a map, a 3D scene, a video or a photo. Four slots sit along the edges (`top`, `start`, `end`, `bottom`) and everything in them is glass by default, translucent and blurred over what is behind.

- The background stays interactive wherever no slot sits on it: drag the map, click a pin.
- `controls` (zoom, compass, locate) and `attribution` (the map's source notes) sit in the free corner above the bottom bar, clear of the panels and the dock.
- `startWidth` and `endWidth` size the side columns (320px and 340px by default).
- The layout publishes `--floating-safe-top`, `-start`, `-end` and `-bottom` (px): how far its slots reach in. Use them to keep a map library's own controls clear, e.g. `.maplibregl-ctrl-bottom-right { right: var(--floating-safe-end); bottom: var(--floating-safe-bottom) }`.
- Below 720px of width the slots stack (top, start, the background, bottom) and the layout grows with them; the `end` slot becomes a bottom sheet over the background whose handle expands and collapses it (`endSheet={false}` stacks it instead).
- `surface="solid"` turns the glass off; `surface="solid"` on a single Panel or Card opts just that one out.
- Panels, cards, the dock, map legends and controls, and loose controls (search field, chips, icon buttons, segmented controls) turn into glass inside it; menus, popovers and dialogs take the stronger glass fill.
- Text on glass keeps 4.5:1 over any backdrop. Reduced transparency, more contrast and forced colours fall back to solid surfaces.

## Example

```tsx
import * as React from "react";
import {
  Badge,
  FloatingLayout,
  IconButton,
  KeyValue,
  MapView,
  Panel,
  SearchField,
  Segmented,
} from "@twentyfirstmedia/halo";
import type { MapPoint } from "@twentyfirstmedia/halo";

const hubs: MapPoint[] = [
  { id: "ham", label: "Hamburg", lat: 53.5, lng: 10, value: 840, sub: "840 parcels today" },
  { id: "rot", label: "Rotterdam", lat: 51.9, lng: 4.5, value: 610, tint: "mint" },
  { id: "nyc", label: "New York", lat: 40.7, lng: -74, value: 380, tint: "butter" },
  { id: "dxb", label: "Dubai", lat: 25.2, lng: 55.3, value: 290, tint: "lilac", pulse: true },
  { id: "sgp", label: "Singapore", lat: 1.3, lng: 103.8, value: 450, tint: "blush" },
];

export default function Preview() {
  const [view, setView] = React.useState("Today");
  return (
    <FloatingLayout
      label="Shipments"
      height={520}
      background={<MapView label="Shipments by hub" bubbles points={hubs} height="100%" />}
      controls={
        <div className="tf-col" style={{ gap: 8 }}>
          <IconButton icon="plus" label="Zoom in" variant="outline" />
          <IconButton icon="minus" label="Zoom out" variant="outline" />
        </div>
      }
      attribution="© OpenStreetMap contributors"
      top={
        <>
          <SearchField placeholder="Search hubs" style={{ flex: "1 1 220px", maxWidth: 360 }} />
          <IconButton icon="filter" label="Filters" variant="outline" />
        </>
      }
      end={
        <Panel
          title="Hamburg"
          subtitle="Hub"
          action={
            <Badge tone="success" dot>
              On time
            </Badge>
          }
        >
          <KeyValue
            items={[
              ["Parcels", "840"],
              ["Delayed", "12"],
              ["Next truck", "14:20"],
            ]}
          />
        </Panel>
      }
      bottom={
        <Segmented
          label="Period"
          options={["Today", "Week", "Month"]}
          value={view}
          onChange={setView}
        />
      }
    />
  );
}
```

## 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. |

### 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. |
