---
name: cart-minimal
title: Cart Minimal
url: https://ui.frontic.com/components/templates/cart-minimal.md
tier: templates
install: npx @frontic/ui add cart-minimal
files:
  - components/templates/cart-minimal/CartMinimal.vue
  - components/templates/cart-minimal/index.ts
  - components/templates/cart-minimal/types.ts
registryDependencies:
  - badge
  - button
  - cart
  - cart-item-minimal
  - empty
  - popover
  - price
  - separator
dependencies:
  - "@lucide/vue"
---

# Cart Minimal

A mini cart in a popover with the first few items, the subtotal and checkout.

## Install

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

Built from: [Badge](https://ui.frontic.com/components/ui/badge.md), [Button](https://ui.frontic.com/components/ui/button.md), [Cart](https://ui.frontic.com/lib/cart.md), [Cart Item Minimal](https://ui.frontic.com/components/templates/cart-item-minimal.md), [Empty](https://ui.frontic.com/components/ui/empty.md), [Popover](https://ui.frontic.com/components/ui/popover.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 { CartMinimal, type CartLine } from '@/components/templates/cart-minimal'

const open = ref(true)

const items: 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',
  },
  {
    id: 'northern-light-hybrid-jacket',
    name: 'Northern Light Hybrid Jacket',
    variant: 'Lavender · S',
    price: { amount: 11500, currency: 'EUR' },
    quantity: 1,
    image: 'https://static.frontic.com/cdn-cgi/image/width=640,format=auto/ui/products/northern-light-hybrid-jacket-lavender.jpg',
  },
]
</script>

<template>
  <div class="flex min-h-150 justify-end">
    <CartMinimal v-model:open="open" :items="items" />
  </div>
</template>
```

## API reference

### CartMinimal

Opens a popover with the cart's first items, subtotal and checkout.

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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `CartLine[]` | `[]` | The cart's lines; the popover lists the first few. |
| `currency` | `string` | `'EUR'` | ISO 4217 code for the totals; the first item's currency takes precedence. |
| `maxVisibleItems` | `number` | `3` | How many items the popover lists before summing up the rest in one line. |
| `title` | `((count: number) => string)`: `(count: number): string` | ``Cart (${count})`` | Builds the popover's heading from the total quantity in the cart. |
| `openLabel` | `string` | `'Open cart'` | Screen-reader name of the cart button that opens the popover. |
| `countLabel` | `((count: number) => string)`: `(count: number): string` | `count > 9 ? '9+' : String(count)` | Builds the badge on the cart button from the total quantity, e.g. `9+`. |
| `moreLabel` | `((count: number) => string)`: `(count: number): string` | ``+ ${count} more ${count === 1 ? 'item' : 'items'}`` | Builds the line under the list from the number of items left out, e.g. `+ 2 more items`. |
| `subtotalLabel` | `string` | `'Subtotal'` | Shown next to the subtotal in the popover. |
| `taxNote` | `string` | `'Shipping and taxes calculated at checkout.'` | Shown under the subtotal, e.g. that shipping and taxes follow at checkout. |
| `checkoutLabel` | `string` | `'Checkout'` | Shown on the button that starts checkout. |
| `viewCartLabel` | `string` | `'View Cart'` | Shown on the button that emits `view-cart` to open the full cart. |
| `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. |
| `quantityLabel` | `((quantity: number) => string)`: `(quantity: number): string` |  | Builds the quantity text next to the variant, e.g. `Qty 2`. |
| `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. |
| `open` | `boolean` | `false` | Whether the cart popover is open. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:open` | `value: boolean` | Fires with the new value when `open` changes; `v-model:open` binds it. |
| `checkout` | `items: CartLine[]` | Fires with the cart lines when the shopper clicks the checkout button. |
| `view-cart` |  | Fires when the shopper clicks the button to view the full cart. |

## Exports

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

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

## Styling

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

## Source

`components/templates/cart-minimal/CartMinimal.vue`

