Blog//7 min read
Real estate website template for Next.js: blocks, not a theme
A real estate website template for Next.js built from ten animated blocks: hero search, listing grid, valuation funnel and mortgage calculator.
MSMarco Schulz, founder of Facade UI
When an agent or a small brokerage asks me for a website, the first thing they send is a link to a theme they found on a marketplace. It looks fine in the demo. Six months after launch it looks like every other brokerage in the state, the listing grid needs a plugin update, and nobody remembers how the contact form sends email. This article is the plan I use instead: a Next.js site assembled from blocks you own, in the order a buyer or seller actually reads a page.
Why themes age badly
A theme is one decision made for you, forever. Layout, typography, animation library, form handler and color system arrive as a single bundle. The day you want to change one of them you discover they are all tied together.
Three problems show up on the themed real estate sites I get asked to fix:
- The listing data is shaped for the theme's demo, not for the MLS feed or the spreadsheet the agent actually has. Mapping one to the other becomes a permanent chore.
- The animations are decorative. Things fade in, but nothing on the page responds to the visitor. A buyer who wants to filter by price gets a page reload.
- Lead capture is a generic contact form. Sellers who want to know what their home is worth are the most valuable visitors on the site, and the form treats them like someone asking about parking.
Blocks solve this because each block is a single TypeScript file in your repo. You install the ones you need, pass your data as props, and edit the source when a client wants something specific. There is no theme to update.
The 10 block page plan
This is the block order for an agent site. It is the same order I use on the real estate agent page, and each block links to its component docs.
- Hero, real estate: a full width hero with a Buy, Rent, Sell search panel and animated stats. The search mode changes the labels, so "Location" becomes "Your address" when a visitor switches to Sell and the button turns into "Get valuation".
- Agent card: the agent's name, rating, license and languages, with call, email and tour actions. On an agent site this belongs above the fold on mobile.
- Property listing grid: filterable and sortable listings with saved homes and a Load more button. The grid re-layouts with a spring when a filter changes instead of blinking.
- Stats band: four animated numbers. Homes sold, average days on market, years in business, neighborhoods covered. Use real numbers from the agent or leave the block out.
- Testimonials wall: reviews with names and neighborhoods.
- Home valuation funnel: the seller lead magnet. Five steps, address first, estimate range at the end.
- Mortgage calculator: the buyer tool. I covered the formula and the install in a separate article.
- FAQ section with FAQPage JSON-LD.
- Contact section with a form, hours and a map card.
- Big footer with NAP details, hours and the brokerage license line.
Ten blocks is the whole site. A neighborhood page reuses blocks 3, 4 and 8 with different data.
The data shape for listings
Before you touch the hero, decide what a listing looks like in code. The listing grid takes a PropertyListing[] array, and every other block that shows property data can be fed from the same records.
import type { PropertyListing } from "@/components/facade/property-listing-grid";
export const listings: PropertyListing[] = [
{
id: "mls-4108",
type: "house",
title: "Modern farmhouse with pool",
address: "4108 Avenue G",
city: "Hyde Park, Austin",
price: 1_285_000,
beds: 4,
baths: 3.5,
sqft: 3120,
daysOnMarket: 2,
status: "New",
href: "/listings/mls-4108",
imageSrc: "/photos/mls-4108/cover.jpg",
},
];Two decisions in that shape matter later. type is a union of "house" | "condo" | "townhome" because the filter chips in the grid are built from it. status is optional and limited to "New" | "Open house" | "Price drop" | "Pending", because a badge vocabulary that grows without limit ends up with twelve colors nobody can read.
Where does the array come from? For a solo agent with twenty listings, a JSON file in the repo, updated through a CMS or a spreadsheet export, costs nothing. For a brokerage with an MLS feed, a server component fetches the feed, maps it to PropertyListing once, and passes the array down. The grid does not care which.
Rendering the grid is one component:
import { PropertyListingGrid } from "@/components/facade/property-listing-grid";
import { listings } from "@/data/listings";
export function Listings() {
return (
<PropertyListingGrid
eyebrow="Featured homes"
heading="Homes on the market this week"
listings={listings}
pageSize={6}
defaultSort="newest"
onToggleSave={(id, saved) => console.log(id, saved)}
/>
);
}Saved homes live in component state. To survive a reload, write to localStorage in onToggleSave; restoring the set on load is a small edit to the installed source.
The lead capture flow
A real estate site has two visitors with two different questions. Buyers ask "Can I afford this?" and sellers ask "What is my home worth?" Each gets its own block, and the two blocks hand leads to the same place.
The valuation funnel asks for the address first. That ordering is deliberate: an address is low commitment and feels like the start of a tool, not the start of a form. Property type, size and condition follow, and the contact details come last, after the visitor has invested a minute. The built-in estimator multiplies square footage by a basePricePerSqft you set for the market, adjusts for condition, and returns a low, mid and high figure. It is clearly labeled as an estimate, not an appraisal, and the estimate prop lets you swap in a real valuation API later without touching the UI.
import { ValuationFunnel } from "@/components/facade/valuation-funnel";
<ValuationFunnel
marketLabel="Austin, TX"
basePricePerSqft={312}
onLead={(data, estimate) => fetch("/api/leads", { method: "POST", body: JSON.stringify({ data, estimate }) })}
bookLabel="Book a walkthrough"
bookHref="/book"
/>onLead is where the site earns money. I post to a route handler that writes the lead to the agent's CRM and sends a notification. The mortgage calculator's onChange callback is the buyer side of the same idea: when a buyer settles on a payment, the pre-approval button next to the donut already knows the numbers.
Install and deploy on Vercel
All four featured blocks are Pro items. Set up the private registry once with your license key (the one time setup is in the installation docs), then install the page in a single command:
npx shadcn@latest add @facade/hero-real-estate @facade/property-listing-grid @facade/valuation-funnel @facade/mortgage-calculatorThe free blocks on the list, agent card, stats band, FAQ section and the footer, install from the public registry URL, for example npx shadcn@latest add https://facadeui.com/r/agent-card.json. Every file lands in components/facade/, depends only on motion and lucide-react, and uses the semantic Tailwind tokens your shadcn setup already defines. Change --brand in globals.css and the whole site re-themes.
Deploying is vercel from the project root or a Git push. Because listings can render on the server, the listing pages are static HTML with the interactive grid hydrating on top. That is what keeps Largest Contentful Paint inside Google's 2.5 second "good" threshold on a phone, which a theme loading several jQuery plugins rarely manages.
FAQ
Is this a template I can download as a zip?
No. It is an ordered set of blocks you install into your own Next.js project with the shadcn CLI. You get the same result as a template, a complete page, but every section is a file you own and can edit. The order and the data shapes are documented on the industry page.
Can I connect an IDX or MLS feed?
Yes. Fetch the feed in a server component, map each record to the PropertyListing type and pass the array to the grid. The grid has no opinion about where the data comes from. IDX compliance rules about attribution and refresh intervals are between you and your MLS.
Does it work with the Pages Router or only the App Router?
The blocks are plain React client components. They work in the App Router, the Pages Router, Vite and Remix, as long as Tailwind v4 and the shadcn tokens are set up. The install docs use the App Router because that is what new Next.js projects start with.
Components in this article
- Hero, real estateProBright brokerage hero with a crisp house render, Buy, Rent, Sell search panel and animated proof numbers.
- Property listing gridProFilterable, sortable listings grid with animated re-layout, saved homes and load more pagination.
- Home valuation funnelProMulti-step "What's my home worth?" lead funnel with direction-aware steps and an animated estimate range.
- Mortgage calculatorProMonthly payment calculator with down payment slider, loan term toggle and an animated donut breakdown.