Ship-It Designv0.0.20

GitHub

ListingCard

A consumer-marketplace card composing photos (Carousel), title, Rating, price, host info, and optional verified badge / favorite toggle. Built for marketplace grids — search results, related listings, a host's other stays.

Two variants share the same photo + price + title spine:

  • default — consumer-stay style: rating, host, distance, favorite heart. Marketplace search-result row.
  • spec — product-spec style: flag pill, photo counter, spec grid, inline CTA on a dark footer. Premium / spec-driven inventory.

Distinct from EntityCard: EntityCard is dev-tool chrome (service / owner / artifact). ListingCard is the consumer card.

ListingCard also emits schema.org JSON-LD by default — see the AI / SEO surface section below.

Default#

The photo carousel loops by default — clicking past the last photo wraps to the first. Pass loop={false} if you need stop-at-end behavior (see Carousel → Loop for what's going on under the hood).

Loading…

On sale#

Pass originalPrice to render a strike-through using the sale semantic token.

Loading…

Spec#

variant="spec" swaps the consumer-stay layout for a product-spec card. The default rating / host / favorite slots are ignored in this variant; use flag, category, meta, specs, pricePrefix, and cta instead.

Loading…

Click & hover#

Pass onClick to make the whole card surface clickable — an invisible stretched <button> sits below the inner actions (favorite, CTA, links) so those keep their own click semantics. href still works for plain navigation; if both are set, onClick wins.

hoverEffect picks the visual treatment. Defaults to lift when the card is interactive (has onClick or href), otherwise none.

Loading…

Editable sections#

Every text slot (title, eyebrow, category, meta, spec cells, price, host, …) accepts arbitrary ReactNode. Drop in <InlineEdit> to make any cell editable in place. Wrap editor cells in <span className="relative z-10" onClick={(e) => e.stopPropagation()}> when the card also has onClick, so dbl-click-to-edit doesn't open the detail.

Flag pill (ListingCardFlag)#

Used in the spec variant for the top-left badge ("Flagship", "New", "Editor's pick").

  • label: ReactNode (required) — the pill text.
  • icon?: GlyphName — optional leading glyph from the design-system manifest.
  • tone?: 'accent' | 'purple' | 'pink' | 'ok' | 'warn' — color palette. Default 'accent'.

Spec cell (ListingCardSpec)#

Each cell in the spec grid (specs[] on variant="spec").

  • label: ReactNode (required) — small uppercase label above the value (e.g. "0-60", "Power", "Drive").
  • value: ReactNode (required) — the headline value ("2.9s", "495 hp", "RWD").

The grid auto-columns to length (2, 3, or 4-up); past 4 cells it wraps to a 4-column grid.

The bottom CTA button on variant="spec". When set, the whole-card stretched link is suppressed so the CTA wins the click.

  • label: ReactNode (required) — button text.
  • onClick?: () => void — click handler.
  • href?: string — when set, renders as an <a> for native navigation.
  • disabled?: boolean — disables without removing the button.

Styling customization#

Every section is independently styleable via the classNames slot map. Each key targets one internal element; values are merged with the component's own utilities via cn() — so consumers can override, extend, or replace any styling without forking.

Slots: root, photos, flag, photoCounter, favorite, body, header, title, eyebrow, category, meta, specs, specCell, specLabel, specValue, footer, price, priceUnit, cta.

Loading…

AI / SEO surface#

ListingCard emits schema.org JSON-LD by default — Accommodation for the default variant, Product for the spec variant. Override via schema?: 'Accommodation' | 'Product' | 'Place' | 'LodgingBusiness' or any custom string. Pass priceCurrency? to include an offers block (otherwise prices are skipped from JSON-LD), and url? to populate the entity URL. Spec cells become additionalProperty entries. Pass noStructuredData to suppress emission entirely.

Top-level props#

Props for ListingCard
PropTypeDefaultDescription
variantenumdefaultVisual variant. Default `default`.
photos *readonly string[]Photo URLs (or anything `Carousel` can render). At least one.
renderPhoto((src: string, index: number) => ReactNode) | undefinedOverride the photo renderer. Defaults to a decorative `<img src>`. Use this when consumers need theme-aware photos (e.g. inline SVG placeholders that follow `currentColor`) or non-image slides.
loopboolean | "circular" | "sweep" | undefinedtrueWrap the photo carousel past the boundaries (next from the last photo goes to the first). Default `true` — marketplace photo browsing expects looping. Pass `false` to restore stop-at-end, or `'circular'` / `'sweep'` to pick the loop variant explicitly.
title *ReactNodeListing title — e.g. "Sun-soaked cabin in Marin".
eyebrowReactNodeOptional eyebrow text above the title (location, listing type).
price *ReactNodeHeadline price (e.g. `189`).
priceUnitReactNode/dayPrice unit suffix (e.g. `/night`).
originalPriceReactNodeOriginal price for sale strike-through.
pricePrefixReactNodePrefix shown before the price — e.g. "from". `spec` variant only.
widthstring | number | undefinedvariant === 'spec' ? 320 : 280Card width override.
hrefstring | undefinedLink target for the whole card (default) or the title (spec).
onClick((e: MouseEvent<HTMLButtonElement, MouseEvent>) => void) | undefinedWhole-card click handler. Renders an invisible stretched `<button>` underneath the inner actions (favorite, CTA, links) so clicks on those take precedence. Use this for "click card → open detail" without leaving the page.
hoverEffectenumVisual treatment on hover. Default `lift` when the card is interactive (has `onClick` / `href`), otherwise `none`.
ratingnumber | undefinedAverage rating (0–5). When undefined, the rating row is hidden.
reviewCountnumber | undefinedNumber of reviews — shown next to the rating.
hostReactNodeHost / owner name.
distanceReactNodeDistance label (e.g. `0.4 mi away`).
verifiedboolean | undefinedWhen true, shows a `verified` badge on the photo.
onFavorite((next: boolean) => void) | undefinedHeart-icon favorite toggle handler.
favoritedboolean | undefinedCurrent favorite state.
flagListingCardFlag | undefinedPill rendered top-left of the photo.
categoryReactNodeSmall category tag right-aligned in the title row.
metaReactNodeDim secondary line under the title (e.g. listing ID · year).
specsreadonly ListingCardSpec[] | undefinedSpec cells rendered as a grid below the title block.
ctaListingCardCta | undefinedBottom CTA button. When set, no whole-card stretched link is rendered.
hidePhotoCounterboolean | undefinedHide the photo counter overlay in `spec` variant. Default `false`.
schema(string & {}) | "Product" | "Accommodation" | "Place" | "LodgingBusiness" | undefinedschema.org `@type` for the JSON-LD entity. Defaults to `'Accommodation'` for the default variant and `'Product'` for the spec variant. Pass `'Place'`, `'LodgingBusiness'`, or any string to override.
priceCurrencystring | undefinedISO 4217 currency code (e.g. `'USD'`). REQUIRED to emit the `offers` block — without it the offer is skipped (the rest of the entity still emits if `title` resolves to a string).
priceAmountnumber | undefinedExplicit numeric price for JSON-LD. When omitted, parsed from the visible `price` prop by stripping non-numeric characters. Pass this when `price` is JSX or not cleanly parseable.
urlstring | undefinedOptional URL of the listing detail page — also emitted as the entity `url`.
titleTextstring | undefinedString version of `title` for the JSON-LD `name`. Required if `title` is JSX.
descriptionTextstring | undefinedString version of `eyebrow` for the JSON-LD `description`.
noStructuredDataboolean | undefinedOpt out of emitting the JSON-LD script.
classNamesPartial<{ root: string; photos: string; flag: string; photoCounter: string; favorite: string; body: string; header: string; title: string; eyebrow: string; category: string; meta: string; specs: string; ... 6 more ...; cta: string; }> | undefined{}Per-section className overrides. Each key targets a specific element in the rendered tree; values are merged with the component's own utilities via `cn()` so consumers can override, extend, or replace any styling without forking the component. The `className` prop still controls the outer Card element — `classNames.root` is an alias that's also merged onto it.

Composition#

The card is a thin assembly over primitives — Card for the shell, Carousel for photos, Rating for the star row, Badge for verified, and heart icon for favorite. For unusual layouts, compose the same primitives yourself.