---
name: product-card-horizontal
title: Product Card Horizontal
url: https://ui.frontic.com/components/sections/product-card-horizontal.md
tier: sections
install: npx @frontic/ui add product-card-horizontal
files:
  - components/sections/product-card-horizontal/ProductCardHorizontal.vue
  - components/sections/product-card-horizontal/index.ts
  - components/sections/product-card-horizontal/types.ts
registryDependencies:
  - badge
  - button
  - card
  - image
  - price
  - product
  - rating
  - swatch-group
  - use-product-sibling
dependencies:
  - "@lucide/vue"
  - "@vueuse/core"
---

# Product Card Horizontal

Product card that lays its image beside the details once its container is wide enough, with description, rating, swatches, price and add to cart.

## Install

```sh
npx @frontic/ui add product-card-horizontal
```

Built from: [Badge](https://ui.frontic.com/components/ui/badge.md), [Button](https://ui.frontic.com/components/ui/button.md), [Card](https://ui.frontic.com/components/ui/card.md), [Image](https://ui.frontic.com/components/ui/image.md), [Price](https://ui.frontic.com/components/ui/price.md), [Product](https://ui.frontic.com/lib/product.md), [Rating](https://ui.frontic.com/components/ui/rating.md), [Swatch Group](https://ui.frontic.com/components/ui/swatch-group.md), [Use Product Sibling](https://ui.frontic.com/composables/use-product-sibling.md)

npm packages: `@lucide/vue`, `@vueuse/core`

## Examples

```vue
<script setup lang="ts">
import { ProductCardHorizontal, type Product } from '@/components/sections/product-card-horizontal'

const product: Product = {
  name: 'Alpine View Horizon',
  key: 'alpine-view-horizon',
  description: 'A wide mirrored lens for a clear view across the piste, and a strap that sits securely over a helmet.',
  link: { path: 'https://sw.demo-shop.com/alpineview-horizon-4fda60a5' },
  price: { amount: 8400, currency: 'EUR', ref: 9900 },
  cover: { src: 'https://static.frontic.com/cdn-cgi/image/width=640,format=auto/ui/products/alpine-view-horizon.jpg' },
  rating: 4.3,
  reviewCount: 42,
  inStore: true,
}
</script>

<template>
  <div class="mx-auto w-full max-w-2xl">
    <ProductCardHorizontal :product="product" />
  </div>
</template>
```

## API reference

### ProductCardHorizontal

Shows a product with its image beside its details and price, with an add-to-cart button.

Styling: [`part-title`](https://ui.frontic.com/DESIGN.md#part-title); [`hint`](https://ui.frontic.com/DESIGN.md#hint). Change them there and every part that extends them follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `product` (required) | `Product` |  | The product the card shows; its `badge`, `clearout` and `inStore` fields add badges over the image. |
| `addToCartLabel` | `string` | `'Add to Cart'` | Shown on the button that adds the product to the cart. |
| `addedLabel` | `string` | `'Added'` | Shown on the add-to-cart button for two seconds after the product was added. |
| `clearoutLabel` | `string` | `'Clearout'` | Shown on the badge of a product marked `clearout`. |
| `inStoreLabel` | `string` | `'In Store'` | Shown on the badge of a product that is available in store. |
| `locale` | `string` |  | BCP 47 locale the price is formatted in, e.g. `de-DE`; the ConfigProvider's when left out. |
| `ratingLabel` | `((score: number, max: number) => string)`: `(score: number, max: number): string` |  | Builds the rating's screen-reader name from the score, to one decimal, and the top of the scale; `4.6 out of 5` when left out. |
| `surface` | `'raised' \| 'bare'` | `'raised'` | Bare drops the edge, shadow and inset, for a section placed inside another Card. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Event | Payload | Description |
| --- | --- | --- |
| `add-to-cart` | `product: Product` | Fires with the product when the shopper adds it to the cart. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-surface` | `'raised' \| 'bare'` | Carries the `surface` prop. |

## Exports

```ts
import { type Product, type ProductSibling } from '@/components/sections/product-card-horizontal'
```

- `Product`, `ProductSibling`: from [Product](https://ui.frontic.com/lib/product.md)

## Styling

| Part | Recipe |
| --- | --- |
| `default` | [`product-card-horizontal`](https://ui.frontic.com/DESIGN.md#product-card-horizontal) |
| `image` | [`product-card-horizontal-image`](https://ui.frontic.com/DESIGN.md#product-card-horizontal-image) |
| `part-title` | [`part-title`](https://ui.frontic.com/DESIGN.md#part-title) |
| `hint` | [`hint`](https://ui.frontic.com/DESIGN.md#hint) |
| `focus-ring` | [`focus-ring`](https://ui.frontic.com/DESIGN.md#focus-ring) |

## Source

`components/sections/product-card-horizontal/ProductCardHorizontal.vue`

```vue
<script lang="ts">
/** Shows a product with its image beside its details and price, with an add-to-cart button. */
export default {}
</script>

<script setup lang="ts">
import { CheckIcon, ShoppingBagIcon } from '@lucide/vue'

import type { HTMLAttributes } from 'vue'
import type { Product } from './types'
import { ref } from 'vue'
import { useTimeoutFn } from '@vueuse/core'
import { useProductSibling } from '@library/composables/use-product-sibling'
import { Badge } from '@library/components/ui/badge'
import { Button } from '@library/components/ui/button'
import { Card, CardContent, CardImage } from '@library/components/ui/card'
import { Image } from '@library/components/ui/image'
import { Price } from '@library/components/ui/price'
import { Rating } from '@library/components/ui/rating'
import { SwatchGroup, SwatchGroupItem } from '@library/components/ui/swatch-group'

const props = withDefaults(
  defineProps<{
    /** The product the card shows; its `badge`, `clearout` and `inStore` fields add badges over the image. */
    product: Product
    /** Shown on the button that adds the product to the cart. */
    addToCartLabel?: string
    /** Shown on the add-to-cart button for two seconds after the product was added. */
    addedLabel?: string
    /** Shown on the badge of a product marked `clearout`. */
    clearoutLabel?: string
    /** Shown on the badge of a product that is available in store. */
    inStoreLabel?: string
    /** BCP 47 locale the price is formatted in, e.g. `de-DE`; the ConfigProvider's when left out. */
    locale?: string
    /** Builds the rating's screen-reader name from the score, to one decimal, and the top of the scale; `4.6 out of 5` when left out. */
    ratingLabel?: (score: number, max: number) => string
    /** Bare drops the edge, shadow and inset, for a section placed inside another Card. */
    surface?: 'raised' | 'bare'
    class?: HTMLAttributes['class']
  }>(),
  {
    surface: 'raised',
    addToCartLabel: 'Add to Cart',
    addedLabel: 'Added',
    clearoutLabel: 'Clearout',
    inStoreLabel: 'In Store',
  },
)
const emit = defineEmits<{
  /** Fires with the product when the shopper adds it to the cart. */
  'add-to-cart': [product: Product]
}>()

const { selected: selectedSibling, cover } = useProductSibling(() => props.product)

const added = ref(false)
const { start: revertLater } = useTimeoutFn(() => (added.value = false), 2000, { immediate: false })

function addToCart() {
  emit('add-to-cart', props.product)
  added.value = true
  revertLater()
}
</script>

<template>
  <Card :surface="surface" :class="['fui-product-card-horizontal group @container', props.class]">
    <div class="flex flex-col gap-8 @md:flex-row">
      <CardImage class="relative @md:w-2/5 @md:shrink-0">
        <a :href="product.link?.path ?? '#'" class="group/link relative block h-full outline-none">
          <div data-slot="product-card-horizontal-image" class="fui-product-card-horizontal-image h-full overflow-hidden">
            <Image :src="cover" :alt="product.name" class="aspect-4/3 size-full transition-transform duration-500 ease-out group-hover/link:scale-105 @md:aspect-square" />
          </div>
          <span data-slot="focus-ring" aria-hidden="true" class="pointer-events-none absolute inset-0 z-10 group-focus-visible/link:fui-focus-ring group-focus-visible/link:-outline-offset-3" />
          <div v-if="product.badge || product.clearout || product.inStore" class="absolute top-3 inset-s-3 flex flex-col items-start gap-1">
            <Badge v-if="product.badge" variant="promo">{{ product.badge }}</Badge>
            <Badge v-if="product.clearout" variant="discount">{{ clearoutLabel }}</Badge>
            <Badge v-if="product.inStore" variant="success">{{ inStoreLabel }}</Badge>
          </div>
        </a>
      </CardImage>

      <CardContent class="flex flex-1 flex-col gap-3 pb-8 @md:pt-8 @md:ps-0">
        <div class="flex flex-col gap-1">
          <h3 data-slot="part-title" class="fui-part-title">
            {{ product.name }}
          </h3>
          <Rating v-if="product.rating" :rating="product.rating" :label="ratingLabel" size="sm">
            <template v-if="product.reviewCount !== undefined" #meta>({{ product.reviewCount }})</template>
          </Rating>
        </div>
        <p v-if="product.description" data-slot="hint" class="fui-hint line-clamp-3">
          {{ product.description }}
        </p>
        <SwatchGroup v-if="product.siblings?.items?.length" v-model="selectedSibling" type="single" size="xs" shape="pill" label-class="sr-only">
          <SwatchGroupItem
            v-for="sibling in product.siblings.items.slice(0, 4)"
            :key="sibling.key"
            :value="sibling.key"
            :hex="sibling.colors?.exactColorHex"
            :label="sibling.colors?.exactColorName ?? sibling.name"
          />
        </SwatchGroup>
        <div class="mt-auto flex flex-wrap items-center justify-between gap-3 pt-2">
          <Price size="lg" :price="product.price" :locale="locale" />
          <Button color="buy" class="w-full @md:w-auto" aria-live="polite" @click="addToCart">
            <CheckIcon v-if="added" data-icon />
            <ShoppingBagIcon v-else data-icon />
            {{ added ? addedLabel : addToCartLabel }}
          </Button>
        </div>
      </CardContent>
    </div>
  </Card>
</template>
```

`components/sections/product-card-horizontal/types.ts`

```ts
export type { Product, ProductSibling } from '@library/lib/product'
```
