Product Card
The card a product grid is made of. It composes image, price, rating, badge and button — so the pieces you already know keep behaving the way you already know.
Brass desk lamp
Warm, dimmable, and heavy enough to stay where you put it.
Prices and sales
Pass price, and compare-at for what it used to cost:
<x-wirekit::product-card name="Brass desk lamp" :price="3900" :compare-at="5900" :minor-units="true" currency="EUR" />
The old price renders struck through in a real <del>, and the whole story is
announced: "€39.00, was €59.00, 34% off". That comes from
price, which already does it properly — so the "Sale" badge on
the corner is pure decoration and carries nothing a screen-reader user would miss.
A compare-at that is equal to or lower than price is not treated as a sale.
It is not a discount, and striking it through would be a claim the numbers do not
support.
Stock
availability |
What happens |
|---|---|
in-stock |
Nothing (the default) |
low-stock |
A "Low stock" badge. The CTA stays live — it is a nudge, not a wall. |
out-of-stock |
An "Out of stock" badge, and the CTA is disabled and says why |
Both states are words, not a wash over the image. A reader who cannot resolve a gray tint sees a perfectly normal product.
Images
image-alt defaults to empty on purpose. The product name is already right there
in text, so an alt that repeats it makes a screen reader read the product twice.
Pass one when the image shows something the name does not:
<x-wirekit::product-card
name="Brass desk lamp"
image="/img/lamp.jpg"
image-alt="Lit and angled over a dark desk"
:price="3900"
/>
The link
href makes the whole card clickable — but the link is the product name, not
the card.
That is deliberate. Wrapping the card in one anchor makes the link's accessible name the entire card (image, price, rating, button text, all read as one breathless sentence), and it swallows the add-to-cart button, which cannot nest inside an anchor at all. The pointer gets the big target; the screen reader gets a short, honest name.
A product without a photo
image takes a URL, which is the ordinary case. When the media is not a URL — a painted
placeholder, a color field, a <video>, a chart — put it in the media slot instead. The slot
wins over image when both are given.
The badge slot sits beside the ones the card works out for itself. Those answer two questions,
on sale and in stock; anything else — "New", "Limited", "Last pair" — goes here, and the two stack
rather than landing on top of each other.
G
With neither image nor media, the card draws no media area at all. That is deliberate: a
placeholder invented here would appear on every card that legitimately has no picture, and a
caller who wants one has the slot to put exactly the box they mean.
A different action
The action slot replaces the default CTA:
<x-wirekit::product-card name="Studio chair" :price="24900" availability="out-of-stock">
<x-slot:action>
<x-wirekit::button intent="neutral" class="w-full">Notify me</x-wirekit::button>
</x-slot:action>
</x-wirekit::product-card>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string |
'' |
The product. Rendered as the heading. |
href |
string|null |
null |
Product page. Makes the card clickable; the name is the link. |
image |
string|null |
null |
Cover image. |
imageAlt |
string|null |
null |
Accessible name for the image. Empty by default — the name is already in text. |
price |
int|float|null |
null |
Current price. |
compareAt |
int|float|null |
null |
What it used to cost. Only treated as a sale when it is higher. |
currency |
string |
config | Currency code. |
minorUnits |
bool |
false |
Treat the amounts as cents. |
rating |
int|float|null |
null |
Average out of 5. |
reviewCount |
int|null |
null |
How many people rated it. |
description |
string|null |
null |
Short line under the name. Clamped to two lines. |
availability |
string |
'in-stock' |
in-stock, low-stock, out-of-stock. |
scope |
string|null |
null |
Class-scope override. |
Slots
| Slot | Description |
|---|---|
media |
Replaces the cover image. For media that is not a URL. Wins over image. |
badge |
An extra badge over the media, stacked under the derived one. |
action |
Replaces the default add-to-cart button. |
Accessibility
- An
<article>, so a screen reader can move between products as units. - The name is the link, never the whole card — see above.
- The sale is announced by price ("was €59.00, 34% off"), so it never depends on seeing a strike-through or a red badge.
- Stock states are words in a badge, not a tint.
- A disabled CTA says why in its accessible name ("Out of stock: Brass desk lamp"). A disabled button with no explanation is a dead end.
- The review count sits next to the stars: "4.5 stars" from two people and from two thousand are different claims.
- A product nobody has rated renders no star row at all — an empty row implies zero stars, and unrated is not zero.
Keyboard Interaction
| Key | Action |
|---|---|
| Tab | Move to the product name, then to the CTA. |
| Enter | Follow the name to the product page. |
| Enter / Space | Fire the CTA when it has focus. |
Two stops per card, not one per element — the image, badges and rating are not focusable, because none of them do anything.