# Nextjs Real Estate Template: Search, Map and Mortgage

> A nextjs real estate template with buy or rent search, listing cards, a price pin map, a mortgage estimate, a valuation form and a phone layout that works.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Nov 6, 2026
- Category: [Build it: screens and apps](https://wingo-ui.com/blog/category/build)
- Reading time: 10 min
- Canonical: https://wingo-ui.com/blog/nextjs-real-estate-template

## TL;DR

A nextjs real estate template needs more than a hero and a grid of houses: a buy or rent search that filters live, listing cards with price and facts, a map with price pins, neighborhood tiles, a mortgage estimate and a valuation form that captures sellers. The Wingo UI Real Estate Page (Pro) ships all of them in one client component, with callbacks for search, saved homes and valuation requests. For a results page with hundreds of homes, add the Listing Grid and a List / Map toggle on phones.

You are building a site for an agency or a listings startup, and the next js real estate template you find usually ships a hero, a grid of houses and a contact form. A nextjs real estate template that buyers use needs more than that. Buyers switch between buying and renting, narrow by price and bedrooms, look at where the homes are, and want to know what the monthly payment would be before they call anyone. Sellers want to know what their home is worth. This guide walks through each piece of a real estate site that buyers use, how the Wingo UI [Real Estate Page](https://wingo-ui.com/templates/real-estate-page) handles it, and how the layout changes on a phone. We build Wingo UI, so we are not neutral; the patterns work with any components.

## What does a real estate website template need?

Seven sections, in roughly this order, because that is the order of the questions a visitor has:

| Section | The visitor's question | What it needs |
| --- | --- | --- |
| Search | "Is there anything for me?" | Buy or rent, location, price, beds, a live count |
| Featured homes | "Show me" | Cards with photo, price, beds, baths, area, a heart |
| Map | "Where are they?" | Price pins and a list that preview a home without leaving the page |
| Neighborhoods | "Which area fits?" | Median price or rent, trend, homes listed |
| Mortgage | "Can I afford it?" | Price, down payment, rate, term, the monthly total and its parts |
| Agents | "Who do I talk to?" | Name, areas, languages, Call and Email |
| Valuation | "What is my home worth?" | A short form that turns an owner into a lead |

As of November 6, 2026, the [Homely template on WrapPixel](https://wrappixel.com/templates/homely-nextjs-template-for-real-estate) costs $29 for a single use license or $59 for multiple use and lists a home page, a property listing, a property detail, a blog, a contact page and a 404 on Next.js 15, React 19, Tailwind v4 and shadcn. The [Clay template on Sanity](https://www.sanity.io/templates/clay-modern-real-estate-next-js-template) is paid, runs on Next.js 14 with Sanity as the CMS and adds a blog and an agent contact form through Resend. Neither listing mentions a map, a mortgage estimate or a valuation form. For a CMS-backed site where you build search yourself, those are a fair start.

## How do you add the template to a Next.js app?

Install it with the CLI. It copies the page, its sample data and every component it uses into your project as source:

```bash
npx wingo-ui@latest add real-estate-page
```

The page is a client component. Load your listings on the server and hand them, with your handlers, to a small client view:

```tsx
// app/page.tsx
import { getListings, getNeighborhoods } from "@/lib/listings"
import { HomesView } from "./homes-view"

export default async function Page() {
  const [listings, neighborhoods] = await Promise.all([getListings(), getNeighborhoods()])
  return <HomesView listings={listings} neighborhoods={neighborhoods} />
}
```

```tsx
// app/homes-view.tsx
"use client"

import Link from "next/link"
import { RealEstatePage, type RealEstateSearch } from "@/components/templates/real-estate-page"
import type { RealEstateListing, RealEstateNeighborhood } from "@/components/templates/real-estate-page-data"
import { requestValuation, saveHome } from "./actions"

export function HomesView({
  listings,
  neighborhoods,
}: {
  listings: RealEstateListing[]
  neighborhoods: RealEstateNeighborhood[]
}) {
  const syncUrl = ({ mode, location, price, minBeds }: RealEstateSearch) => {
    const params = new URLSearchParams({ mode })
    if (location) params.set("loc", location)
    if (price[0] != null) params.set("min", String(price[0]))
    if (price[1] != null) params.set("max", String(price[1]))
    if (minBeds > 0) params.set("beds", String(minBeds))
    window.history.replaceState(null, "", `?${params}`)
  }

  return (
    <RealEstatePage
      linkComponent={Link}
      listings={listings}
      neighborhoods={neighborhoods}
      phone="(718) 555-0100"
      mapBounds={{ north: 40.742, south: 40.66, west: -74.01, east: -73.918 }}
      onSearch={syncUrl}
      onFavoriteChange={(listing, favorite) => saveHome(listing.id, favorite)}
      onValuation={requestValuation}
    />
  )
}
```

Each listing is `{ id, mode, price, address, neighborhood, city, beds, baths, sqft, kind, color, lat, lng }` plus the optional `previousPrice`, `image`, `badge`, `href` and `favorite`. Without an `image` the card draws a tinted illustration of the `kind` (brownstone, condo, loft, house, townhouse), which is handy while you wait for photos. Every string is in `labels`.

Replace the sample content before launch. The stats under the search ("1,200+ homes sold since 2009"), the agents, the testimonials and the footer's broker line all belong to the fictional Harbor & Pine agency. Pass your own `stats` (or `stats={[]}` to hide them), `agents` and `testimonials`, and set `labels.brand` and `labels.legal`. Sample reviews and sales numbers must never reach a live site.

> Live demo (Real Estate Page): Type Williamsburg into the search, open its pin on the map and press Estimate payment: the calculator below takes that home's price. Then switch to Rent and watch the homes, pins and price choices change. Try it at [Real Estate Page](https://wingo-ui.com/templates/real-estate-page) and install it with `npx wingo-ui@latest add real-estate-page`.

## How should the buy or rent search work?

The Buy / Rent switch changes everything that follows it: the featured homes, the map pins, the neighborhood medians (sale price or rent) and the price choices. Switching resets the price filter on purpose, since a $1M to $2M band means nothing for rentals. The location field matches the address, the neighborhood and the city as you type, so "11215", "Union Street" and "Park Slope" all work, and the count above the homes is a polite live region, so screen readers hear "4 homes for sale" without stealing focus.

The filtering runs on the listings you pass, fine for the 10 to 50 homes an agency features. `onSearch` fires on Search, on Show homes in the phone filters sheet and on a neighborhood tile, with `{ mode, location, price: [min, max], minBeds }`. Use it to sync the URL or to send the visitor to a full results page. Only `mode` is controllable from outside, so for deep links that restore the whole search, build the results page from the Listing Grid.

## What goes on a property listing card?

Good property listing page design puts the decision facts on the card so people tap fewer homes they would reject: the price (and the old price after a cut), beds, baths, area, the neighborhood and one short badge such as "Open house Sat". The template's card does that, with a heart that rolls back if `onFavoriteChange` rejects.

On a results page with real photos, use the [Listing Card](https://wingo-ui.com/components/listing-card). It swipes through several photos on a phone (vertical scrolling still wins, and a swipe never opens the listing), shows the price per square foot under the amount, and has grid, list and compact layouts. A home maps onto its `ListingData` like this:

```tsx
import { ListingCard, type ListingData } from "@/components/ui/listing-card"

const home: ListingData = {
  id: "h-482",
  title: "3-bed brownstone duplex with garden",
  href: "/homes/h-482",
  images: [
    { src: "/homes/h-482/front.jpg", alt: "The brownstone from Union Street" },
    { src: "/homes/h-482/kitchen.jpg", alt: "The kitchen" },
  ],
  price: 1_895_000,
  compareAtPrice: 1_950_000,
  currency: "USD",
  unit: { quantity: 1_840, unit: "sq ft" },
  location: "Brooklyn, Park Slope",
  attributes: ["3 beds", "2 baths", "1,840 sq ft"],
  badges: [{ label: "Open house Sat", tone: "info" }],
}

export function HomeCard() {
  return <ListingCard listing={home} imageAspect="16/10" size="lg" />
}
```

For rentals, add `priceSuffix: "/mo"`. The `16/10` photo shape suits homes better than the default `4/3`.

> Live demo (Listing card): In the list layout the Park Slope apartment shows its price per square foot under the price. Swipe or drag its photos, then tap the heart to save it. Try it at [Listing card](https://wingo-ui.com/components/listing-card) and install it with `npx wingo-ui@latest add listing-card`.

The [Listing Grid](https://wingo-ui.com/components/listing-grid) wraps these cards for a results page: the count, a sort (a bottom sheet on phones), a grid / list switch it can remember per visitor, removable filter chips and loading more as you scroll.

## How does the price pin map work, and when do you need a real map?

The template's map band shows every matching home as a pin with its compact price ("$1.25M", "$4,200") next to a list of the same homes. A pin or a row opens a preview card with View home and Estimate payment; Escape or the close button closes it and focus goes back to the pin. The map is drawn, not tiled: pins are placed by `lat` and `lng` inside the `mapBounds` you pass. That means no API key, no tile bill and no map library on a landing page, the right trade for one city's featured homes. It will not show the streets around a home.

For the street view of one home, on a listing detail page, use the [Location Map](https://wingo-ui.com/components/location-map). It is Leaflet with OpenStreetMap tiles, loads only when it nears the viewport, and falls back to a drawn card without network:

```tsx
import { LocationMap } from "@/components/ui/location-map"

export function HomeLocation({ lat, lng }: { lat: number; lng: number }) {
  return <LocationMap value={{ lat, lng }} readOnly defaultZoom={15} height={280} locale="en-US" />
}
```

Three caveats from its docs. OpenStreetMap's public tiles are meant for light use, so point `tileUrl` at your own provider in production. `readOnly` hides the search, but where an agent places a home on the map (an add listing form), the default offline search only knows Romanian towns: pass `geocoder={nominatimGeocoder({ email, countryCodes: "us" })}` or your own geocoder. And the line under the map names the place from that same Romanian list, so a US home shows only its coordinates there unless you pass `reverseGeocoder={nominatimReverseGeocoder({ email })}`. It holds one pin, so for a results map with hundreds of homes and clustering, use MapLibre or Leaflet with a cluster plugin directly. The [Listing Detail Page](https://wingo-ui.com/components/listing-detail-page) block already puts a Location Map on the detail screen.

## What happens on a phone?

Below 768px the template changes shape instead of shrinking:

- The search becomes a stacked card: the Buy / Rent switch, the location field, then Filters and Search side by side. The price and beds selects are hidden.
- Filters opens a bottom sheet with the [Price Range](https://wingo-ui.com/components/price-range) (a histogram of the current homes over a two-thumb slider, Min and Max fields and preset chips) and a beds switch. Its button says "Show 4 homes" as you drag, so nobody applies a filter that returns nothing.
- The featured homes become a snap rail: each card is 82% of the screen wide, so the next one peeks in and shows there is more to swipe.
- In the map band, the list sits above the map, capped at 300px and scrolling on its own, with a 320px map under it.

That stacked map works for a dozen featured homes. On a results page with a long list it does not: the map ends up below a scroll the visitor never finishes. Show one view at a time instead, with a List / Map switch pinned near the thumb. Here is that pattern with the Listing Grid and the free [Segmented Control](https://wingo-ui.com/components/segmented-control); `ResultsMap` is your own map:

```tsx
"use client"

import { useState } from "react"
import { List, Map as MapIcon } from "lucide-react"
import { ListingGrid } from "@/components/ui/listing-grid"
import type { ListingData } from "@/components/ui/listing-card"
import { SegmentedControl } from "@/components/ui/segmented-control"
import { ResultsMap } from "./results-map"

export function Results({ homes, total }: { homes: ListingData[]; total: number }) {
  const [view, setView] = useState("list")
  return (
    <div className="md:grid md:grid-cols-2 md:gap-6">
      <div className={view === "map" ? "max-md:hidden" : undefined}>
        <ListingGrid items={homes} total={total} defaultView="list" imageAspect="16/10" columns={{ base: 1, xl: 2 }} />
      </div>
      <div className={view === "list" ? "max-md:hidden" : "max-md:h-[calc(100dvh-8rem)]"}>
        <ResultsMap homes={homes} />
      </div>
      <div className="fixed inset-x-0 bottom-[max(1rem,env(safe-area-inset-bottom))] z-sticky flex justify-center md:hidden">
        <SegmentedControl
          aria-label="Show results as"
          value={view}
          onValueChange={setView}
          options={[
            { value: "list", label: "List", icon: <List /> },
            { value: "map", label: "Map", icon: <MapIcon /> },
          ]}
          className="shadow-floating"
        />
      </div>
    </div>
  )
}
```

Both views stay mounted, so the map keeps its position and the list keeps its scroll when the visitor switches back. One catch: a map library measures its container, and a container hidden with `display: none` measures zero, so call `map.resize()` (MapLibre) or `map.invalidateSize()` (Leaflet) inside `ResultsMap` when the map view opens. From 768px the toggle disappears and the list and map sit side by side.

## How does the mortgage estimate calculate the payment?

The calculator uses the standard amortization formula for principal and interest, then adds property tax (a yearly percent of the price), insurance and HOA fees per month. With the defaults (a $1,250,000 home, 20% down, 6.5% over 30 years, 0.9% tax and $150 insurance), the loan is $1,000,000, principal and interest come to $6,321, tax adds $938 and the estimate is $7,408 a month. The breakdown bar and totals update as the sliders move (arrow keys and Page Up / Down work too).

Set your market's averages through the `mortgage` prop, for example `mortgage={{ taxRate: 1.1, insurance: 120, hoa: 350 }}`; anything you leave out keeps its default. Estimate payment on a map preview fills the calculator with that home's price and scrolls to it. Keep the note that it is an estimate for planning only, and keep the wording away from anything that reads like a loan offer.

## How do you wire the valuation form?

On a real estate landing page template, the valuation band is the lead capture for sellers. The form checks the address, the name and the email, focuses the first problem, and calls `onValuation` with `{ address, propertyType, beds, timeline, name, email, phone }`. A returned promise keeps the button spinning; a rejection keeps what was typed and shows `labels.failed`. A server action fits:

```ts
// app/actions.ts
"use server"

import type { RealEstateValuationValues } from "@/components/templates/real-estate-page"
import { db } from "@/lib/db"

export async function requestValuation(values: RealEstateValuationValues) {
  if (!values.address?.trim() || !values.name?.trim() || !values.email?.includes("@")) {
    throw new Error("invalid request")
  }
  await db.valuationRequest.create({ data: { ...values, createdAt: new Date() } })
}

export async function saveHome(listingId: string, favorite: boolean) {
  // check the session here; a throw rolls the heart back
  await db.savedHome.upsert({ listingId, favorite })
}
```

`db` stands for your own data layer. Validate again on the server, as here: a server action is a public endpoint, and the TypeScript types do not exist at runtime. Without `onValuation` the form pretends for 900 ms and confirms, which is fine for a preview and wrong for production.

## Which parts are free and which are Pro?

The Real Estate Page, the Listing Card, the Listing Grid, the Location Map and the Price Range are Pro items. The Segmented Control is free, along with 74 other items of the 369 in the catalog. Pro is $8 a month, $80 a year or $150 once on the [pricing page](https://wingo-ui.com/pricing), and the 14-day refund applies while no Pro item has been downloaded. On a nextjs real estate website the template is usually the first screen, followed by a Listing Grid results page and a detail page. If your site is closer to a classifieds market, the [React marketplace template tutorial](https://wingo-ui.com/blog/react-marketplace-template) builds those screens around a search URL.

More full screens are in the [Build it category](https://wingo-ui.com/blog/category/build), and the [Next.js admin dashboard tutorial](https://wingo-ui.com/blog/nextjs-admin-dashboard-tutorial) covers the back office where agents would manage these listings.

## Try it before you install it

Open the [Real Estate Page template](https://wingo-ui.com/templates/real-estate-page) on your phone, switch to Rent, open Filters and drag the price, then tap a pin. If it fits your real estate website template react project, `npx wingo-ui@latest add real-estate-page` copies it into your app with a Pro account, ready for your listings, your bounds and your valuation action.

## Components in this post

- [Real Estate Page](https://wingo-ui.com/templates/real-estate-page) (Pro): A React real estate template: buy or rent search, listing cards, a price pin map, neighborhoods, a mortgage estimate and a valuation form. Install: `npx wingo-ui@latest add real-estate-page`
- [Listing card](https://wingo-ui.com/components/listing-card) (Pro): A React listing card for marketplaces: swipeable photos, price, place and time, badges and a heart, in grid, list and compact layouts. Install: `npx wingo-ui@latest add listing-card`
- [Listing grid](https://wingo-ui.com/components/listing-grid) (Pro): A React listing grid for marketplace results: count, sort, view switch and filter chips over listing cards that load more as you scroll. Install: `npx wingo-ui@latest add listing-grid`
- [Location Map](https://wingo-ui.com/components/location-map) (Pro): A React location map to pick or show a place: a draggable pin, an optional radius with a towns count, offline search and a static fallback. Install: `npx wingo-ui@latest add location-map`
- [Price range](https://wingo-ui.com/components/price-range) (Pro): A React price range filter: a histogram of the results over a two-thumb slider, synced min and max inputs, preset chips and a live count. Install: `npx wingo-ui@latest add price-range`

## FAQ

### Is there a free Next.js real estate template?

The two polished ones we checked on 2026-11-06 are paid: Homely costs $29 for a single use license on WrapPixel, and Sanity lists the Clay template as a paid template. The Wingo UI Real Estate Page is a Pro item, included in the $8 a month plan.

### What pages does a real estate website need?

A home page with a buy or rent search, a results page with filters and a map, a listing detail page with photos, facts and the location, and a way for owners to request a valuation. Agent profiles, neighborhood guides and a mortgage estimate are what make buyers stay.

### How should a property search map work on mobile?

Show either the list or the map, not both, and switch with a toggle that stays in reach of the thumb. Pins should show the price, and tapping one should open a short preview instead of leaving the results.

### How is a monthly mortgage payment calculated?

Principal and interest use the standard amortization formula: loan times the monthly rate, divided by one minus (1 + monthly rate) to the power of minus the number of months. Property tax, insurance and HOA fees are then added per month.

### Does the template need a Google Maps or Mapbox key?

No. The template draws its own map and places the pins from latitude and longitude inside the bounds you pass, so there is no key and no tile bill. The separate Location Map component uses Leaflet with OpenStreetMap tiles by default, and you point it at your own tile provider in production.

---

Source: https://wingo-ui.com/blog/nextjs-real-estate-template
