# Unit Map

> Source: https://design.nanatec.co.ke/components/unit-map/
> A building as an interactive explorer — each floor a row of unit cards with the unit's number, its state, a small photograph and the faces of the people who live there. Hover for rent and lease, select for the side panel.
> Status: beta
> Since: v0.0.36

Not a floor-plan diagram: an information system with a human layer. Thin
borders, strong numbers and restrained colour carry the structure; the
photographs and avatars say which home is which and who lives there.

## Preview

Hover a unit for its rent and lease. Select one for the side panel.

```html
<nana-unit-map
  label="Building A"
  panel
  selected="401"
  units='[{"id":"401","label":"A-401","floor":4,"status":"occupied","bedrooms":2,"bathrooms":2,"rent":45000,"leaseStart":"2026-01-01","leaseEnd":"2026-12-31","image":"https://images.pexels.com/photos/7060814/pexels-photo-7060814.jpeg?auto=compress&cs=tinysrgb&w=480&h=300&fit=crop","tenants":[{"name":"John Kamau","avatar":"https://images.pexels.com/photos/6569952/pexels-photo-6569952.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"},{"name":"Mary Kamau","avatar":"https://images.pexels.com/photos/7657229/pexels-photo-7657229.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop","role":"Co-tenant"}]},{"id":"402","label":"A-402","floor":4,"status":"vacant","bedrooms":1,"bathrooms":1,"rent":32000,"image":"https://images.pexels.com/photos/8146149/pexels-photo-8146149.jpeg?auto=compress&cs=tinysrgb&w=480&h=300&fit=crop"},{"id":"403","label":"A-403","floor":4,"status":"occupied","bedrooms":2,"bathrooms":2,"rent":45000,"image":"https://images.pexels.com/photos/5998024/pexels-photo-5998024.jpeg?auto=compress&cs=tinysrgb&w=480&h=300&fit=crop","tenants":[{"name":"Achieng Otieno","avatar":"https://images.pexels.com/photos/29852895/pexels-photo-29852895.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"},{"name":"Brian Otieno"},{"name":"Zawadi Otieno"},{"name":"Imani Otieno"}]},{"id":"404","label":"A-404","floor":4,"status":"reserved","bedrooms":1,"bathrooms":1,"rent":32000,"image":"https://images.pexels.com/photos/6032225/pexels-photo-6032225.jpeg?auto=compress&cs=tinysrgb&w=480&h=300&fit=crop","tenant":"Wekesa Brian"},{"id":"301","label":"A-301","floor":3,"status":"arrears","bedrooms":2,"bathrooms":1,"rent":40000,"balance":42000,"image":"https://images.pexels.com/photos/7031720/pexels-photo-7031720.jpeg?auto=compress&cs=tinysrgb&w=480&h=300&fit=crop","tenant":"Kamau Juma","leaseEnd":"2027-03-31"},{"id":"302","label":"A-302","floor":3,"status":"occupied","bedrooms":3,"bathrooms":2,"rent":60000,"image":"https://images.pexels.com/photos/6969863/pexels-photo-6969863.jpeg?auto=compress&cs=tinysrgb&w=480&h=300&fit=crop","tenants":[{"name":"Njeri Wambui","avatar":"https://images.pexels.com/photos/7577049/pexels-photo-7577049.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"},{"name":"Peter Wambui"}]},{"id":"303","label":"A-303","floor":3,"status":"maintenance","bedrooms":1,"bathrooms":1,"rent":32000,"note":"Plumbing until 14 Oct"},{"id":"304","label":"A-304","floor":3,"status":"notice","bedrooms":2,"bathrooms":2,"rent":45000,"tenant":"Mutua Peter","leaseEnd":"2026-11-30"},{"id":"201","label":"A-201","floor":2,"status":"occupied","bedrooms":0,"bathrooms":1,"rent":22000,"tenant":"Chebet Langat","avatar":"https://images.pexels.com/photos/7577049/pexels-photo-7577049.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"},{"id":"202","label":"A-202","floor":2,"status":"vacant","bedrooms":0,"bathrooms":1,"rent":22000}]'
></nana-unit-map>
```

## The card

```
┌──────────────────────────┐
│        thumbnail         │   16:9, lazy-loaded, one size on every card
├──────────────────────────┤
│ A-304         ▣ Occupied │   the number, and the state in words with an icon
│ 2 Bed · 2 Bath           │   rooms, or the kind of unit
│ (JK)(MK)+1   John Kamau  │   stacked avatars, then the tenant or the balance
└──────────────────────────┘
```

- **Avatars** show who lives there without opening the unit. Up to three faces
  overlap; past that the last becomes a count, `+2`. Without a photograph a
  tenant gets their initials. An empty unit shows a dashed seat.
- **Thumbnails** are small on purpose: the larger picture is in the hover card
  and the panel. Once any unit has an `image`, every card makes room for one
  so a row keeps one height; `no-images` drops them all.
- **States** never rely on colour alone — each has its own label and icon.

