PropertyListingCard

Primitive property listing card building blocks for search, recommendations, and saved listing surfaces.

Quick Preview

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.

Layout Variants

Use variant="elevated" for compact bordered surfaces such as side panels, dialogs, or recommendation trays.

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.

Photos coming soon
Awaiting photos

Gwarinpa, Abuja

3 Bed • 3 Baths • Terraced Duplex

Off Plan

Sizes Examples

Use css transform scale to increase or reduce the size.

SM

MD

LG

Props

PrimitiveElementDescription
PropertyListingCardarticleRoot card shell with default and elevated variants.
PropertyListingCardMediadivThe positioned media well. Compose the backdrop and overlays as children.
PropertyListingCardCarouselCarouselCarousel sized to fill the well and sunk behind the overlays. Takes any carousel part as children.
PropertyListingCardCarouselItemCarouselItemOne slide, sized to the well.
PropertyListingCardCarouselIndicatorCarouselIndicatorSlide dots, defaulting to bottom-16 to clear the options bar. Any bottom-* moves them; maxVisible limits the visible dot window.
PropertyListingCardCarouselPreviousCarouselPreviousPrevious-slide control, revealed on carousel hover or keyboard focus.
PropertyListingCardCarouselNextCarouselNextNext-slide control, revealed on carousel hover or keyboard focus.
PropertyListingCardImageimgA listing photo. Takes every image prop — loading, fetchPriority, sizes — plus render for a framework's image component.
PropertyListingCardImagePlaceholderdivStands in for a listing with no photo. Children replace the default copy.
PropertyListingCardMediaTopdivPositioned top overlay row for match, play, close, or custom controls.
PropertyListingCardOptionsBaraPositioned full-width media CTA link. Supports Base UI render for framework links.
PropertyListingCardDetailsdivDetails stack for title, overview, badges, and custom content.
PropertyListingCardTitleh3Listing title text.
PropertyListingCardOverviewpSecondary listing facts or summary.
PropertyListingCardBadgesdivFlex wrapper for Badge components.
PropertyListingCardActionsdivFooter action row.
PropertyListingCardActionButtonButtonFooter button with default tertiary styling.
PropertyListingCardActionButtonLinkButtonLinkFooter 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.

On this page