ShadcnEstate
Components

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-01

The 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

PropTypeRequiredBehavior
classNamestringNoAppended to the article. Conflicting Tailwind utilities are not merged.
labelsListingCardLabelsNoLocalized fallback, loading, and period text.
FieldTypeRequiredBehavior
imageFallbackstringNoText in the image fallback. Blank text hides that area from assistive tech.
priceFallbackstringNoShown when price is missing or cannot be formatted, and when no priceLabel applies.
loadingstringNoAccessible name for the loading article, after trimming.
periodsPartial<Record<ListingPricePeriod, string>>NoNon-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.

PropTypeRequiredBehavior
state"ready"NoDefault variant.
titlestringYesVisible heading. Trimmed before render.
headingLevel2 | 3 | 4NoHeading element. Defaults to 2.
imageListingCardImageNoNative image, or the local fallback.
locationListingCardLocationNoOne location line, or nothing.
priceListingCardPriceNoFormatted price, priceLabel, fallback, or nothing. Does not throw.
propertyTypeListingCardPropertyTypeNoLocalized type label above the title. A blank label hides it.
featuresreadonly ListingCardFeature[]NoFirst four entries only, after id checks on the full list.
hrefstringNoMakes the whole card one link when cta is absent and the URL is safe.
ctaListingCardCtaNoOne 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

FieldTypeRequiredBehavior
srcstringYesUsed only when it is non-blank after trimming and passes the link safety check.
altstringYesTrimmed. Pass an empty string when the image is decorative.
widthnumberNoEmitted only for a positive integer. An invalid value is omitted and the image still renders.
heightnumberNoEmitted only for a positive integer. An invalid value is omitted and the image still renders.

ListingCardLocation

FieldTypeRequiredBehavior
formattedLocationstringNoWins when it contains non-whitespace text.
line1stringNoFirst structured part.
localitystringNoJoined with postalCode by a space.
regionstringNoIncluded after the postal locality.
postalCodestringNoJoined with locality by a space.
countryCodestringNoLast structured part.

Structured parts are joined in that order with a comma and a space. An empty location is omitted.

ListingCardPrice

FieldTypeRequiredBehavior
amountstringYesMajor units: 0 or a number without a leading zero, optionally a decimal point and digits. No sign and no thousands separators. Example: 1999.50.
currencystringYesISO-4217 alphabetic code. Trimmed and uppercased; must be three letters.
period"sale" | "month" | "week" | "night" | "year"NoSelects caller-supplied period text. It does not add text by itself.
localestringNoBCP-47 locale for Intl.NumberFormat. An invalid locale does not use the runtime default. Omitting it does, and is not stable across machines.
priceLabelstringNoReplaces the formatted amount and any period label.

Invalid or missing prices show priceFallback when that text is non-blank, and otherwise show nothing.

ListingCardPropertyType

FieldTypeRequiredBehavior
labelstringYesAlready localized. A blank label hides the type.
code"apartment" | "house" | "land" | "commercial" | "other"NoStored on data-property-type when the label is visible.

ListingCardFeature

FieldTypeRequiredBehavior
idstringYesUnique and non-empty after trimming. Checked for every entry, including entries that are not shown.
labelstringYesAlready localized. Units are the caller's responsibility.
valuestringNoShown 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

FieldTypeRequiredBehavior
hrefstringYesLink target. An unsafe target removes the link.
labelstringYesVisible 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.

PropertyFallback
--cardCanvas
--card-foregroundCanvasText
--borderGrayText
--ringHighlight

--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.

On this page