# Roofline — Real Estate Map Search for React, shadcn/ui & Tailwind (Property Listings)

> A complete real estate property search for React and shadcn/ui: a clustered MapLibre map with price pills, synced to a fast listings panel, smart search, filters with a price histogram, a property page with gallery, reviews and a visit request form, favourites and compare.

- Kind: Component (Search Bars)
- Price: comes with the Pro plan
- License: UpFork Standard License. Use it in unlimited personal and commercial projects. Don't resell or redistribute it on its own.
- Developer: UpFork
- Tags: search, list, map, real-estate, property, rental, listings, maplibre, mapcn, shadcn, nextjs, tailwind
- npm packages: @hookform/resolvers, @tanstack/react-virtual, cmdk, date-fns, embla-carousel-react, lucide-react, maplibre-gl, motion, next-themes, radix-ui, react-hook-form, sonner, supercluster, vaul, zod, zustand
- shadcn/ui components: avatar, button, calendar, collapsible, dialog, dropdown-menu, kbd, popover, skeleton, sonner, switch, tabs, toggle-group, tooltip
- Published: 2026-10-15
- Updated: 2026-10-15
- Page: https://upfork.dev/products/real-estate-map-listings-react-shadcn

## How to get it

Install it into a React + Tailwind project with the shadcn CLI:

```bash
npx shadcn@latest add @upfork/real-estate-map-listings-react-shadcn
```

It installs with the member's personal CLI token (UPFORK_TOKEN), made on their UpFork account page. It comes with UpFork's Pro plan and the plans after it.

## About

