Rental
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.
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
Section titled “Preview”Hover a unit for its rent and lease. Select one for the side panel.
<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
Section titled “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-imagesdrops them all. - States never rely on colour alone — each has its own label and icon.
States
Section titled “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, 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
Section titled “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
Section titled “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.
<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
Section titled “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.
<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
Section titled “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
Section titled “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
Section titled “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.
Installation
Section titled “Installation”npm install @nana-tec/ui-componentsOr copy the source in and own it:
npx @nana-tec/ui add unit-mapimport "@nana-tec/ui-components/unit-map";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);});Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
currency | currency | string | 'KES' | Currency the rents and balances are in |
detail | detail | UnitMapDetail | 'tenant' | What each card shows beside the avatars |
explorer | explorer | boolean | false | Add the rail of counts and floors and the search, and open the panel on selection |
highlight | highlight | string | '' | A state to isolate; the other units fade back |
label | label | string | '' | The building's name |
locale | locale | string | '' | BCP 47 locale for money and dates; the reader's own when empty |
noImages | no-images | boolean | false | Leave the thumbnails off the cards |
noLegend | no-legend | boolean | false | Leave out the legend |
noOccupancy | no-occupancy | boolean | false | Leave out the occupancy bar |
panel | panel | boolean | false | Open a side panel with the selected unit's details |
query | query | string | '' | Text to find units or tenants by; the rest fade back |
selected | selected | string | '' | The chosen unit's id |
units | units | StackUnit[] | [] | Every unit: id, label, floor and a status from the unit set, with any of tenants (or one tenant and avatar), image, kind, bedrooms, bathrooms, rent, leaseStart, leaseEnd, balance, note |
Events
| Event | Description |
|---|---|
nana-unit-select | A unit was chosen (detail.unit) |
nana-unit-action | An action in the panel was chosen (detail.action is view or lease, detail.unit) |
nana-panel-close | The panel was closed |
Slots
| Slot | Description |
|---|---|
empty | Shown when there are no units |
actions | Controls beside the title, such as a building switcher or an add-unit button |
panel-actions | Replaces the panel's View unit and Manage lease buttons |
CSS parts
| Part | Description |
|---|---|
::part(base) | The container |
::part(header) | The title, summary, search and actions |
::part(title) | The building's name |
::part(summary) | The occupancy line |
::part(search) | The search field |
::part(occupancy) | The bar showing the share of units in each state |
::part(legend) | The state legend |
::part(legend-item) | One legend entry |
::part(rail) | The explorer's side rail |
::part(plan) | The floors |
::part(floor) | One floor |
::part(floor-label) | The floor's name |
::part(unit) | One unit card |
::part(unit-image) | The unit's thumbnail |
::part(unit-label) | The unit's number |
::part(unit-state) | The unit's state, in words and an icon |
::part(unit-facts) | Bedrooms and bathrooms, or the kind of unit |
::part(avatars) | The stacked tenant avatars |
::part(avatar) | One tenant's photograph or initials |
::part(unit-detail) | The tenant or balance beside the avatars |
::part(preview) | The card shown on hover |
::part(panel) | The side panel for the selected unit |
::part(panel-close) | The panel's close button |
CSS custom properties
| Property | Description |
|---|---|
--nana-unit-map-cell-min | Narrowest a unit card may get before the row wraps |
--nana-unit-map-bg | Container background |
--nana-unit-map-image-ratio | Aspect ratio of a unit's thumbnail |
--nana-unit-map-panel-width | Width of the side panel |
--nana-tone-alert | Colour of the alert tone, used for maintenance |
--nana-tone-change | Colour of the change tone, used for notice given |
--nana-press-scale | How far a unit depresses on press, shared with the other pressable controls |