Listing card
Props, states, and styling for listing-card-01. Access is free and the license is MIT.
listing-card-01 is an editorial real-estate listing card. Every visible value comes from props. The card does not fetch data. Catalog access is free. The published component is MIT, Copyright (c) 2026 ShadcnEstate.
Live preview
The live preview is the marketing route /blocks/listing-card-01.
Installation
pnpm dlx shadcn@latest add shadcnestate/registry/listing-card-01The component declares no npm dependencies and no registry dependencies. The host must provide React >=18.2 <20 and Tailwind CSS >=4. It is a server-capable function component and has no client directive.
Tailwind CSS 4 generates utilities only from scanned sources. If the component directory is outside that scan, point @source at it from the host stylesheet. The path below is a host path, not an install location. The component does not ship its own CSS file.
@source "../components/listing-card-01";Usage
Import ListingCard from the local component entry. The specifier below is that local entry, not a published package name.
import { ListingCard } from "./listing-card-01";
export function ListingPreview() {
return (
<ListingCard
title="Courtyard loft"
image={{
src: "/photos/courtyard.jpg",
alt: "Courtyard",
width: 800,
height: 600,
}}
location={{ formattedLocation: "Mitte, Berlin" }}
price={{
amount: "1999.50",
currency: "EUR",
locale: "de-DE",
period: "month",
}}
labels={{ periods: { month: "per month" } }}
propertyType={{ label: "Apartment", code: "apartment" }}
features={[
{ id: "rooms", label: "Rooms", value: "3" },
{ id: "area", label: "Living area", value: "92 m²" },
]}
href="/listings/courtyard-loft"
/>
);
}Omitting state selects the ready variant, and omitting headingLevel renders an h2. The price line is the runtime currency format of 1999.50 EUR for de-DE, followed by per month. The href wraps the whole card because no cta is set.
A loading card accepts only shared props:
<ListingCard state="loading" labels={{ loading: "Loading listing" }} />Props
ListingCardProps is ListingCardReadyProps when state is omitted or "ready", and ListingCardLoadingProps when state is "loading".
Shared
| Prop | Type | Required | Behavior |
|---|---|---|---|
className | string | No | Appended to the article. Conflicting Tailwind utilities are not merged. |
labels | ListingCardLabels | No | Localized fallback, loading, and period text. |
| Field | Type | Required | Behavior |
|---|---|---|---|
imageFallback | string | No | Text in the image fallback. Blank text hides that area from assistive tech. |
priceFallback | string | No | Shown when price is missing or cannot be formatted, and when no priceLabel applies. |
loading | string | No | Accessible name for the loading article, after trimming. |
periods | Partial<Record<ListingPricePeriod, string>> | No | Non-blank text appended after a formatted amount. A period adds no text by itself. |
Ready
Empty or whitespace-only titles throw ListingCard requires a non-empty title.
| Prop | Type | Required | Behavior |
|---|---|---|---|
state | "ready" | No | Default variant. |
title | string | Yes | Visible heading. Trimmed before render. |
headingLevel | 2 | 3 | 4 | No | Heading element. Defaults to 2. |
image | ListingCardImage | No | Native image, or the local fallback. |
location | ListingCardLocation | No | One location line, or nothing. |
price | ListingCardPrice | No | Formatted price, priceLabel, fallback, or nothing. Does not throw. |
propertyType | ListingCardPropertyType | No | Localized type label above the title. A blank label hides it. |
features | readonly ListingCardFeature[] | No | First four entries only, after id checks on the full list. |
href | string | No | Makes the whole card one link when cta is absent and the URL is safe. |
cta | ListingCardCta | No | One labeled link inside the article. Wins over href. |
cta wins even when its URL is unsafe or its label is blank. A rejected call to action does not fall back to href. With neither a usable call to action nor a safe href, the card is not a link. A safe href without cta wraps the image and the details; that link has no separate label.
Safe targets are http, https, and relative URLs that pass the card's checks. Unsafe schemes, protocol-relative URLs, backslashes, and values that contain control characters or whitespace are not rendered. Encoded spaces in an otherwise safe URL are preserved. The same check applies to image src.
ListingCardImage
| Field | Type | Required | Behavior |
|---|---|---|---|
src | string | Yes | Used only when it is non-blank after trimming and passes the link safety check. |
alt | string | Yes | Trimmed. Pass an empty string when the image is decorative. |
width | number | No | Emitted only for a positive integer. An invalid value is omitted and the image still renders. |
height | number | No | Emitted only for a positive integer. An invalid value is omitted and the image still renders. |
ListingCardLocation
| Field | Type | Required | Behavior |
|---|---|---|---|
formattedLocation | string | No | Wins when it contains non-whitespace text. |
line1 | string | No | First structured part. |
locality | string | No | Joined with postalCode by a space. |
region | string | No | Included after the postal locality. |
postalCode | string | No | Joined with locality by a space. |
countryCode | string | No | Last structured part. |
Structured parts are joined in that order with a comma and a space. An empty location is omitted.
ListingCardPrice
| Field | Type | Required | Behavior |
|---|---|---|---|
amount | string | Yes | Major units: 0 or a number without a leading zero, optionally a decimal point and digits. No sign and no thousands separators. Example: 1999.50. |
currency | string | Yes | ISO-4217 alphabetic code. Trimmed and uppercased; must be three letters. |
period | "sale" | "month" | "week" | "night" | "year" | No | Selects caller-supplied period text. It does not add text by itself. |
locale | string | No | BCP-47 locale for Intl.NumberFormat. An invalid locale does not use the runtime default. Omitting it does, and is not stable across machines. |
priceLabel | string | No | Replaces the formatted amount and any period label. |
Invalid or missing prices show priceFallback when that text is non-blank, and otherwise show nothing.
ListingCardPropertyType
| Field | Type | Required | Behavior |
|---|---|---|---|
label | string | Yes | Already localized. A blank label hides the type. |
code | "apartment" | "house" | "land" | "commercial" | "other" | No | Stored on data-property-type when the label is visible. |
ListingCardFeature
| Field | Type | Required | Behavior |
|---|---|---|---|
id | string | Yes | Unique and non-empty after trimming. Checked for every entry, including entries that are not shown. |
label | string | Yes | Already localized. Units are the caller's responsibility. |
value | string | No | Shown after the label when both are non-blank. |
A blank id throws ListingCard features require a non-empty id. A duplicate id throws ListingCard features contain a duplicate id " followed by the trimmed id and a closing quote. The card then renders at most the first four entries. An entry with a blank label and no value is skipped, and later entries do not fill that gap.
ListingCardCta
| Field | Type | Required | Behavior |
|---|---|---|---|
href | string | Yes | Link target. An unsafe target removes the link. |
label | string | Yes | Visible link text. Blank text removes the link. |
Loading
state must be "loading". This variant accepts only className and labels. It does not accept listing content, href, or cta. The article has aria-busy="true". A non-blank labels.loading is its accessible name. The skeleton is decorative, pulses, and disables that animation under prefers-reduced-motion. It contains no heading, image, price, feature list, or link.
Customization
Color comes from four custom properties. Each has a system-color fallback. Set them on an ancestor. The card does not ship theme values.
| Property | Fallback |
|---|---|
--card | Canvas |
--card-foreground | CanvasText |
--border | GrayText |
--ring | Highlight |
--ring colors the keyboard focus outline of the single link.
The article is an @container. Below a 40rem container width the image and text stack. From 40rem they sit in a row, and the image area uses 44% of that row. These are container queries, not viewport breakpoints.
className is appended to the article and does not merge conflicting Tailwind utilities.