build69
build69  /  Components  /  Marketing Sections  /  PropertySearchHero

PropertySearchHero

v1.0.4Updated 19 Oct 2026

The front door of a property portal (fb_97d176de): a headline over a search FORM whose first control is the kind of home as a keyboard-operable tab row, then a buy-or-rent switch, a text box, and a row of shortcut chips each carrying a LIVE count the host supplies. It holds no results — it builds a query and hands it over, so a home page renders it before a single listing has been fetched (PropertyMapSearch and the PropertySearch page are the results surface). It is a real <form> with named fields, so the search works with JavaScript off; onSearch upgrades that to client navigation. The counts are never invented: a chip with no figure prints none, a figure still in flight shows a placeholder, and a failed count fetch drops every number and offers a retry.

Preview

Live · 14 stories
DefaultOpen ↗
▸Why this is recommendedheropropertyreal-estatesearch+11

The default, out-of-the-box PropertySearchHero.

Recommended for+14
  • ✓A property portal home page with a search box for condo / house / land and buy or rent
  • ✓A hero with a search form instead of a headline and a CTA
  • ✓Shortcut chips that each show how many results they currently find
  • ✓A rental or classifieds front page where the visitor picks a category then types a place
  • ✓A search band that has to work from a server-rendered page with no JavaScript
Tags+6
heropropertyreal-estatesearchcondohouselandbuyrentchipscountslandingportalsectiondefault
Features+4
  • Kind of home as a real tablist over ONE shared panel: arrows, Home and End move between condo / house / land, moving also chooses, and the panel is never remounted so the caret in the search box survives every switch (which is why it is not built on Tabs — three panels around one text box would remount the input)
  • Buy-or-rent as a radiogroup with the same arrow contract and one tab stop
  • Shortcut chips with LIVE counts from the host: a count of 0 is printed because it is a true answer, a missing count prints nothing, countsState loading shows a placeholder per chip, and countsState error drops every number, says so once and offers a retry — there is no state where a stale figure sits beside a broken refresh
  • The count is spelled into the chip aria-label (the visible number is aria-hidden), so every browser announces the same sentence instead of stitching two inline spans
  • Works with JavaScript off: the tabs and the switch are buttons that never submit, their values ride hidden fields, and action / method / fieldNames point the GET at the portal own route
  • Per-kind and per-deal counts on the tabs and the switch, under the same honesty rules as the chips
  • A disabled option (a market not open yet) is visible, skipped by the arrow keys, and can never end up selected — and a stale kind in a bookmarked URL is corrected to a real one instead of leaving a tablist with nothing chosen
  • Consumer safety: a chip href passes an allow-list before it becomes an anchor (relative or http(s) only by default, so a javascript: or data: address a seller typed is refused and the chip stays a working button), and every label is rendered as text with invisible and direction-reordering characters stripped
  • Slots for the rest of the band: aside for the strip of live figures (StatCard), background for an image or gradient in an aria-hidden layer, footer for extra links, eyebrow as a node or false
  • Controlled or uncontrolled query ({ kind, deal, query }), searching state on the button, disabled for the whole section, size sm / md / lg, heading level 1-3
  • i18n labels prop (PropertySearchHeroLabels) with English defaults, covering the three built-in kinds and the two deals so a portal that passes no option data still translates; counts format through the locale provider
  • heroValueToSearchParams turns the query into the parts of a URL that are actually set, and the same normalisation is importable from lib/property-search-hero so a server route can CALL it
Not recommended for−2
  • ✕Showing results: this section has no list and no map (use PropertyMapSearch or the PropertySearch page)
  • ✕Filtering rows already on screen (use FilterBar or PropertyMapSearch filters)
  • ✕A SaaS marketing hero with a headline and a CTA (use Hero)
  • ✕Inventing the counts: the figures come from the host, and there is no demo data behind them
  • ✕A full filter panel with price ranges and bedrooms (that belongs on the results screen)

Install

Ask your agent, or call the MCP tool directly. The resolved bundle is written into your project.

