PropertyListingCard
Primitive property listing card building blocks for search, recommendations, and saved listing surfaces.
Quick Preview
Maitama, Abuja
2 Bed • 3 Baths • Semi-Detached Bungalow
Usage
Compose the card from primitives and keep listing data, product callbacks, routing, and badge choices in the consuming surface.
The media well is composed the same way. PropertyListingCardMedia is a positioned box; what fills it — a carousel, one image, a placeholder — and where the indicator sits are yours to say.
import { Badge } from "@giddaa-housing/ui/badge";
import {
PropertyListingCard,
PropertyListingCardActionButton,
PropertyListingCardActionButtonLink,
PropertyListingCardActions,
PropertyListingCardBadges,
PropertyListingCardCarousel,
PropertyListingCardCarouselIndicator,
PropertyListingCardCarouselItem,
PropertyListingCardCarouselNext,
PropertyListingCardCarouselPrevious,
PropertyListingCardDetails,
PropertyListingCardImage,
PropertyListingCardImagePlaceholder,
PropertyListingCardMedia,
PropertyListingCardMediaTop,
PropertyListingCardOptionsBar,
PropertyListingCardOverview,
PropertyListingCardTitle,
} from "@giddaa-housing/ui/property-listing-card";
import { CarouselContent } from "@giddaa-housing/ui/carousel";
import { Match } from "@giddaa-housing/ui/match";
import { PriceTag } from "@giddaa-housing/ui/price-tag";
import { Link } from "@tanstack/react-router";
<PropertyListingCard>
<PropertyListingCardMedia>
<PropertyListingCardCarousel>
<CarouselContent>
{images.map((image, index) => (
<PropertyListingCardCarouselItem key={image.id}>
<PropertyListingCardImage
src={image.src}
alt={image.alt}
loading={index === 0 ? "eager" : "lazy"}
/>
</PropertyListingCardCarouselItem>
))}
</CarouselContent>
<PropertyListingCardCarouselIndicator className="bottom-28" />
</PropertyListingCardCarousel>
<PropertyListingCardMediaTop>
<Match value={95} label="Match" variant="donut" />
{/* Add play, close, share, or other overlay controls here. */}
</PropertyListingCardMediaTop>
<PriceTag className="absolute bottom-16 left-2 z-10" value="N78,650,000" />
<PropertyListingCardOptionsBar
render={
<Link
to="/listings/$listingId/purchase-options"
params={{ listingId }}
/>
}
>
7 options available to buy this house
</PropertyListingCardOptionsBar>
</PropertyListingCardMedia>
<PropertyListingCardDetails>
<PropertyListingCardTitle>Maitama, Abuja</PropertyListingCardTitle>
<PropertyListingCardOverview>
2 Bed • 3 Baths • Semi-Detached Bungalow
</PropertyListingCardOverview>
<PropertyListingCardBadges>
<Badge variant="success" border={false} shape="box" size="sm">
You Qualify to Buy
</Badge>
</PropertyListingCardBadges>
</PropertyListingCardDetails>
<PropertyListingCardActions>
<PropertyListingCardActionButton>Message Us</PropertyListingCardActionButton>
<PropertyListingCardActionButtonLink href="/listings/maitama">
View Listing
</PropertyListingCardActionButtonLink>
</PropertyListingCardActions>
</PropertyListingCard>Examples
Default Card
Use the default variant for full-width listing surfaces where the media should lead the card.
Maitama, Abuja
2 Bed • 3 Baths • Semi-Detached Bungalow
Layout Variants
Use variant="elevated" for compact bordered surfaces such as side panels, dialogs, or recommendation trays.
Maitama, Abuja
2 Bed • 3 Baths • Semi-Detached Bungalow
Maitama, Abuja
2 Bed • 3 Baths • Semi-Detached Bungalow
Composed Media
Build the backdrop yourself when the defaults do not fit: lazy-load every slide but the first, move the indicator clear of the price tag and options bar, or stand a listing with no photos on the placeholder.
Maitama, Abuja
2 Bed • 3 Baths • Semi-Detached Bungalow
Gwarinpa, Abuja
3 Bed • 3 Baths • Terraced Duplex
Sizes Examples
Use css transform scale to increase or reduce the size.
Maitama, Abuja
2 Bed • 3 Baths • Semi-Detached Bungalow
SM
Maitama, Abuja
2 Bed • 3 Baths • Semi-Detached Bungalow
MD
Maitama, Abuja
2 Bed • 3 Baths • Semi-Detached Bungalow
LG
Props
| Primitive | Element | Description |
|---|---|---|
PropertyListingCard | article | Root card shell with default and elevated variants. |
PropertyListingCardMedia | div | The positioned media well. Compose the backdrop and overlays as children. |
PropertyListingCardCarousel | Carousel | Carousel sized to fill the well and sunk behind the overlays. Takes any carousel part as children. |
PropertyListingCardCarouselItem | CarouselItem | One slide, sized to the well. |
PropertyListingCardCarouselIndicator | CarouselIndicator | Slide dots, defaulting to bottom-16 to clear the options bar. Any bottom-* moves them; maxVisible limits the visible dot window. |
PropertyListingCardCarouselPrevious | CarouselPrevious | Previous-slide control, revealed on carousel hover or keyboard focus. |
PropertyListingCardCarouselNext | CarouselNext | Next-slide control, revealed on carousel hover or keyboard focus. |
PropertyListingCardImage | img | A listing photo. Takes every image prop — loading, fetchPriority, sizes — plus render for a framework's image component. |
PropertyListingCardImagePlaceholder | div | Stands in for a listing with no photo. Children replace the default copy. |
PropertyListingCardMediaTop | div | Positioned top overlay row for match, play, close, or custom controls. |
PropertyListingCardOptionsBar | a | Positioned full-width media CTA link. Supports Base UI render for framework links. |
PropertyListingCardDetails | div | Details stack for title, overview, badges, and custom content. |
PropertyListingCardTitle | h3 | Listing title text. |
PropertyListingCardOverview | p | Secondary listing facts or summary. |
PropertyListingCardBadges | div | Flex wrapper for Badge components. |
PropertyListingCardActions | div | Footer action row. |
PropertyListingCardActionButton | Button | Footer button with default tertiary styling. |
PropertyListingCardActionButtonLink | ButtonLink | Footer link button with default tertiary styling. |
Deprecated: the images prop
<PropertyListingCardMedia images={images} /> still builds the same backdrop — carousel for several, a single image for one, the placeholder for none — and will until the next major. It is deprecated because a list of { id, src, alt } left no way to lazy-load a photo, move the indicator, or hand the images to a framework's image component. Compose the backdrop instead.
One behavioural note: media with no images prop at all is now an empty well rather than the placeholder, since that is how you compose your own backdrop. Render PropertyListingCardImagePlaceholder to ask for the placeholder explicitly. Passing images={[]} behaves as it always did.
Custom image components
PropertyListingCardImage accepts Base UI render, and reads ImageProvider from @giddaa-housing/ui/image — so a Next.js app names next/image once at the root and every listing photo follows.
Linked images
Wrap each image in a real link when selecting a photo should open the listing. Keep the previous/next controls and indicator outside the link so interactive elements are not nested and carousel navigation remains independent.
<PropertyListingCardCarousel>
<CarouselContent>
{images.map((image) => (
<PropertyListingCardCarouselItem key={image.id}>
<Link
to="/listings/$listingId"
params={{ listingId }}
className="block size-full"
aria-label={`View listing: ${listingTitle}`}
>
<PropertyListingCardImage
src={image.src}
alt={image.alt}
loading="lazy"
/>
</Link>
</PropertyListingCardCarouselItem>
))}
</CarouselContent>
<PropertyListingCardCarouselIndicator maxVisible={5} />
<PropertyListingCardCarouselPrevious />
<PropertyListingCardCarouselNext />
</PropertyListingCardCarousel>Use the router's link component, as above, or a native <a href="…">. Avoid an onClick navigation handler: a link preserves keyboard navigation, open-in-new-tab, copy-link, and assistive-technology semantics.
Accessibility
- Provide descriptive image alt text when the listing photo communicates property details.
- Pass accessible names for icon-only share, favorite, close, or custom actions.
- Keep product routing and analytics outside the primitives; wire them in the consuming surface.