---
name: cart-summary
title: Cart Summary
url: https://ui.frontic.com/components/sections/cart-summary.md
tier: sections
install: npx @frontic/ui add cart-summary
files:
  - components/sections/cart-summary/CartSummary.vue
  - components/sections/cart-summary/index.ts
  - components/sections/cart-summary/types.ts
registryDependencies:
  - badge
  - button
  - card
  - cart
  - image
  - item
  - price
  - separator
dependencies:
  - "@lucide/vue"
---

# Cart Summary

Cart card with an item count, product lines with images and line totals, subtotal, shipping and savings rows, the total with its tax note, and a secure checkout button.

## Install

```sh
npx @frontic/ui add cart-summary
```

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), [Cart](https://ui.frontic.com/lib/cart.md), [Image](https://ui.frontic.com/components/ui/image.md), [Item](https://ui.frontic.com/components/ui/item.md), [Price](https://ui.frontic.com/components/ui/price.md), [Separator](https://ui.frontic.com/components/ui/separator.md)

npm packages: `@lucide/vue`

## Examples

```vue
<script setup lang="ts">
import { CartSummary, type CartSummaryLine } from '@/components/sections/cart-summary'

const lines: CartSummaryLine[] = [
  {
    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-md">
    <CartSummary :lines="lines" description="Free shipping and returns on all orders." />
  </div>
</template>
```

## API reference

### CartSummary

Shows the cart's lines with subtotal, shipping and total, and a button to check out.

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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `lines` (required) | `CartLine[]` |  | The products in the cart, one row each. |
| `title` | `string` | `'Your cart'` | The card's heading. |
| `description` | `string` |  | Shown under the heading. |
| `currency` | `string` | `'EUR'` | ISO 4217 code for the totals; the first line's currency wins when there is one. |
| `shippingCents` | `number` | `0` | Shipping in minor units; 0 shows the free badge. |
| `countLabel` | `((count: number) => string)`: `(count: number): string` | ``${count} ${count === 1 ? 'item' : 'items'}`` | Builds the badge beside the heading from the number of items in the cart. |
| `quantityLabel` | `((quantity: number) => string)`: `(quantity: number): string` | ``Qty ${quantity}`` | Builds each line's quantity text from its quantity. |
| `subtotalLabel` | `string` | `'Subtotal'` | Shown beside the sum of the lines. |
| `shippingLabel` | `string` | `'Shipping'` | Shown beside the shipping cost. |
| `freeShippingLabel` | `string` | `'Free'` | Shown on the badge in place of the shipping cost when shipping is free. |
| `savingsLabel` | `string` | `'You save'` | Shown beside the amount saved; the row shows only when a line is reduced. |
| `totalLabel` | `string` | `'Total'` | Shown beside the order total. |
| `taxNote` | `string` | `'Incl. VAT'` | Shown under the total's label, e.g. whether VAT is included. |
| `checkoutLabel` | `string` | `'Secure checkout'` | Shown on the button that goes to checkout. |
| `locale` | `string` |  | BCP 47 locale the prices are formatted in, e.g. `de-DE`; the ConfigProvider's 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 |
| --- | --- | --- |
| `checkout` |  | Fires when the shopper clicks the checkout button. |

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

## Exports

```ts
import { type CartSummaryLine } from '@/components/sections/cart-summary'
```

- `CartSummaryLine`: `CartLine`

## Styling

| Part | Recipe |
| --- | --- |
| `hint` | [`hint`](https://ui.frontic.com/DESIGN.md#hint) |
| `heading` | [`heading`](https://ui.frontic.com/DESIGN.md#heading) |
| `meta` | [`meta`](https://ui.frontic.com/DESIGN.md#meta) |

## Source

`components/sections/cart-summary/CartSummary.vue`

```vue
<script lang="ts">
/** Shows the cart's lines with subtotal, shipping and total, and a button to check out. */
export default {}
</script>

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

import type { HTMLAttributes } from 'vue'
import type { PriceType } from '@library/components/ui/price'
import type { CartSummaryLine } from './types'
import { itemCount, lineTotal, savings, subtotal } from '@library/lib/cart'
import { computed } from 'vue'
import { Badge } from '@library/components/ui/badge'
import { Button } from '@library/components/ui/button'
import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@library/components/ui/card'
import { Image } from '@library/components/ui/image'
import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from '@library/components/ui/item'
import { Price } from '@library/components/ui/price'
import { Separator } from '@library/components/ui/separator'

const props = withDefaults(
  defineProps<{
    /** The products in the cart, one row each. */
    lines: CartSummaryLine[]
    /** The card's heading. */
    title?: string
    /** Shown under the heading. */
    description?: string
    /** ISO 4217 code for the totals; the first line's currency wins when there is one. */
    currency?: string
    /** Shipping in minor units; 0 shows the free badge. */
    shippingCents?: number
    /** Builds the badge beside the heading from the number of items in the cart. */
    countLabel?: (count: number) => string
    /** Builds each line's quantity text from its quantity. */
    quantityLabel?: (quantity: number) => string
    /** Shown beside the sum of the lines. */
    subtotalLabel?: string
    /** Shown beside the shipping cost. */
    shippingLabel?: string
    /** Shown on the badge in place of the shipping cost when shipping is free. */
    freeShippingLabel?: string
    /** Shown beside the amount saved; the row shows only when a line is reduced. */
    savingsLabel?: string
    /** Shown beside the order total. */
    totalLabel?: string
    /** Shown under the total's label, e.g. whether VAT is included. */
    taxNote?: string
    /** Shown on the button that goes to checkout. */
    checkoutLabel?: string
    /** BCP 47 locale the prices are formatted in, e.g. `de-DE`; the ConfigProvider's when left out. */
    locale?: string
    /** Bare drops the edge, shadow and inset, for a section placed inside another Card. */
    surface?: 'raised' | 'bare'
    class?: HTMLAttributes['class']
  }>(),
  {
    surface: 'raised',
    title: 'Your cart',
    currency: 'EUR',
    shippingCents: 0,
    countLabel: (count: number) => `${count} ${count === 1 ? 'item' : 'items'}`,
    quantityLabel: (quantity: number) => `Qty ${quantity}`,
    subtotalLabel: 'Subtotal',
    shippingLabel: 'Shipping',
    freeShippingLabel: 'Free',
    savingsLabel: 'You save',
    totalLabel: 'Total',
    taxNote: 'Incl. VAT',
    checkoutLabel: 'Secure checkout',
  },
)
const emit = defineEmits<{
  /** Fires when the shopper clicks the checkout button. */
  checkout: []
}>()

const itemsTotal = computed(() => subtotal(props.lines, props.currency))
const money = (amount: number): PriceType => ({ ...itemsTotal.value, amount })
const count = computed(() => itemCount(props.lines))
const saved = computed(() => savings(props.lines, props.currency).amount)
const total = computed(() => itemsTotal.value.amount + props.shippingCents)
</script>

<template>
  <Card :surface="surface" :class="props.class">
    <CardHeader>
      <CardTitle>{{ title }}</CardTitle>
      <CardDescription v-if="description">{{ description }}</CardDescription>
      <CardAction>
        <Badge variant="primary" size="sm">{{ countLabel(count) }}</Badge>
      </CardAction>
    </CardHeader>
    <CardContent>
      <ItemGroup>
        <Item v-for="line in lines" :key="line.id" size="sm" class="flex-nowrap px-0">
          <ItemMedia v-if="line.image" variant="image">
            <Image :src="line.image" :alt="line.name" />
          </ItemMedia>
          <ItemContent class="min-w-0">
            <ItemTitle>{{ line.name }}</ItemTitle>
            <ItemDescription>
              <template v-if="line.variant">{{ line.variant }} · </template>{{ quantityLabel(line.quantity) }}
            </ItemDescription>
          </ItemContent>
          <ItemActions>
            <Price :price="lineTotal(line)" size="sm" :show-discount="false" :locale="locale" align="end" />
          </ItemActions>
        </Item>
      </ItemGroup>
    </CardContent>
    <CardFooter class="flex-col items-stretch gap-4">
      <Separator />
      <div class="flex flex-col gap-2">
        <div class="flex items-center justify-between">
          <span data-slot="hint" class="fui-hint">{{ subtotalLabel }}</span>
          <Price align="end" :price="itemsTotal" size="sm" :locale="locale" />
        </div>
        <div class="flex items-center justify-between">
          <span data-slot="hint" class="fui-hint">{{ shippingLabel }}</span>
          <Badge v-if="shippingCents === 0" variant="success" size="sm">{{ freeShippingLabel }}</Badge>
          <Price align="end" v-else :price="money(shippingCents)" size="sm" :locale="locale" />
        </div>
        <div v-if="saved > 0" class="flex items-center justify-between">
          <span data-slot="hint" class="fui-hint">{{ savingsLabel }}</span>
          <Price align="end" :price="money(saved)" size="sm" :locale="locale" />
        </div>
      </div>
      <Separator />
      <div class="flex items-baseline justify-between">
        <div class="flex flex-col">
          <span data-slot="heading" class="fui-heading">{{ totalLabel }}</span>
          <span v-if="taxNote" data-slot="meta" class="fui-meta">{{ taxNote }}</span>
        </div>
        <Price align="end" :price="money(total)" size="lg" :locale="locale" />
      </div>
      <Button color="checkout" class="w-full" @click="emit('checkout')">
        <LockIcon data-icon />
        {{ checkoutLabel }}
      </Button>
    </CardFooter>
  </Card>
</template>
```

`components/sections/cart-summary/types.ts`

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

/** A cart line: its unit price's `ref` shows the line's saving. */
export type CartSummaryLine = CartLine
```