get_registry_item({ name: "PropertySearchHero" })

Usage

import { PropertySearchHero } from '@/components/sections/property-search-hero';

// The counts come from the host — the section never invents one.
<PropertySearchHero
  chips={[
    { id: 'bts', label: 'Near the BTS', count: counts.bts, value: { kind: 'condo' }, href: '/search?near=bts' },
    { id: 'under-3m', label: 'Under 3M baht', count: counts.cheap, href: '/search?max=3000000' },
  ]}
  countsState={countsQuery.status}
  onCountsRetry={countsQuery.refetch}
  onSearch={(q) => router.push('/search?' + new URLSearchParams(heroValueToSearchParams(q)))}
/>;

Tick variants in the left menu — each adds a tab with that variant’s real story source, the same code the preview above renders.

Props

PropTypeRequired
kinds

Kinds of home in tab order: { value, label?, count?, disabled? }. Default condo / house / land, worded by the labels bag so they translate without passing data. A value the catalogue does not know is labelled by its own value, never by another kind word

PropertySearchHeroOption[]—
deals

Buy / rent (or lease, or auction), same shape. Default buy / rent

PropertySearchHeroOption[]—
value

Controlled query { kind, deal, query }. A kind the portal no longer lists is corrected on the way out, so the tab row stays valid

PropertySearchHeroValue—
defaultValue

Uncontrolled initial query. Anything missing falls to the first choosable option

Partial<PropertySearchHeroValue>—
onValueChange

Fires on every change: a tab, the switch, a keystroke, a chip — with the whole query, ready for the URL

(value: PropertySearchHeroValue) => void—
onSearch

The visitor pressed Search or Enter. The native submit is prevented, so this is client navigation. Without it the form submits for real

(value: PropertySearchHeroValue) => void—
chips

Shortcut chips: { id, label, count?, href?, value?, description?, disabled? }. count is a LIVE figure from the host; value is the part of the query the chip stands for, merged over the current one

PropertySearchHeroChip[]—
onChipSelect

A chip was used. Fires alongside the chip own href, with the resulting query

(chip: PropertySearchHeroChip, value: PropertySearchHeroValue) => void—
countsState

How the live counts are doing. Default ready. loading shows a placeholder per chip; error prints no number at all and explains itself

'ready' | 'loading' | 'error'—
onCountsRetry

Retry the counts. With none, a failed count fetch is still explained, without a dead button

() => void—
action

Where a no-JavaScript submit goes. With none, the current URL — which is what a home page that reads its own query string wants

string—
method

Submit method for that path. Default get

'get' | 'post'—
fieldNames

Rename the submitted fields. Defaults kind, deal and q

PropertySearchHeroFieldNames—
searching

The search is running — the button shows its busy state instead of looking like a dead press

boolean—
disabled

Every control inert, chips included

boolean—
title

Replace the headline. Defaults to the title label

React.ReactNode—
subtitle

Replace the supporting line

React.ReactNode—
eyebrow

Small line above the headline (a Badge works well). false hides it

React.ReactNode | false—
headingLevel

Heading level the title renders as. Default 1 — a hero usually owns the page, but not always

1 | 2 | 3—
background

A real element behind the content (image, gradient, video), rendered in an aria-hidden layer. A node, not a URL, so the host keeps control of loading and its own content policy

React.ReactNode—
aside

A panel beside the form on wide screens — the strip of live figures a portal puts here, built from StatCard

React.ReactNode—
footer

Extra nodes under the chips (a link to the map search, a phone number)

React.ReactNode—
size

Vertical density and headline scale. Default md

'sm' | 'md' | 'lg'—
hrefAllowed

May a chip link there? Default isSafePropertyHref: relative or http(s) only. Widen it deliberately for an app scheme

(href: string) => boolean—
formatCount

Format a live count. Default: through the locale provider

(count: number) => string—
labels

Override every built-in string and aria template (i18n). Merged over DEFAULT_PROPERTY_SEARCH_HERO_LABELS; the defaults are English

Partial<PropertySearchHeroLabels>—

Related