---
name: cart-full
title: Cart Full
url: https://ui.frontic.com/components/templates/cart-full.md
tier: templates
install: npx @frontic/ui add cart-full
files:
  - components/templates/cart-full/CartFull.vue
  - components/templates/cart-full/index.ts
  - components/templates/cart-full/types.ts
registryDependencies:
  - badge
  - button
  - card
  - cart
  - cart-item-detailed
  - empty
  - format
  - price
  - separator
dependencies:
  - "@lucide/vue"
---

# Cart Full

The cart page: editable lines with quantity controls, an order summary and a checkout button.

## Install

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

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), [Cart Item Detailed](https://ui.frontic.com/components/templates/cart-item-detailed.md), [Empty](https://ui.frontic.com/components/ui/empty.md), [Format](https://ui.frontic.com/lib/format.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 { ref } from 'vue'
import { CartFull, type CartLine } from '@/components/templates/cart-full'

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="w-full">
    <CartFull v-model:items="items" />
  </div>
</template>
```

## API reference

### CartFull

Shows the full cart page with editable items, an order summary and a checkout button. Renders a `<div>`.

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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `currency` | `string` | `'EUR'` | ISO 4217 code for the totals; the first item's currency takes precedence. |
| `freeShippingThreshold` | `number` | `10000` | Subtotal in minor units from which shipping is free, exactly at it included. |
| `shippingCost` | `number` | `599` | Shipping charged in cents while the subtotal is at or below the free-shipping threshold. |
| `title` | `((count: number) => string)`: `(count: number): string` | ``Shopping Cart (${count})`` | Builds the page heading from the number of lines in the cart. |
| `summaryTitle` | `string` | `'Order Summary'` | Shown as the heading of the order summary card. |
| `subtotalLabel` | `string` | `'Subtotal'` | Shown next to the subtotal in the order summary. |
| `shippingLabel` | `string` | `'Shipping'` | Shown next to the shipping cost in the order summary. |
| `freeShippingLabel` | `string` | `'Free'` | Shown in the badge that replaces the shipping cost once shipping is free. |
| `savingsLabel` | `string` | `'You save'` | Shown next to the amount saved on reduced items; the row hides when nothing is saved. |
| `totalLabel` | `string` | `'Total'` | Shown next to the order total. |
| `taxNote` | `string` | `'Incl. VAT'` | Shown under the total's label, e.g. `Incl. VAT`; hidden when empty. |
| `checkoutLabel` | `string` | `'Checkout'` | Shown on the button that starts checkout. |
| `continueLabel` | `string` | `'Continue Shopping'` | Shown on the link under checkout and on the empty state's button; both emit `continue-shopping`. |
| `emptyTitle` | `string` | `'Your cart is empty'` | Shown as the heading when the cart is empty. |
| `emptyText` | `string` | `'Add some items to get started.'` | Shown under the heading when the cart is empty. |
| `maxQuantity` | `number` |  | Highest quantity a shopper can set on one item. |
| `removeLabel` | `((name: string) => string)`: `(name: string): string` |  | Builds the screen-reader name of an item's remove button from the item's name. |
| `decreaseLabel` | `string` |  | Screen-reader name of the button that lowers an item's quantity. |
| `increaseLabel` | `string` |  | Screen-reader name of the button that raises an item's quantity. |
| `eachLabel` | `((price: string) => string)`: `(price: string): string` |  | 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 |
| --- | --- | --- |
| `checkout` | `items: CartLine[]` | Fires with the cart lines when the shopper clicks the checkout button. |
| `update:items` | `value: CartLine[]` | Fires with the new value when `items` changes; `v-model:items` binds it. |
| `continue-shopping` |  | Fires when the shopper chooses to continue shopping. |

## Exports

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

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

## Styling

| Part | Recipe |
| --- | --- |
| `default` | [`cart-full`](https://ui.frontic.com/DESIGN.md#cart-full) |
| `page-title` | [`page-title`](https://ui.frontic.com/DESIGN.md#page-title) |
| `hint` | [`hint`](https://ui.frontic.com/DESIGN.md#hint) |
| `meta` | [`meta`](https://ui.frontic.com/DESIGN.md#meta) |
| `total` | [`total`](https://ui.frontic.com/DESIGN.md#total) |

## Source

`components/templates/cart-full/CartFull.vue`

```vue
<script lang="ts">
/** Shows the full cart page with editable items, an order summary and a checkout button. */
export default {}
</script>

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

import type { HTMLAttributes } from 'vue'
import type { CartLine } from './types'
import { savings, shippingFor, subtotal } from '@library/lib/cart'
import type { PriceType } from '@library/lib/format'
import { computed } from 'vue'
import { Badge } from '@library/components/ui/badge'
import { Button } from '@library/components/ui/button'
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@library/components/ui/empty'
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@library/components/ui/card'
import { Price } from '@library/components/ui/price'
import { Separator } from '@library/components/ui/separator'
import { CartItemDetailed } from '@library/components/templates/cart-item-detailed'

const props = withDefaults(
  defineProps<{
    /** ISO 4217 code for the totals; the first item's currency takes precedence. */
    currency?: string
    /** Subtotal in minor units from which shipping is free, exactly at it included. */
    freeShippingThreshold?: number
    /** Shipping charged in cents while the subtotal is at or below the free-shipping threshold. */
    shippingCost?: number
    /** Builds the page heading from the number of lines in the cart. */
    title?: (count: number) => string
    /** Shown as the heading of the order summary card. */
    summaryTitle?: string
    /** Shown next to the subtotal in the order summary. */
    subtotalLabel?: string
    /** Shown next to the shipping cost in the order summary. */
    shippingLabel?: string
    /** Shown in the badge that replaces the shipping cost once shipping is free. */
    freeShippingLabel?: string
    /** Shown next to the amount saved on reduced items; the row hides when nothing is saved. */
    savingsLabel?: string
    /** Shown next to the order total. */
    totalLabel?: string
    /** Shown under the total's label, e.g. `Incl. VAT`; hidden when empty. */
    taxNote?: string
    /** Shown on the button that starts checkout. */
    checkoutLabel?: string
    /** Shown on the link under checkout and on the empty state's button; both emit `continue-shopping`. */
    continueLabel?: string
    /** Shown as the heading when the cart is empty. */
    emptyTitle?: string
    /** Shown under the heading when the cart is empty. */
    emptyText?: string
    /** 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']
  }>(),
  {
    currency: 'EUR',
    freeShippingThreshold: 10000,
    shippingCost: 599,
    title: (count: number) => `Shopping Cart (${count})`,
    summaryTitle: 'Order Summary',
    subtotalLabel: 'Subtotal',
    shippingLabel: 'Shipping',
    freeShippingLabel: 'Free',
    savingsLabel: 'You save',
    totalLabel: 'Total',
    taxNote: 'Incl. VAT',
    checkoutLabel: 'Checkout',
    continueLabel: 'Continue Shopping',
    emptyTitle: 'Your cart is empty',
    emptyText: 'Add some items to get started.',
  },
)
const emit = defineEmits<{
  /** Fires with the cart lines when the shopper clicks the checkout button. */
  checkout: [items: CartLine[]]
  /** Fires when the shopper chooses to continue shopping. */
  'continue-shopping': []
}>()
/** The cart's lines; quantity changes and removals update it. */
const items = defineModel<CartLine[]>('items', { default: () => [] })

const itemsTotal = computed(() => subtotal(items.value, props.currency))
const money = (amount: number): PriceType => ({ ...itemsTotal.value, amount })
const saved = computed(() => savings(items.value, props.currency).amount)
const shipping = computed(() => shippingFor(itemsTotal.value, props.shippingCost, props.freeShippingThreshold).amount)
const total = computed(() => itemsTotal.value.amount + shipping.value)
</script>

<template>
  <div :class="['fui-cart-full @container', props.class]">
    <h1 data-slot="page-title" class="fui-page-title">{{ title(items.length) }}</h1>

    <div v-if="items.length" class="flex flex-col gap-6 @3xl:flex-row">
      <CartItemDetailed
        v-model:items="items"
        :max-quantity="maxQuantity"
        :remove-label="removeLabel"
        :decrease-label="decreaseLabel"
        :increase-label="increaseLabel"
        :each-label="eachLabel"
        :locale="locale"
        class="min-w-0 flex-1"
      />

      <Card class="h-fit shrink-0 @3xl:w-80">
        <CardHeader>
          <CardTitle>{{ summaryTitle }}</CardTitle>
        </CardHeader>
        <CardContent class="space-y-3">
          <div class="flex justify-between">
            <span data-slot="hint" class="fui-hint">{{ subtotalLabel }}</span>
            <Price align="end" :price="itemsTotal" :locale="locale" />
          </div>
          <div class="flex justify-between">
            <span data-slot="hint" class="fui-hint">{{ shippingLabel }}</span>
            <Badge v-if="shipping === 0" variant="success" size="sm">{{ freeShippingLabel }}</Badge>
            <Price align="end" v-else :price="money(shipping)" :locale="locale" />
          </div>
          <div v-if="saved > 0" class="flex justify-between">
            <span data-slot="hint" class="fui-hint">{{ savingsLabel }}</span>
            <Price align="end" :price="money(saved)" :locale="locale" />
          </div>
          <div data-slot="total" class="fui-total">
            <Separator />
            <div class="flex justify-between pt-3">
              <div class="flex flex-col">
                <span>{{ totalLabel }}</span>
                <span v-if="taxNote" data-slot="meta" class="fui-meta">{{ taxNote }}</span>
              </div>
              <Price align="end" size="lg" :price="money(total)" :locale="locale" />
            </div>
          </div>
        </CardContent>
        <CardFooter class="flex-col gap-2">
          <Button color="checkout" class="w-full" @click="emit('checkout', items)">
            <LockIcon data-icon />
            {{ checkoutLabel }}
          </Button>
          <Button variant="link" class="w-full" @click="emit('continue-shopping')">{{ continueLabel }}</Button>
        </CardFooter>
      </Card>
    </div>

    <Empty v-else>
      <EmptyHeader>
        <EmptyMedia variant="icon"><ShoppingBagIcon data-icon /></EmptyMedia>
        <EmptyTitle>{{ emptyTitle }}</EmptyTitle>
        <EmptyDescription>{{ emptyText }}</EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button @click="emit('continue-shopping')">{{ continueLabel }}</Button>
      </EmptyContent>
    </Empty>
  </div>
</template>
```

`components/templates/cart-full/types.ts`

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