## States

| Value | Reads as | Colour |
|---|---|---|
| `vacant` or `available` | Available | Neutral, with a dashed seat |
| `occupied` | Occupied | The brand colour |
| `reserved` | Reserved | Amber |
| `maintenance` | Maintenance | Orange |
| `arrears` or `overdue` | Overdue | Red |
| `notice` | Notice given | Purple |

These come from the `unit` set of [Status](/components/status/), so a
`<nana-status set="unit">` elsewhere on the page uses the same words and
colours. A unit on notice or overdue still counts as occupied: it has a tenant.

## Hover and select

Hovering a card lifts it, brings the thumbnail slightly closer and spreads the
avatars; after a moment a card opens beside it with the larger photograph, the
kind of unit, every tenant, the rent and the lease. Keyboard focus opens the
same card.

Selecting a unit fires `nana-unit-select`. With `panel`, it also opens a side
panel instead of sending the reader away: the photograph, the tenants with
their roles, rent per month, the lease, any balance, and two actions that fire
`nana-unit-action` with `view` or `lease`. Put your own buttons in the
`panel-actions` slot to replace them. Escape or the close button fires
`nana-panel-close`.

## Explorer

`explorer` turns the map into a property explorer: a rail with the building's
counts by state — each one a filter — and its floors to jump between, a search
across units and tenants, and the panel on selection. On a narrow screen the
rail folds away and the panel drops under the plan.

```html
<nana-unit-map
  label="Building A"
  explorer
  units='[{"id":"401","label":"A-401","floor":4,"status":"occupied","bedrooms":2,"bathrooms":2,"rent":45000,"leaseStart":"2026-01-01","leaseEnd":"2026-12-31","image":"https://images.pexels.com/photos/7060814/pexels-photo-7060814.jpeg?auto=compress&cs=tinysrgb&w=480&h=300&fit=crop","tenants":[{"name":"John Kamau","avatar":"https://images.pexels.com/photos/6569952/pexels-photo-6569952.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"},{"name":"Mary Kamau","avatar":"https://images.pexels.com/photos/7657229/pexels-photo-7657229.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop","role":"Co-tenant"}]},{"id":"402","label":"A-402","floor":4,"status":"vacant","bedrooms":1,"bathrooms":1,"rent":32000,"image":"https://images.pexels.com/photos/8146149/pexels-photo-8146149.jpeg?auto=compress&cs=tinysrgb&w=480&h=300&fit=crop"},{"id":"403","label":"A-403","floor":4,"status":"occupied","bedrooms":2,"bathrooms":2,"rent":45000,"image":"https://images.pexels.com/photos/5998024/pexels-photo-5998024.jpeg?auto=compress&cs=tinysrgb&w=480&h=300&fit=crop","tenants":[{"name":"Achieng Otieno","avatar":"https://images.pexels.com/photos/29852895/pexels-photo-29852895.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"},{"name":"Brian Otieno"},{"name":"Zawadi Otieno"},{"name":"Imani Otieno"}]},{"id":"404","label":"A-404","floor":4,"status":"reserved","bedrooms":1,"bathrooms":1,"rent":32000,"image":"https://images.pexels.com/photos/6032225/pexels-photo-6032225.jpeg?auto=compress&cs=tinysrgb&w=480&h=300&fit=crop","tenant":"Wekesa Brian"},{"id":"301","label":"A-301","floor":3,"status":"arrears","bedrooms":2,"bathrooms":1,"rent":40000,"balance":42000,"image":"https://images.pexels.com/photos/7031720/pexels-photo-7031720.jpeg?auto=compress&cs=tinysrgb&w=480&h=300&fit=crop","tenant":"Kamau Juma","leaseEnd":"2027-03-31"},{"id":"302","label":"A-302","floor":3,"status":"occupied","bedrooms":3,"bathrooms":2,"rent":60000,"image":"https://images.pexels.com/photos/6969863/pexels-photo-6969863.jpeg?auto=compress&cs=tinysrgb&w=480&h=300&fit=crop","tenants":[{"name":"Njeri Wambui","avatar":"https://images.pexels.com/photos/7577049/pexels-photo-7577049.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"},{"name":"Peter Wambui"}]},{"id":"303","label":"A-303","floor":3,"status":"maintenance","bedrooms":1,"bathrooms":1,"rent":32000,"note":"Plumbing until 14 Oct"},{"id":"304","label":"A-304","floor":3,"status":"notice","bedrooms":2,"bathrooms":2,"rent":45000,"tenant":"Mutua Peter","leaseEnd":"2026-11-30"},{"id":"201","label":"A-201","floor":2,"status":"occupied","bedrooms":0,"bathrooms":1,"rent":22000,"tenant":"Chebet Langat","avatar":"https://images.pexels.com/photos/7577049/pexels-photo-7577049.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"},{"id":"202","label":"A-202","floor":2,"status":"vacant","bedrooms":0,"bathrooms":1,"rent":22000}]'
></nana-unit-map>
```