```vue
<script lang="ts">
/** Opens a popover with the cart's first items, subtotal and checkout. */
export default {}
</script>

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

import type { HTMLAttributes } from 'vue'
import type { CartLine } from './types'
import { itemCount, subtotal } from '@library/lib/cart'
import { computed } from 'vue'
import { Badge } from '@library/components/ui/badge'
import { Button } from '@library/components/ui/button'
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@library/components/ui/empty'
import { Popover, PopoverContent, PopoverTrigger } from '@library/components/ui/popover'
import { Price } from '@library/components/ui/price'
import { Separator } from '@library/components/ui/separator'
import { CartItemMinimal } from '@library/components/templates/cart-item-minimal'

const props = withDefaults(
  defineProps<{
    /** The cart's lines; the popover lists the first few. */
    items?: CartLine[]
    /** ISO 4217 code for the totals; the first item's currency takes precedence. */
    currency?: string
    /** How many items the popover lists before summing up the rest in one line. */
    maxVisibleItems?: number
    /** Builds the popover's heading from the total quantity in the cart. */
    title?: (count: number) => string
    /** Screen-reader name of the cart button that opens the popover. */
    openLabel?: string
    /** Builds the badge on the cart button from the total quantity, e.g. `9+`. */
    countLabel?: (count: number) => string
    /** Builds the line under the list from the number of items left out, e.g. `+ 2 more items`. */
    moreLabel?: (count: number) => string
    /** Shown next to the subtotal in the popover. */
    subtotalLabel?: string
    /** Shown under the subtotal, e.g. that shipping and taxes follow at checkout. */
    taxNote?: string
    /** Shown on the button that starts checkout. */
    checkoutLabel?: string
    /** Shown on the button that emits `view-cart` to open the full cart. */
    viewCartLabel?: string
    /** Shown as the heading when the cart is empty. */
    emptyTitle?: string
    /** Shown under the heading when the cart is empty. */
    emptyText?: string
    /** Builds the quantity text next to the variant, e.g. `Qty 2`. */
    quantityLabel?: (quantity: number) => string
    /** BCP 47 locale the prices are formatted in; the ConfigProvider's when left out. */
    locale?: string
    class?: HTMLAttributes['class']
  }>(),
  {
    items: () => [],
    currency: 'EUR',
    maxVisibleItems: 3,
    title: (count: number) => `Cart (${count})`,
    openLabel: 'Open cart',
    countLabel: (count: number) => (count > 9 ? '9+' : String(count)),
    moreLabel: (count: number) => `+ ${count} more ${count === 1 ? 'item' : 'items'}`,
    subtotalLabel: 'Subtotal',
    taxNote: 'Shipping and taxes calculated at checkout.',
    checkoutLabel: 'Checkout',
    viewCartLabel: 'View Cart',
    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 clicks the button to view the full cart. */
  'view-cart': []
}>()
/** Whether the cart popover is open. */
const open = defineModel<boolean>('open', { default: false })

const count = computed(() => itemCount(props.items))
const itemsTotal = computed(() => subtotal(props.items, props.currency))
const visible = computed(() => props.items.slice(0, props.maxVisibleItems))
const hidden = computed(() => Math.max(props.items.length - props.maxVisibleItems, 0))
</script>

<template>
  <Popover v-model:open="open">
    <PopoverTrigger as-child>
      <Button variant="ghost" color="neutral" size="icon-md" :aria-label="openLabel" :class="['relative', props.class]">
        <ShoppingBagIcon class="size-5" />
        <Badge v-if="count > 0" variant="primary" size="sm" class="absolute -top-1 -inset-e-1">{{ countLabel(count) }}</Badge>
      </Button>
    </PopoverTrigger>

    <PopoverContent align="end" class="@container w-80 p-0">
      <div class="flex items-center justify-between px-4 pt-4 pb-2">
        <span data-slot="cart-minimal-title" class="fui-cart-minimal-title">{{ title(count) }}</span>
      </div>

      <div v-if="items.length" class="flex flex-col gap-3 px-4 pb-3">
        <CartItemMinimal :items="visible" :quantity-label="quantityLabel" :locale="locale" />
        <p v-if="hidden > 0" data-slot="meta" class="fui-meta">{{ moreLabel(hidden) }}</p>
      </div>

      <Empty v-else>
        <EmptyHeader>
          <EmptyMedia variant="icon"><ShoppingBagIcon data-icon /></EmptyMedia>
          <EmptyTitle>{{ emptyTitle }}</EmptyTitle>
          <EmptyDescription>{{ emptyText }}</EmptyDescription>
        </EmptyHeader>
      </Empty>

      <template v-if="items.length">
        <Separator />
        <div class="flex flex-col gap-2 p-4">
          <div class="flex items-center justify-between">
            <span data-slot="hint" class="fui-hint">{{ subtotalLabel }}</span>
            <Price align="end" :price="itemsTotal" :locale="locale" />
          </div>
          <p data-slot="meta" class="fui-meta">{{ taxNote }}</p>
          <div class="mt-1 flex flex-col gap-2">
            <Button color="checkout" size="sm" class="w-full" @click="emit('checkout', items)">
              <LockIcon data-icon />
              {{ checkoutLabel }}
            </Button>
            <Button variant="outline" color="neutral" size="sm" class="w-full" @click="emit('view-cart')">{{ viewCartLabel }}</Button>
          </div>
        </div>
      </template>
    </PopoverContent>
  </Popover>
</template>
```

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

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