PropertyCard

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

Installation

pnpm add @wandercom/design-system-web

Usage

23 lines
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...
12 lines
<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...
12 lines
<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)}
/>

With description

Provide a description to show a short tagline below the property name and above the features.

Loading example...
12 lines
<PropertyCard
  name="Wander Forest Retreat"
  description="Forest views with a hot tub and deck"
  images={images}
  price={1060}
  nights={2}
  rating={4.8}
  features={{ bedrooms: 4, beds: 6, baths: 4 }}
  href="#"
  isWishlisted={isWishlisted}
  onWishlistClick={() => setIsWishlisted(!isWishlisted)}
/>

New listing

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

Loading example...
11 lines
<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)}
/>

With badge

Use the badge prop to overlay any node in the image's top-left corner. Today this slot is used to render a tier badge, but it accepts any ReactNode so consumers can render their own component. Style the wrapper via slots.badge.

Loading example...
14 lines
import { Badge } from '@wandercom/design-system-web/ui/badge';

<PropertyCard
  name="Home in Newport beach"
  badge={<Badge variant="info">Tier 1</Badge>}
  images={images}
  price={1060}
  nights={2}
  rating={4.8}
  features={{ bedrooms: 4, beds: 6, baths: 4 }}
  href="#"
  isWishlisted={isWishlisted}
  onWishlistClick={() => setIsWishlisted(!isWishlisted)}
/>

Not swipeable

Set isSwipeable={false} to disable touch swiping on the image carousel. The progress bar is hidden when the card container is narrower than 65rem (1040px), but remains visible in wider cards where arrow navigation is available.

Loading example...
12 lines
<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...
7 lines
<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...
10 lines
<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.

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

badge?:

React.ReactNode
Optional content overlaid in the image's top-left corner. Used today to render a tier badge, but accepts any `ReactNode`.

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.

description?:

string | null
Short description displayed below the property name and above the features row.

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 when the card container is narrower than 65rem (1040px). 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`, `badge`, `content`, `progress`, `nav`.

className?:

string
Additional CSS classes to apply to the root element.

Types

PropertyCardImage

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

PropertyCardFeatures

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

PropertyCardSlots

8 lines
type PropertyCardSlots = {
  root?: string;     // Root element
  image?: string;    // Image container
  badge?: string;    // Badge container (top-left overlay)
  content?: string;  // Content container
  progress?: string; // Progress bar container
  nav?: string;      // Navigation arrow buttons
};
PropertyCard