# Cards and grids

`ListingCard` shows one listing as a card: photo, price, address, specs and the listing brokerage. `ListingGrid` lays a list of them out in a responsive grid. Both render on the server with no client JavaScript, so they work in search results, building pages, home page sections and anywhere else you have listings.

```tsx
import { ListingCard, ListingGrid } from '@sonordev/re-site-kit'
```

## ListingGrid

```tsx
<ListingGrid
  listings={listings}
  hrefFor={(listing) => (listing.slug ? `/listings/${listing.slug}` : null)}
  emptyState={<p>No homes match those filters.</p>}
/>
```

| Prop         | Type                          | What it does                                                                                                  |
| ------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `listings`   | `RealEstateListing[]`         | The listings to show. Required.                                                                               |
| `hrefFor`    | `(listing) => string \| null` | Each card's link. Return `null` for no link. Leave the prop out and cards link to the listing's `source_url`. |
| `emptyState` | `ReactNode`                   | Rendered instead of the grid when `listings` is empty. Without it, an empty list renders nothing.             |
| `className`  | `string`                      | Added next to `re-listing-grid`.                                                                              |

The grid auto-fills columns at least 320px wide with a 1.5rem gap. Every card in it credits the listing brokerage (see [Brokerage credit](#brokerage-credit)).

**Link to your own pages.** Without `hrefFor`, cards link to `source_url`, the listing's page at its source, and open it in a new tab. On a site with a page per listing you'll almost always want `hrefFor`. Guard against listings without a slug so no card links to `/listings/null`:

```tsx
hrefFor={(listing) => (listing.slug ? `/listings/${listing.slug}` : null)}
```

**A different layout.** The grid's column rule is an inline style. For a carousel, a two-column layout or anything else, map `ListingCard` inside your own container:

```tsx
<ul className="featured-homes">
  {listings.map((listing) => (
    <li key={listing.id}>
      <ListingCard listing={listing} href={listing.slug ? `/listings/${listing.slug}` : null} />
    </li>
  ))}
</ul>
```

## ListingCard

```tsx
<ListingCard listing={listing} href="/listings/12-oak-st-springfield-123456" />
```

| Prop              | Type                | Default                    | What it does                                                         |
| ----------------- | ------------------- | -------------------------- | -------------------------------------------------------------------- |
| `listing`         | `RealEstateListing` |                            | The listing. Required.                                               |
| `href`            | `string \| null`    | the listing's `source_url` | Where the card links. Pass `null` to render the card without a link. |
| `showAttribution` | `boolean`           | `true`                     | Show "Courtesy of" the listing brokerage.                            |
| `className`       | `string`            |                            | Added next to `re-listing-card`.                                     |

What's on the card:

- **The first photo**, cropped to 16:10, lazy-loaded, with the street address as its alt text. With no photo, the frame keeps its shape on a gray background.
- **The price**, from `formatPrice` (the feed's own display string when it has one).
- **A status badge**, only when the listing isn't `active` (for example "pending").
- **The street address, the location and the specs** ("3 bd · 2 ba · 1,960 sqft").
- **"Courtesy of Example Realty"**, the listing brokerage, when the listing has a `listing_office_name` and `showAttribution` is on.

**Links.** An `href` that starts with `http://` or `https://` opens in a new tab with `rel="noopener noreferrer"`. A path like `/listings/12-oak-st` opens in the same tab. So pass a path, not a full URL, for pages on your own site.

## Brokerage credit

MLS IDX rules generally require the listing office to be named wherever another broker's listing is shown, and that includes search results, not just the detail page. It's also the common case: a page of search results usually shows listings from many brokerages, and only a few are your own.

So `ListingCard` credits the listing brokerage by default, and `ListingGrid` always shows it. Style it with `.re-listing-card__courtesy`:

```css
.re-listing-card__courtesy {
  color: #4b5563;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
```

Its size, opacity and margin are inline styles, so changing those takes `!important` (see [Styling and performance](https://sonor.dev/re-site-kit/styling#overriding-inline-styles)).

Pass `showAttribution={false}` only where the surrounding UI already credits the office, for example in your own card layout that prints the brokerage elsewhere. More on what IDX asks for is in [IDX compliance](https://sonor.dev/re-site-kit/idx).

## Styling

Both components ship a few inline layout essentials and a class on every element. The full list of classes, and how to override the inline styles, is in [Styling and performance](https://sonor.dev/re-site-kit/styling).
