Property Card

Pro

Real-estate listing card — pageable photo strip with the real gallery count, status ribbon, price with derived price-per-area, a beds/baths/parking/area row that keeps zero and unknown apart, amenity chips, save and compare toggles and an agent line. Grid, list and wide layouts.

</>sv-property-card propertycardreal-estatelistingestate

Complexity tier: Primitive Presents a value: held to semantic markup and contrast, with its right-to-left and print gaps recorded and the right-to-left debt allowed only to shrink.

import { SvPropertyCardComponent } from '@styloviz-pro/commerce';

Demo & usage

Basic usage

Pro

Usage example — open the gallery for the full interactive demo.

Pro

An interactive demo for this component lives in the gallery.

Open live demo

More examples

A rental

Pro

A studio, and a plot with no rooms at all

Pro

`0` renders as "Studio"; `null` renders nothing. Never conflate them.

Map-list row

Pro

Sold — the actions are removed, not disabled

Pro

When to use

  • Property search results, saved shortlists, agent portfolios and "similar homes" rails.
  • Anywhere the price needs a per-unit-area figure beside it to be comparable.
  • For a generic product with a rating and an add-to-cart, use the Product Card instead.

Async states

None — this component has nothing to load, so it has no loading, empty or error state to drive.

Inputs

NameTypeDefaultDescription
titlerequiredstring—Headline — usually the street address.
locationstring''Secondary line — neighbourhood, city, postcode.
hrefstring''Link target. Renders the title as a stretched link when set.
pricerequirednumber—Asking price or rent, in `currency`.
compareAtnumber | nullnullPrevious asking price, to show a reduction.
currencystring'USD'ISO 4217 currency code.
localestring'en-US'BCP-47 locale.
minorUnitsbooleanfalseTreat prices as minor units.
pricePeriodPropertyPricePeriod''Rent period. `''` (the default) means this is a sale price.
fractionDigitsnumber | null0Decimal places on the price. `null` uses the currency's own convention. Defaults to `0` here, unlike `sv-price`, which defaults to the currency convention. Property prices are not quoted to the cent in any market: `$1,180,000.00` reads as a spreadsheet export rather than an asking price, and the two trailing zeroes are the widest thing on the card. Pass `null` to get the currency default back.
bedsnumber | nullnullBedrooms. `0` is a studio; `null` means nobody said.
bathsnumber | nullnullBathrooms. `null` means nobody said.
parkingnumber | nullnullParking spaces. `0` renders as "No parking"; `null` renders nothing.
areanumber | nullnullInternal floor area in `areaUnit`. `null` means nobody measured.
lotAreanumber | nullnullPlot area in the same unit. `null` means there is none, or none recorded. It earns its place on a card that already shows floor area because of land: a building plot has no internal area at all, so without this input the card for one renders a price and an empty stat row. It is also the figure that separates two otherwise identical houses. Shown after the floor area, never instead of it — they are different measurements and a card that silently substituted one for the other would make a 1,200 sq ft house on a 9,000 sq ft plot look enormous.
areaUnitPropertyAreaUnit'sqft'The unit `area` and `lotArea` are GIVEN in.
displayAreaUnitPropertyAreaUnit | nullnullThe unit to DISPLAY in. Converts; it does not merely relabel. RELABELLING WITHOUT CONVERTING IS THE WHOLE DEFECT. `areaUnit` says what the number IS, and a card that swapped the suffix would turn 1,800 sq ft into "1,800 m²" — a house four times its real size, stated with total confidence. Leave this `null` to display in the unit the data arrived in.
updatedAtstring''When this listing was last confirmed with its source, as `YYYY-MM-DD` or an ISO timestamp. A SOLD LISTING SHOWING AS AVAILABLE IS THE DEFINING FAILURE OF A PROPERTY CARD, and a status badge alone cannot prevent it: the badge is only ever as true as the last sync, and it never says when that was. A reader who can see the date can judge the badge; one who cannot has to trust it absolutely.
staleAfterDaysnumber30How many days before a listing is called out as possibly stale.
sourcestring''Where this listing came from — the portal, board or agency. In several markets naming the source is a LEGAL requirement rather than a courtesy, and a card with nowhere to put it cannot be shipped there at all. Rendered with the disclaimer beneath the card.
sourceUrlstring''Optional link to the listing at its source.
disclaimerstring''Required legal text, rendered verbatim beneath the card.
featuresreadonly PropertyFeature[][]Amenity chips under the stat row.
featureLimitnumber3How many chips to show before "+n more".
mediareadonly PropertyMedia[][]The photos this card can actually page through.
mediaCountnumber0The true size of the gallery, when `media` is a trimmed subset. `0` (the default) means `media` is all of it.
hasTourbooleanfalseShow the 360°-tour marker.
tourLabelstring'360° tour'Wording for the tour marker.
statusPropertyStatus'for-sale'Lifecycle state.
statusLabelstring''Override the status wording, e.g. a localised label.
variantPropertyCardVariant'grid'Layout.
listedLabelstring''Small line under the actions — "Listed 3 days ago".
agentNamestring''Agent or agency name.
agentAvatarstring''Agent avatar URL.
hoverablebooleantrueLift the card on hover.
showSavebooleantrueShow the save (heart) toggle.
showComparebooleantrueShow the compare toggle.
tourInteractivebooleanfalseMake the 360° tour marker a button rather than a label. Off by default, because a control that refuses every click is worse than a plain badge. Turn it on and bind `(tourOpen)`; the card then stops the click reaching the stretched card link, so opening the tour does not also navigate to the property.
linkMode'browser' | 'host''browser'Who handles a plain left-click on the card when `href` is set. `'browser'` follows the link, which is a full page load. `'host'` cancels the plain left-click and emits `open` instead, leaving every modified click and every middle-click to the browser — which is what a router-driven app wants: client-side navigation on a normal click, a real new tab on ⌘-click. Bind `(open)` when using `'host'`, or the card does nothing.
compareDisabledbooleanfalseThe comparison is full, so this card cannot be added to it. A TRAY WITH A LIMIT NEEDS THIS, AND EVERY TRAY HAS A LIMIT. Without it the host's only move is to refuse the click after the fact: the card optimistically flips `compared`, the host's binding flips it back on the next change-detection pass, and the reader sees the icon blink and nothing else. Told in advance, the button is properly disabled, says why on hover, and is skipped by the keyboard — which is the difference between a limit and a bug. A card ALREADY in the tray stays enabled whatever this says, because removing is how the reader makes room.
compareFullHintstring'The comparison is full'Tooltip on the disabled compare button.
savedboolean (two-way)falseSaved to a shortlist (two-way: `[(saved)]`).
comparedboolean (two-way)falseAdded to the compare tray (two-way: `[(compared)]`).

