---
name: cart-item-detailed
title: Cart Item Detailed
url: https://ui.frontic.com/components/templates/cart-item-detailed.md
tier: templates
install: npx @frontic/ui add cart-item-detailed
files:
  - components/templates/cart-item-detailed/CartItemDetailed.vue
  - components/templates/cart-item-detailed/index.ts
  - components/templates/cart-item-detailed/types.ts
registryDependencies:
  - button
  - cart
  - format
  - image
  - price
  - separator
dependencies:
  - "@lucide/vue"
  - reka-ui
---

# Cart Item Detailed

Cart lines with large thumbnails, the line total with any saving, quantity buttons and a remove action.

## Install

```sh
npx @frontic/ui add cart-item-detailed
```

Built from: [Button](https://ui.frontic.com/components/ui/button.md), [Cart](https://ui.frontic.com/lib/cart.md), [Format](https://ui.frontic.com/lib/format.md), [Image](https://ui.frontic.com/components/ui/image.md), [Price](https://ui.frontic.com/components/ui/price.md), [Separator](https://ui.frontic.com/components/ui/separator.md)

Used by: [Cart Full](https://ui.frontic.com/components/templates/cart-full.md)

npm packages: `@lucide/vue`, `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { CartItemDetailed, type CartLine } from '@/components/templates/cart-item-detailed'

const items = ref<CartLine[]>([
  {
    id: 'frost-escape-parka',
    name: 'Frost Escape Parka',
    variant: 'Sun yellow · M',
    price: { amount: 14900, currency: 'EUR' },
    quantity: 1,
    image: 'https://static.frontic.com/cdn-cgi/image/width=640,format=auto/ui/products/frost-escape-parka.jpg',
  },
  {
    id: 'snowflake-ski-goggles',
    name: 'Snowflake Ski Goggles',
    variant: 'Mint · One size',
    price: { amount: 12700, currency: 'EUR', ref: 15900 },
    quantity: 2,
    image: 'https://static.frontic.com/cdn-cgi/image/width=640,format=auto/ui/products/snowflake-ski-goggles.jpg',
  },
  {
    id: 'trailmaster-22',
    name: 'TrailMaster 22',
    variant: 'Brown · 20L',
    price: { amount: 14100, currency: 'EUR' },
    quantity: 1,
    image: 'https://static.frontic.com/cdn-cgi/image/width=640,format=auto/ui/products/trailmaster-22.jpg',
  },
])
</script>

<template>
  <div class="mx-auto w-full max-w-xl">
    <CartItemDetailed v-model:items="items" />
  </div>
</template>
```

## API reference

### CartItemDetailed

Lists the cart's items with price per unit, a quantity stepper and a remove button.

Styling: `cart-item-detailed-image`, which extends [`thumbnail`](https://ui.frontic.com/DESIGN.md#thumbnail); [`section-title`](https://ui.frontic.com/DESIGN.md#section-title); [`meta`](https://ui.frontic.com/DESIGN.md#meta); [`quiet-action`](https://ui.frontic.com/DESIGN.md#quiet-action). Change them there and every part that extends them follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `maxQuantity` | `number` | `10` | Highest quantity a shopper can set on one item. |
| `removeLabel` | `((name: string) => string)`: `(name: string): string` | ``Remove ${name}`` | Builds the screen-reader name of an item's remove button from the item's name. |
| `decreaseLabel` | `string` | `'Decrease quantity'` | Screen-reader name of the button that lowers an item's quantity. |
| `increaseLabel` | `string` | `'Increase quantity'` | Screen-reader name of the button that raises an item's quantity. |
| `eachLabel` | `((price: string) => string)`: `(price: string): string` | ``${price} each`` | Builds the unit-price line from the formatted price; shows when an item's quantity is above 1. |
| `locale` | `string` |  | BCP 47 locale the prices are formatted in; the ConfigProvider's when left out. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `items` | `CartLine[]` | `[]` | The cart's lines; quantity changes and removals update it. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:items` | `value: CartLine[]` | Fires with the new value when `items` changes; `v-model:items` binds it. |

## Exports

```ts
import { type CartLine } from '@/components/templates/cart-item-detailed'
```

- `CartLine`: from [Cart](https://ui.frontic.com/lib/cart.md)

## Styling

| Part | Recipe |
| --- | --- |
| `default` | [`cart-item-detailed`](https://ui.frontic.com/DESIGN.md#cart-item-detailed) |
| `line` | [`cart-item-detailed-line`](https://ui.frontic.com/DESIGN.md#cart-item-detailed-line) |
| `image` | [`cart-item-detailed-image`](https://ui.frontic.com/DESIGN.md#cart-item-detailed-image) |
| `quantity` | [`cart-item-detailed-quantity`](https://ui.frontic.com/DESIGN.md#cart-item-detailed-quantity) |
| `section-title` | [`section-title`](https://ui.frontic.com/DESIGN.md#section-title) |
| `meta` | [`meta`](https://ui.frontic.com/DESIGN.md#meta) |
| `quiet-action` | [`quiet-action`](https://ui.frontic.com/DESIGN.md#quiet-action) |

## Source

`components/templates/cart-item-detailed/CartItemDetailed.vue`

```vue
<script lang="ts">
/** Lists the cart's items with price per unit, a quantity stepper and a remove button. */
export default {}
</script>

<script setup lang="ts">
import { MinusIcon, PlusIcon, Trash2Icon } from '@lucide/vue'

import type { HTMLAttributes } from 'vue'
import type { CartLine } from './types'
import { clampQuantity, lineTotal } from '@library/lib/cart'
import { Button } from '@library/components/ui/button'
import { Image } from '@library/components/ui/image'
import { formatPrice } from '@library/lib/format'
import { Price } from '@library/components/ui/price'
import { Separator } from '@library/components/ui/separator'
import { useLocale } from 'reka-ui'
import { toRef } from 'vue'

const props = withDefaults(
  defineProps<{
    /** Highest quantity a shopper can set on one item. */
    maxQuantity?: number
    /** Builds the screen-reader name of an item's remove button from the item's name. */
    removeLabel?: (name: string) => string
    /** Screen-reader name of the button that lowers an item's quantity. */
    decreaseLabel?: string
    /** Screen-reader name of the button that raises an item's quantity. */
    increaseLabel?: string
    /** Builds the unit-price line from the formatted price; shows when an item's quantity is above 1. */
    eachLabel?: (price: string) => string
    /** BCP 47 locale the prices are formatted in; the ConfigProvider's when left out. */
    locale?: string
    class?: HTMLAttributes['class']
  }>(),
  {
    maxQuantity: 10,
    removeLabel: (name: string) => `Remove ${name}`,
    decreaseLabel: 'Decrease quantity',
    increaseLabel: 'Increase quantity',
    eachLabel: (price: string) => `${price} each`,
  },
)
const locale = useLocale(toRef(() => props.locale))
/** The cart's lines; quantity changes and removals update it. */
const items = defineModel<CartLine[]>('items', { default: () => [] })

function setQuantity(id: string, quantity: number) {
  items.value = items.value.map((line) => (line.id === id ? { ...line, quantity: clampQuantity(quantity, props.maxQuantity) } : line))
}

function remove(id: string) {
  items.value = items.value.filter((line) => line.id !== id)
}
</script>

<template>
  <TransitionGroup
    tag="ul"
    role="list"
    :class="['fui-cart-item-detailed', props.class]"
    leave-active-class="transition-opacity duration-200 ease-out"
    leave-to-class="opacity-0"
    move-class="transition-transform duration-300 ease-out"
  >
    <li v-for="(line, index) in items" :key="line.id" class="flex flex-col gap-5">
      <Separator v-if="index > 0" />
      <div data-slot="cart-item-detailed-line" class="fui-cart-item-detailed-line flex">
        <div data-slot="cart-item-detailed-image" class="fui-cart-item-detailed-image shrink-0 overflow-hidden">
          <Image :src="line.image ?? '/placeholder.svg'" :alt="line.name" class="size-full object-cover" />
        </div>
        <div class="flex min-w-0 flex-1 flex-col justify-between gap-3">
          <div class="flex items-start justify-between gap-4">
            <div class="flex min-w-0 flex-col gap-1">
              <h3 data-slot="section-title" class="fui-section-title">{{ line.name }}</h3>
              <p v-if="line.variant" data-slot="meta" class="fui-meta">{{ line.variant }}</p>
              <p v-if="line.quantity > 1" data-slot="meta" class="fui-meta tabular-nums">{{ eachLabel(formatPrice(line.price, locale)) }}</p>
            </div>
            <Price size="sm" :price="lineTotal(line)" :locale="locale" :show-discount="false" align="end" class="shrink-0" />
          </div>
          <div class="flex items-center justify-between gap-4">
            <div class="flex items-center gap-1">
              <Button variant="outline" color="neutral" size="icon-sm" :aria-label="decreaseLabel" :disabled="line.quantity <= 1" @click="setQuantity(line.id, line.quantity - 1)">
                <MinusIcon data-icon />
              </Button>
              <span data-slot="cart-item-detailed-quantity" class="fui-cart-item-detailed-quantity text-center tabular-nums" aria-live="polite">{{ line.quantity }}</span>
              <Button variant="outline" color="neutral" size="icon-sm" :aria-label="increaseLabel" :disabled="line.quantity >= maxQuantity" @click="setQuantity(line.id, line.quantity + 1)">
                <PlusIcon data-icon />
              </Button>
            </div>
            <Button variant="ghost" color="neutral" size="icon-sm" data-slot="quiet-action" class="fui-quiet-action" :aria-label="removeLabel(line.name)" @click="remove(line.id)">
              <Trash2Icon data-icon />
            </Button>
          </div>
        </div>
      </div>
    </li>
  </TransitionGroup>
</template>
```

`components/templates/cart-item-detailed/types.ts`

```ts
export type { CartLine } from '@library/lib/cart'
```
