PropertyCard

A card component for displaying property listings with image carousel, details, and wishlist functionality.

Installation

pnpm add @wandercom/design-system-web

Usage

import { useState } from 'react';
import { PropertyCard } from '@wandercom/design-system-web/blocks/property-card';

export function Example() {
  const [isWishlisted, setIsWishlisted] = useState(false);

  return (
    <PropertyCard
      name="Home in Newport beach"
      images={[
        { src: "/img1.jpg", alt: "Front view" },
        { src: "/img2.jpg", alt: "Interior" },
      ]}
      price={1060}
      nights={2}
      rating={4.8}
      features={{ bedrooms: 4, beds: 6, baths: 4 }}
      href="/property/newport-beach"
      isWishlisted={isWishlisted}
      onWishlistClick={() => setIsWishlisted(!isWishlisted)}
    />
  );
}

Examples

Loading example...

Default

Basic property card with rating, price, and features.

Loading example...
<PropertyCard
  name="Home in Newport beach"
  images={images}
  price={1060}
  nights={2}
  rating={4.8}
  features={{ bedrooms: 4, beds: 6, baths: 4 }}
  href="https://wander.com"
  target="_blank"
  isWishlisted={isWishlisted}
  onWishlistClick={() => setIsWishlisted(!isWishlisted)}
/>

With location

When a location is provided, it appears above the property name.

Loading example...
<PropertyCard
  name="Oceanfront Villa"
  location="Malibu, California"
  images={images}
  price={2450}
  nights={2}
  rating={4.9}
  features={{ bedrooms: 5, beds: 7, baths: 5 }}
  href="#"
  isWishlisted={isWishlisted}
  onWishlistClick={() => setIsWishlisted(!isWishlisted)}
/>

New listing

When no rating is provided, the card displays "New" with a star icon.

Loading example...
<PropertyCard
  name="Mountain Retreat"
  location="Aspen, Colorado"
  images={images}
  price={850}
  nights={1}
  features={{ bedrooms: 3, beds: 4, baths: 2 }}
  href="#"
  isWishlisted={isWishlisted}
  onWishlistClick={() => setIsWishlisted(!isWishlisted)}
/>

Not swipeable

Set isSwipeable={false} to disable touch swiping on the image carousel. The progress bar is hidden on mobile viewports but remains visible on desktop where arrow navigation is available.

Loading example...
<PropertyCard
  name="Home in Newport beach"
  images={images}
  price={1060}
  nights={2}
  rating={4.8}
  features={{ bedrooms: 4, beds: 6, baths: 4 }}
  href="#"
  isSwipeable={false}
  isWishlisted={isWishlisted}
  onWishlistClick={() => setIsWishlisted(!isWishlisted)}
/>

Without wishlist

Set showWishlist={false} to hide the wishlist button entirely. Useful for contexts where wishlisting isn't available.

Loading example...
<PropertyCard
  name="Beachfront Bungalow"
  images={images}
  price={1200}
  href="#"
  showWishlist={false}
/>

Without rating

Set showRating={false} to hide the rating and star icon entirely, including the "New" fallback state. Useful when rating data is unavailable or not relevant.

Loading example...
<PropertyCard
  name="Home in Newport beach"
  images={images}
  price={1060}
  nights={2}
  href="#"
  showRating={false}
  isWishlisted={isWishlisted}
  onWishlistClick={() => setIsWishlisted(!isWishlisted)}
/>

Use the asChild prop to compose with routing libraries like Next.js Link.

import Link from 'next/link';

<PropertyCard
  name="Home in Newport beach"
  images={[{ src: "/img.jpg", alt: "Property" }]}
  price={1060}
  nights={2}
  asChild
>
  <Link href="/property/newport-beach" />
</PropertyCard>

Accessibility

The image carousel follows the WAI-ARIA Carousel Pattern (basic, non-auto-rotating variant).

Carousel ARIA structure

  • Carousel container uses role="region" with aria-roledescription="carousel" and an accessible label derived from the property name
  • Each slide uses role="group" with aria-roledescription="slide" and an aria-label indicating position (e.g., "Front view (1 of 3)")
  • Non-visible slides are hidden from the accessibility tree via aria-hidden
  • The slides container uses aria-live="polite" so screen readers announce slide changes

Keyboard navigation

  • ArrowLeft / ArrowRight navigate between slides when the carousel is focused
  • Tab moves focus through the previous/next buttons and wishlist button
  • Previous/next buttons become visible on focus for keyboard discoverability

Rendering semantics

  • Renders as <a> when href is provided
  • Renders as <button> when onClick is provided (without href)
  • Renders as <article> with aria-label set to the property name when neither is provided
  • Wishlist button has aria-pressed state and dynamic aria-label reflecting wishlisted state
  • Decorative elements (star icon, progress indicator) are hidden from the accessibility tree

Props

name:

string
Property name displayed below the image.

images:

PropertyCardImage[]
Array of images for the carousel. Each image has `src`, `alt`, and optional `thumbhash` for blur placeholder.

price?:

number | null
Price as a number (e.g., 1060). Automatically formatted based on `currency` and `locale` props.

currency?:

string
Currency code for formatting (e.g., "USD", "EUR"). Defaults to "USD".

locale?:

string
Locale for currency formatting (e.g., "en-US", "de-DE"). Defaults to "en-US".

nights?:

number | null
Number of nights for the price. Displays as "for X night(s)" after the price.

rating?:

number | null
Rating value displayed with a star icon (e.g., 4.8). When not provided, displays "New".

features?:

PropertyCardFeatures | null
Property features object with `bedrooms`, `beds`, and `baths` counts.

location?:

string | null
Location string displayed above the property name.

href?:

string
Link destination. When provided, the card renders as an anchor tag.

target?:

string
Link target (e.g., "_blank" for new tab). Automatically adds `rel="noopener noreferrer"` when set to "_blank".

onClick?:

() => void
Click handler. When provided (without href), the card renders as a button.

asChild?:

boolean
Use the asChild pattern to compose with a custom element (e.g., Next.js Link).

isSwipeable?:

boolean
Whether the image carousel is swipeable. When false, hides the progress bar on mobile. Defaults to true.

showRating?:

boolean
Whether to show the rating/reviews section. Defaults to true. Set to false to hide rating, star icon, and the "New" fallback state.

showWishlist?:

boolean
Whether to show the wishlist button. Defaults to true. Set to false to hide the wishlist functionality entirely.

isWishlisted?:

boolean
Whether the property is wishlisted. Controls the heart icon filled state and visibility.

onWishlistClick?:

(e: MouseEvent) => void
Callback when the wishlist button is clicked.

slots?:

PropertyCardSlots
Slot-based className overrides for subcomponents: `root`, `image`, `content`, `progress`, `nav`.

className?:

string
Additional CSS classes to apply to the root element.

Types

PropertyCardImage

type PropertyCardImage = {
  src: string;
  alt: string;
  thumbhash?: string; // Optional blur placeholder hash
};

PropertyCardFeatures

type PropertyCardFeatures = {
  bedrooms: number;
  beds: number;
  baths: number;
};

PropertyCardSlots

type PropertyCardSlots = {
  root?: string;     // Root element
  image?: string;    // Image container
  content?: string;  // Content container
  progress?: string; // Progress bar container
  nav?: string;      // Navigation arrow buttons
};
PropertyCard