Outputs

EventPayload typeDescription
openvoidThe card body was activated. Fired instead of navigating when `href` is empty. There is deliberately no `savedChange` / `comparedChange` output declared here: `model()` already emits one per two-way input, and declaring a second output with the same job is how a host ends up subscribed to one of them and puzzled by the other.
contactAgentvoidThe agent line was activated.
tourOpenvoidThe 360° tour marker was activated. Only reachable when `tourInteractive` is set — see the note beside that input.
mediaChangenumberThe visible photo changed, with its zero-based index.
savedChangebooleanEmits the new `saved` whenever it changes — the output half of `[(saved)]`.
comparedChangebooleanEmits the new `compared` whenever it changes — the output half of `[(compared)]`.

Customization & theming

  • Grid, list and wide layouts.
  • Seven lifecycle statuses with their own ribbon colours, overridable via `statusLabel`.
  • Amenity chips capped by `featureLimit` with a "+n more" remainder.
  • Optional 360°-tour marker, agent line and "listed" label.

Best practices

  • Do: Pass `null` for a figure nobody recorded and `0` for a figure that is genuinely zero — a studio is 0 bedrooms, a building plot has none.
  • Do: Set `mediaCount` when the feed sends a trimmed photo subset, so the counter states the real gallery size.
  • Do: Leave `fractionDigits` at 0 — property prices are never quoted to the cent.
  • Don't: Do not coerce a missing area to zero to get a per-area figure; the card omits the line for a reason.
  • Tip: A `sold` or `let` status removes the save and compare controls rather than disabling them.

Accessibility

  • Photo paging exposes labelled previous/next buttons and a polite live region announcing "Photo 2 of 24" — the visible counter sits inside an aria-hidden cluster.
  • The stat row is a `<dl>` whose `<dt>` carries the written label ("3 bedrooms"), so the figures mean something without the icons.
  • Save and compare are toggle buttons with `aria-pressed` and labels naming the property.
  • Alt text is required on every photo rather than optional.

Feedback on Property Card

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.