A full-screen map of listings with a floating listings panel, filters and a property detail panel, built on
shadcn/ui (Radix), Tailwind CSS 4, [MapLibre GL](https://maplibre.org) in the [mapcn](https://mapcn.dev) way,
`supercluster`, TanStack Virtual, Embla, vaul, react-hook-form + zod, `motion` and `sonner`. Light and dark,
phone to wide desktop.

```bash
pnpm install
pnpm dev                     # http://localhost:5301
pnpm build && pnpm start     # review build on http://localhost:5300
```

```tsx
import { PropertyExplorer } from "@/components/explorer/explorer"

export default function Page() {
  return (
    <main className="h-dvh">
      <PropertyExplorer brand="Roofline" onRequestVisit={async (request) => api.post("/visits", request)} />
    </main>
  )
}
```

| Prop | Default | |
| --- | --- | --- |
| `listings` | the 60 demo listings | your data, `ListingSeed[]` (see `types.ts`); reviews, nearby places and price history are derived |
| `brand` | `"Roofline"` | the wordmark and the agency name on agent cards |
| `storagePrefix` | `"roofline"` | `localStorage` keys: `${prefix}-favourites`, `${prefix}-public-filters` (versioned, migrated) |
| `assetBase` | `""` | prefix for the photos under `/images` (a CDN) |
| `stadiaApiKey` | — | the Outdoors style outside `localhost` (Stadia Maps); without it Outdoors falls back to Default |
| `syncUrl` | `true` | mirror filters, the open property and the camera in the address bar |
| `dashboardHref`, `buyHref` | `/agent/dashboard`, `/pricing` | the footer buttons |
| `onRequestVisit` | — | receives a validated visit request; the success card shows when it resolves |
| `toaster` | `true` | mounts a sonner `<Toaster />`; pass `false` if your app has one |

| `initialFilters` | — | filters to open with (`{ city: "Dubai" }` for a /rent/dubai page), over saved ones; the address bar wins |
| `initialPropertyId` | — | a property to open on load |

The explorer fills its parent: give the parent a height. Its layout follows its own width (under 768 px it is the
phone layout with a bottom sheet), so it also works inside a page section.

## What is in it

- **Map:** CARTO Positron / Dark Matter that follows the theme, OpenFreeMap Streets and Stadia Outdoors with real
  previews in the style menu, a loading bar, and a fallback to Default when a style stays blank; clusters with a type
  ring and breakdown, spiderfied listings that share an address, 32 px type circles below zoom 11 and price pills from
  it, a pulsing halo on the selected listing, muted pills for viewed ones, hover previews, named and focusable markers,
  fit-to-results that respects the floating panels, fly-to with panel-aware offsets, "Search this area" and "Search as
  I move the map", 3D buildings, my location with sort by distance, a legend that filters, themed credits.
- **Listings panel:** context title ("Villas · Dubai") with an animated, announced count, sort, card and list views,
  favourites, filters badge, a search combobox (cities, neighbourhoods, properties, recent searches, `/`), quick chips,
  removable active filters, a virtualized list with photo carousels (only the first photo loads up front), hover sync
  with the map, keyboard navigation, skeletons, an empty state with ways out, collapse to a rail, drag to resize.
- **Filters:** docked beside the panel on wide screens, over it below; sort, availability (switch and status kept in
  sync), type tiles with counts, a price histogram with a two-thumb slider and exact inputs, bedrooms, bathrooms,
  furnishing, parking, rating, amenities, changed-section dots, a live "Show N properties", Save search.
- **Property detail:** gallery with thumbnails, a full-screen lightbox (keys, swipe, zoom) and "Show all photos";
  price with savings, deposit, move-in cost and a six-month trend; key facts with sq ft / m²; Overview, Location (mini
  map and walking times), Reviews (breakdown and reviewer photos) and Similar tabs; agent card with real listing count,
  call, email, message and "View listings"; Request a Visit (zod validation, past and booked days disabled, time slots,
  success card with an `.ics`); previous / next, share, save, full-listing view; a sticky action bar.
- **Also:** compare two or three saved properties, a keyboard cheat sheet (`?`), toasts with Undo, URL state,
  reduced motion everywhere, a phone layout with a snapping bottom sheet and a card row that follows the markers.

## Where things live

| Path | |
| --- | --- |
| `components/explorer/explorer.tsx` | `PropertyExplorer`: providers, persistence, URL state, layout, shortcuts |
| `components/explorer/store.ts` | the zustand store (filters, favourites, UI) and `localStorage` migration |
| `components/explorer/lib/` | filtering and sorting, formatting, URL state, photo paths, labels and colours |
| `components/explorer/map/` | the MapLibre map (adapted from mapcn), clusters and markers, controls and overlays |
| `components/explorer/panel/`, `filters/`, `detail/` | the listings panel, the filters panel, the property detail |
| `components/explorer/data/listings.ts` | the demo listings, agents and reviewers |
| `components/explorer/data/enrich.ts` | derived fields, seeded per listing (stable between visits) |
| `components/explorer/data/media.ts` | gallery assignment and blur previews, generated by `pnpm media` |
| `public/images/` | photos (`pnpm media`, credits in `public/images/CREDITS.md`) and style previews |

The `components/ui/*` files are standard shadcn/ui (new-york, Radix). Installed, the explorer follows your own shadcn/ui
theme; for Roofline's emerald look, copy the `:root` and `.dark` variables from `app/globals.css` (also in
`registry/demos/roofline-theme.css`).

**MapLibre's worker.** MapLibre GL 6 draws on a Web Worker kept in a file of its own
(`node_modules/maplibre-gl/dist/maplibre-gl-worker.mjs`) that app bundlers don't ship. Unless you set one, the
explorer fetches the same version's file from unpkg (jsDelivr as a fallback) once and starts it from a blob. To serve
it yourself, copy that file to `public/` and call `setWorkerUrl("/maplibre-gl-worker.mjs")` from `maplibre-gl` before
the explorer renders. Under a Content-Security-Policy, allow `worker-src blob:`, `data:` in `connect-src` and the map
servers (basemaps.cartocdn.com, tiles.openfreemap.org, tiles.stadiamaps.com).

## Scripts

`pnpm media` downloads and processes every photo (`scripts/media-picks.json`), `node scripts/style-thumbs.mjs` renders
the style previews from a running server, `pnpm shots` takes review screenshots (light and dark, 1440 → 390 px) into
`.review/`, and `node scripts/e2e.mjs` clicks through the main interactions, `node scripts/a11y.mjs` runs axe on the main screens,
`pnpm registry` writes `registry/` (what UpFork lists) and `pnpm zip` packs `roofline.zip` (see `LISTING.md`).

Map data © OpenStreetMap contributors, © CARTO, OpenFreeMap / OpenMapTiles, © Stadia Maps. The map component is
adapted from mapcn (MIT). Photos from Unsplash (see `public/images/CREDITS.md`). Listings, agents and reviews are
fictional.