## Balances on the cards

`detail="balance"` puts each unit's balance beside the avatars — owed in red,
credit, or paid up. With a state picked in the legend, the rest fade back.

```html
<nana-unit-map
  label="Building A"
  detail="balance"
  highlight="arrears"
  no-images
  units='[{"id":"401","label":"A-401","floor":4,"status":"occupied","bedrooms":2,"bathrooms":2,"rent":45000,"leaseStart":"2026-01-01","leaseEnd":"2026-12-31","tenants":[{"name":"John Kamau","avatar":"https://images.pexels.com/photos/6569952/pexels-photo-6569952.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"},{"name":"Mary Kamau","avatar":"https://images.pexels.com/photos/7657229/pexels-photo-7657229.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop","role":"Co-tenant"}]},{"id":"402","label":"A-402","floor":4,"status":"vacant","bedrooms":1,"bathrooms":1,"rent":32000},{"id":"403","label":"A-403","floor":4,"status":"occupied","bedrooms":2,"bathrooms":2,"rent":45000,"tenants":[{"name":"Achieng Otieno","avatar":"https://images.pexels.com/photos/29852895/pexels-photo-29852895.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"},{"name":"Brian Otieno"},{"name":"Zawadi Otieno"},{"name":"Imani Otieno"}]},{"id":"404","label":"A-404","floor":4,"status":"reserved","bedrooms":1,"bathrooms":1,"rent":32000,"tenant":"Wekesa Brian"},{"id":"301","label":"A-301","floor":3,"status":"arrears","bedrooms":2,"bathrooms":1,"rent":40000,"balance":42000,"tenant":"Kamau Juma","leaseEnd":"2027-03-31"},{"id":"302","label":"A-302","floor":3,"status":"occupied","bedrooms":3,"bathrooms":2,"rent":60000,"tenants":[{"name":"Njeri Wambui","avatar":"https://images.pexels.com/photos/7577049/pexels-photo-7577049.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"},{"name":"Peter Wambui"}]},{"id":"303","label":"A-303","floor":3,"status":"maintenance","bedrooms":1,"bathrooms":1,"rent":32000,"note":"Plumbing until 14 Oct"},{"id":"304","label":"A-304","floor":3,"status":"notice","bedrooms":2,"bathrooms":2,"rent":45000,"tenant":"Mutua Peter","leaseEnd":"2026-11-30"},{"id":"201","label":"A-201","floor":2,"status":"occupied","bedrooms":0,"bathrooms":1,"rent":22000,"tenant":"Chebet Langat","avatar":"https://images.pexels.com/photos/7577049/pexels-photo-7577049.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"},{"id":"202","label":"A-202","floor":2,"status":"vacant","bedrooms":0,"bathrooms":1,"rent":22000}]'
  style="max-width:48rem"
></nana-unit-map>
```

## Unit shape

| Field | Meaning |
|---|---|
| `id` | Your key for the unit; comes back in every event |
| `label` | The unit's number, as it is written on the door |
| `floor` | `0` is the ground, negatives are basements |
| `status` | A value from the table above |
| `tenants` | Everyone on the lease, as `{ name, avatar?, role? }`; the first is the primary tenant |
| `tenant`, `avatar` | A shorthand for a single tenant |
| `image` | A photograph of the unit |
| `bedrooms`, `bathrooms` | Shown as "2 Bed · 2 Bath"; `0` bedrooms is a studio |
| `kind` | What the unit is when rooms do not say it — "Shop", "Office" |
| `rent` | Monthly rent, in `currency` |
| `leaseStart`, `leaseEnd` | ISO dates |
| `balance` | Positive is owed, negative is credit |
| `note` | Read out with the unit and shown in the panel |

## Keyboard

The plan is one tab stop. Arrow keys move along a floor and up and down
between floors; Home and End go to the ends of a floor, Page Up and Page Down
to the top and bottom of the building. Enter or Space selects, and Escape
closes the hover card, then the panel.

## A drawn floor plan

For a building with dozens of units on a floor, a drawing of the floor itself —
each apartment a shape you can click — places a unit faster than any grid.
That needs the building's geometry, which this component does not have; pair
it with your own SVG plan and drive the same `selected` and `highlight` from
both.

Photographs on this page are from [Pexels](https://www.pexels.com).

## Installation

```bash
npm install @nana-tec/ui-components
```

Or copy the source in and own it:

```bash
npx @nana-tec/ui add unit-map
```

## Usage

```js
import "@nana-tec/ui-components/unit-map";
```

```js
const map = document.querySelector("nana-unit-map");

map.units = await api.units(buildingId);
map.addEventListener("nana-unit-select", (e) => history.replaceState(null, "", `#${e.detail.unit.id}`));
map.addEventListener("nana-unit-action", (e) => {
  if (e.detail.action === "lease") openLease(e.detail.unit.id);
});
```

## API

## Storybook

See every state and prop interactively in Storybook: https://storybook.nanatec.co.ke/?path=/docs/components-unit-map--docs
