---
title: Cart List
description: The cart — a semantic list of lines, each with a thumbnail, a price, a quantity stepper and a line total.
category: Display
related:
  - /components/product-card
  - /components/price
  - /components/number-input
  - /components/image
visibility: guest
draft: false
---

# Cart List

A cart is a list, and saying so out loud is most of this component's job.

`<x-wirekit::cart-list>` is the list; `<x-wirekit::cart-item>` is one line in it. The line
composes pieces you already know — [image](/components/image), [price](/components/price),
[number-input](/components/number-input) and [button](/components/button) — so nothing here
behaves differently from the way it behaves everywhere else.

:::preview{title="A cart with three lines"}
<x-wirekit::cart-list>
    <x-wirekit::cart-item
        name="Brass desk lamp"
        image="/placeholder/400x400?bg=fde68a&fg=78350f&label=Lamp&gradient=none"
        :price="3900"
        :compare-at="5900"
        :minor-units="true"
        currency="EUR"
        :quantity="1"
        :line-total="3900"
    >
        Warm white · Dimmable
    </x-wirekit::cart-item>
    <x-wirekit::cart-item
        name="Walnut notebook"
        image="/placeholder/400x400?bg=d6d3d1&fg=1c1917&label=Notebook&gradient=none"
        :price="1200"
        :minor-units="true"
        currency="EUR"
        :quantity="2"
        :line-total="2400"
    >
        A5 · Dotted
    </x-wirekit::cart-item>
    <x-wirekit::cart-item
        name="Merino crew"
        image="/placeholder/400x400?bg=cbd5e1&fg=0f172a&label=Crew&gradient=none"
        :price="8900"
        :minor-units="true"
        currency="EUR"
        :quantity="1"
        :line-total="8900"
        :max="3"
    >
        Charcoal · M
    </x-wirekit::cart-item>
</x-wirekit::cart-list>
:::

## Why the list is a component

A `<ul>` you style yourself is one line of markup, and it misses four things you would not think
to write. The list carries them:

- **`role="list"`.** Removing the bullet markers removes the list semantics in WebKit — so a
  cart styled with `list-none` is announced as a run of text on every iPhone, and the page
  looks completely normal while it happens. The explicit role puts the semantics back.
- **A name.** A checkout page usually holds more than one list. Without a name each is
  announced as "list, 3 items" and they cannot be told apart.
- **The separator.** `divide-y` draws a rule between lines and none after the last one. A line
  cannot draw that for itself — it has no way to know it is the last, and CSS has no
  previous-sibling combinator to tell it.
- **A prose reset.** Carts are often rendered inside prose-styled containers, which add list
  indentation and markers back.

## The totals are the application's

::: warning
`cart-item` never multiplies quantity by price. You pass `line-total`, and you pass a new one
when the quantity changes.

That is deliberate. A component that computed the total would own cart state — and cart state
is where tax rules, tiered pricing, bundle discounts and currency rounding live, none of which
belong in a UI library. Every application that got a computed total would then have to take it
back out.
:::

## Sales

Pass `compare-at` and the previous price is struck through, exactly as on
[product-card](/components/product-card). The strike only appears when the compare-at price is
genuinely higher — passing a lower one draws a discount that does not exist, so it is ignored.

:::preview{title="A line on sale"}
<x-wirekit::cart-list label="Sale items">
    <x-wirekit::cart-item
        name="Brass desk lamp"
        :price="3900"
        :compare-at="5900"
        :minor-units="true"
        currency="EUR"
        :line-total="3900"
    />
</x-wirekit::cart-list>
:::

## Quantity

The stepper is [number-input](/components/number-input). `min` defaults to 1, because a cart
line holding none of something is a removal rather than a quantity. Pass `max` when you know
the stock ceiling, and `unit` when the thing is not counted in pieces.

Bind the quantity by writing the binding on the line, `wire:model` in any of its forms, such as
`wire:model.live` or `wire:model.blur`, or `x-model`. It goes to the stepper and to nothing else.

:::preview{title="Stock ceiling and a unit suffix"}
<x-wirekit::cart-list label="Quantities">
    <x-wirekit::cart-item name="Merino crew" :price="8900" :minor-units="true" currency="EUR" :quantity="3" :max="3" :line-total="26700" />
    <x-wirekit::cart-item name="Loose-leaf tea" :price="1450" :minor-units="true" currency="EUR" :quantity="2" unit="kg" :step="1" :line-total="2900" />
</x-wirekit::cart-list>
:::

::: info
The stepper renders at `size="lg"` rather than the usual `md`, and that is measured rather than
chosen by eye. `--size-wk-md` clears WCAG 2.2 SC 2.5.8 (24px, level AA) but misses the 44px of
SC 2.5.5 (level AAA) and of platform touch guidance; `--size-wk-lg` clears both. Raising the
token instead would move every component that reads it for one ecommerce case, so the size
lives on this component and you can still override it per line.
:::

## Removing a line

Every line ships a remove control unless you pass `:removable="false"`. It dispatches a
browser event carrying the line's key, so a Livewire component can listen for it without this
library knowing anything about your cart.

```blade
<div x-on:wirekit:cart-remove="$wire.remove($event.detail.item)">
    <x-wirekit::cart-list>
        @foreach ($lines as $line)
            <x-wirekit::cart-item :id="$line->id" :name="$line->name" :price="$line->price" :line-total="$line->total" />
        @endforeach
    </x-wirekit::cart-list>
</div>
```

::: warning
Give each line an `id`. Without one the key is derived from the product name, which is stable
enough for a demo and wrong the moment two lines share a name — the same shirt in two sizes is
the ordinary case, not the edge case.

The events carry that key as it is. The row also uses it as its HTML `id`, and when a second cart
on the page shows the same line, a mini cart beside the cart page, that row's `id` gets `-2`
after it, so the two rows never share one.
:::

Replace the control entirely with the `remove` slot when you need a different affordance — a
"Save for later" link, a confirmation dialog, an undo row.

:::preview{title="A line that cannot be removed, and one with a custom control"}
<x-wirekit::cart-list label="Remove controls">
    <x-wirekit::cart-item name="Gift wrapping" :price="500" :minor-units="true" currency="EUR" :quantity="1" :line-total="500" :removable="false" />
    <x-wirekit::cart-item name="Merino crew" :price="8900" :minor-units="true" currency="EUR" :quantity="1" :line-total="8900">
        <x-slot:remove>
            <x-wirekit::button intent="neutral" surface="ghost" size="sm">Save for later</x-wirekit::button>
        </x-slot:remove>
    </x-wirekit::cart-item>
</x-wirekit::cart-list>
:::

## Wiring It to Livewire

The cart lives in your Livewire component: the lines, their quantities and what to do on a
removal. The component below keeps the lines in an array keyed by something stable, with the
prices in cents:

```php
<?php

namespace App\Livewire;

use Livewire\Component;

class Cart extends Component
{
    // 1. One entry per line. The key identifies the line when it is removed.
    public array $lines = [
        'lamp-warm-white' => ['name' => 'Brass desk lamp', 'price' => 3900, 'quantity' => 1],
        'notebook-a5' => ['name' => 'Walnut notebook', 'price' => 1200, 'quantity' => 2],
    ];

    // 2. The remove control sends the line's key; taking the line out is all there is to do.
    public function remove(string $item): void
    {
        unset($this->lines[$item]);
    }
}
```

The view binds each quantity and computes each line total, so a changed quantity renders a new
total on the next request:

```blade
{{-- resources/views/livewire/cart.blade.php --}}

{{-- 1. One listener around the list hears the remove control of every line. --}}
<div x-on:wirekit:cart-remove="$wire.remove($event.detail.item)">
    <x-wirekit::cart-list>
        @foreach ($lines as $key => $line)
            {{-- 2. `id` is the key the remove control sends; `wire:model` reaches the stepper. --}}
            <x-wirekit::cart-item
                wire:key="{{ $key }}"
                :id="$key"
                wire:model.live="lines.{{ $key }}.quantity"
                :name="$line['name']"
                :price="$line['price']"
                :minor-units="true"
                currency="EUR"
                :quantity="(int) $line['quantity']"
                :line-total="$line['price'] * (int) $line['quantity']"
            />
        @endforeach
    </x-wirekit::cart-list>
</div>
```

A field someone empties sends an empty string, and `(int)` reads it as 0, so the line costs
nothing until a quantity is typed. Validate the quantity in your component when an order is placed
from it; the stepper's `min` and `max` guide a person but do not stop a request.

## A line that is only shown

A cart someone looks at but does not change, such as the customer-facing screen of a till or an
order confirmation, takes `readonly`. The quantity reads as text with its unit, and the
built-in remove control goes: a stepper on a screen nobody touches looks operable and does
nothing. A `remove` slot you pass still renders.

Where another device shows the quantity, such as the weight on a scale beside the till, add
`hide-quantity` and the line leaves it out, the visually hidden word "Quantity" with it: a "1"
beside a weighed article would read as one piece or one kilogram.

Two more things belong on such a line. `unitPrice` and `unitMeasure` add the unit price a
packaged or measured article has to show beside its price, and the `details` slot takes further
lines about the line, such as what it saves or the promotion it is part of.

:::preview{title="A cart that is only shown"}
<x-wirekit::cart-list label="Your purchase">
    <x-wirekit::cart-item name="Olive oil, 500 ml" :price="749" :compare-at="799" :unit-price="1498" unit-measure="1 l" :minor-units="true" currency="EUR" :quantity="2" :line-total="1498" readonly>
        <x-slot:details>You save €1.00 · Spring offer</x-slot:details>
    </x-wirekit::cart-item>
    <x-wirekit::cart-item name="Coffee beans" :price="1598" :minor-units="true" currency="EUR" :quantity="0.532" unit="kg" :line-total="850" readonly />
</x-wirekit::cart-list>
:::

In a list where some articles have a picture and some do not, a line without one takes the `media`
slot, so its name starts where the others do. The slot wins over `image` and fills the same column,
and there is no placeholder of the kit's own: a line with neither has no column. `total-size` sets
the line total on the scale of [price](/components/price), from `xs` to `xl`, so a screen read from
across a counter can show what a line costs above its name:

:::preview{title="Lines with and without a picture, the total above the name"}
<x-wirekit::cart-list label="Your purchase">
    <x-wirekit::cart-item name="Desk lamp" image="/placeholder/400x400?bg=fde68a&fg=78350f&label=Lamp&gradient=none" :price="4900" :minor-units="true" currency="EUR" :quantity="1" :line-total="4900" total-size="lg" readonly />
    <x-wirekit::cart-item name="Gift wrapping" :price="350" :minor-units="true" currency="EUR" :quantity="2" :line-total="700" total-size="lg" readonly>
        <x-slot:media>
            <x-wirekit::aspect-ratio ratio="1/1" style="background: var(--color-wk-bg-subtle); border-radius: var(--radius-wk-md); display: grid; place-items: center;">
                <x-wirekit::icon name="package" size="lg" />
            </x-wirekit::aspect-ratio>
        </x-slot:media>
    </x-wirekit::cart-item>
</x-wirekit::cart-list>
:::

A screen read from across a counter needs the whole type scale larger, not one component. Set
`--font-scale-wk` on the page that shows it, `<html style="--font-scale-wk: 1.75">`, and every
step of the scale grows with it, the prices and the [cart summary](/components/cart-summary)
included. [Design tokens](/theming/design-tokens) describes the factor.

## A line for a till

A till lists its lines in a narrow column, and every press on − or + is something the till has to
record. `layout="compact"` sets a line in two rows: the name on one line beside what the line
costs, and under them what it is on the left and how many on the right. Where the unit price does
not fit beside the quantity, the quantity and the line's controls move to a row of their own, which
happens in a narrow column and on a touch screen, where the controls are larger; a long variant
shortens instead. `stepper="buttons"` shows −
and + with the quantity between them, and each press dispatches `wirekit:cart-decrement` or
`wirekit:cart-increment` with the line's key. Nothing holds a press back while your component
answers, so two quick presses are two presses, and the quantity shown is the one it renders next.

```blade
<div x-on:wirekit:cart-increment="$wire.increment($event.detail.item)"
     x-on:wirekit:cart-decrement="$wire.decrement($event.detail.item)">
    <x-wirekit::cart-list label="Till">
        @foreach ($lines as $line)
            <x-wirekit::cart-item :id="$line->id" layout="compact" stepper="buttons" :min="0" removes-at-zero remove-icon="trash"
                :name="$line->name" :price="$line->price" :quantity="$line->quantity" :line-total="$line->total" />
        @endforeach
    </x-wirekit::cart-list>
</div>
```

With `:min="0"` the − stays active on the last piece, and your component decides what that press
means, such as taking the line out. When it does take the line out, say so with `removes-at-zero`:
the − of the last piece then draws the remove control's glyph and is named "Remove Butter croissant"
rather than "Decrease the quantity of Butter croissant", and it still dispatches
`wirekit:cart-decrement`. `remove-icon` picks that glyph for the remove control as well, `trash` for
a bin instead of the cross. Put a line's own actions, a discount or why a price applies, in the
`actions` slot beside the quantity.

:::preview{title="Compact lines with a press stepper and line actions"}
<x-wirekit::cart-list label="Till" style="max-width: 26rem;">
    <x-wirekit::cart-item id="till-line-1" layout="compact" stepper="buttons" :min="0" name="Oat flat white, large" :price="420" :minor-units="true" currency="EUR" :quantity="2" :line-total="840">
        Art. 1042 · Oat milk
        <x-slot:actions>
            <x-wirekit::dropdown label="Line actions">
                <x-slot:trigger>
                    <x-wirekit::button intent="neutral" surface="ghost" size="md" icon-only>
                        <x-slot:iconLeft>
                            <x-wirekit::icon name="more-vertical" size="sm" aria-hidden="true" />
                        </x-slot:iconLeft>
                        Actions for Oat flat white, large
                    </x-wirekit::button>
                </x-slot:trigger>
                <x-wirekit::dropdown.item>Discount</x-wirekit::dropdown.item>
                <x-wirekit::dropdown.item>Why this price</x-wirekit::dropdown.item>
            </x-wirekit::dropdown>
        </x-slot:actions>
    </x-wirekit::cart-item>
    <x-wirekit::cart-item id="till-line-2" layout="compact" stepper="buttons" :min="0" name="Butter croissant" :price="290" :minor-units="true" currency="EUR" :quantity="1" :line-total="290">
        Art. 2210
    </x-wirekit::cart-item>
</x-wirekit::cart-list>
:::

## A total that fits its line

A line total in a narrow column, a large amount or one in a currency with many digits, can shrink to
fit with `fit-total`, which takes what `fit` takes on [price](/components/price): `fit-total` alone
for the `--text-wk-fit-floor` token, or a length such as `fit-total="0.5rem"` for a floor of its
own. In the compact line the name keeps at least eight characters first, then the total shrinks
down to its floor, and only below that does the name give up more, ending in an ellipsis. In the
default line the total shrinks once its controls stand on a row of their own.

:::preview{title="A compact line whose total fits beside the name"}
<x-wirekit::cart-list label="Till" style="max-width: 13rem;">
    <x-wirekit::cart-item id="fit-line-1" layout="compact" name="Espresso machine, dual boiler" :price="249900" :minor-units="true" currency="EUR" :quantity="5" :line-total="1249500" fit-total>
        Art. 3310
    </x-wirekit::cart-item>
</x-wirekit::cart-list>
:::

## Without separators

:::preview{title="Lines in cards instead of a divided list"}
<x-wirekit::cart-list :divided="false" label="Boxed cart">
    <x-wirekit::cart-item name="Brass desk lamp" :price="3900" :minor-units="true" currency="EUR" :quantity="1" :line-total="3900" />
    <x-wirekit::cart-item name="Walnut notebook" :price="1200" :minor-units="true" currency="EUR" :quantity="2" :line-total="2400" />
</x-wirekit::cart-list>
:::

## Props

### `cart-list`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `label` | `string` | `Cart` | Accessible name for the list. |
| `divided` | `bool` | `true` | Draw a separator between lines. |
| `scope` | `string\|null` | `null` | Theme scope. |

### `cart-item`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `string\|null` | `null` | Product name. Also what the remove control announces. |
| `image` | `string\|null` | `null` | Thumbnail URL. |
| `imageAlt` | `string` | `''` | Alt text. Empty on purpose — the name is already in the row as text. |
| `price` | `int\|float\|null` | `null` | Unit price. An empty string counts as `null`. |
| `compareAt` | `int\|float\|null` | `null` | Previous price, struck through when it is higher. |
| `maxPrecision` | `int\|null` | `null` | The most decimals for the unit price and the previous price, for a piece that costs less than the currency's smallest unit, such as 0.045 €. The line total is an amount and keeps the currency's digits. See [price](/components/price#more-decimals-than-the-currency). |
| `currency` | `string` | config | Currency code. |
| `minorUnits` | `bool` | `false` | Treat the amounts as minor units (cents). |
| `locale` | `string\|null` | `null` | Locale the amounts and the quantity are written in. Unset, the application's locale. Give the line and the [cart summary](/components/cart-summary) the same one, so a display reads alike from the lines to the total. |
| `lineTotal` | `int\|float\|null` | `null` | What the line costs at this quantity. You compute it. An empty string counts as `null`. |
| `quantity` | `int\|float` | `1` | Current quantity. A `readonly` line also shows a measured one, such as `0.532` with `unit="kg"`. |
| `min` | `int` | `1` | Lowest quantity. |
| `max` | `int\|null` | `null` | Stock ceiling. |
| `step` | `int` | `1` | Stepper increment. |
| `size` | `sm\|md\|lg` | `lg` | Stepper size. |
| `unit` | `string\|null` | `null` | Unit suffix for the stepper. |
| `removable` | `bool` | `true` | Render the built-in remove control. |
| `removeIcon` | `string` | `close` | The icon alias the built-in remove control draws, such as `trash` for a bin. A − that takes the line out (`removesAtZero`) draws it too. |
| `readonly` | `bool` | `false` | Show the line without operating it: the quantity as text with its unit, no built-in remove control. |
| `hideQuantity` | `bool` | `false` | Leave the quantity of a `readonly` line out, visibly and for a screen reader. An editable line keeps its field. |
| `unitPrice` | `int\|float\|null` | `null` | Unit price by a reference measure, shown beside the price. Needs `unitMeasure`. |
| `unitMeasure` | `string\|null` | `null` | The reference for `unitPrice`, such as `kg` or `1 l`. |
| `layout` | `default\|compact` | `default` | `compact` sets the line in two rows for a narrow column: the name beside the line total, then the variant and unit price beside the quantity. |
| `stepper` | `field\|buttons` | `field` | `buttons` shows − and + around the quantity, and each press dispatches `wirekit:cart-decrement` or `wirekit:cart-increment` with the line's key. `field` is the number field a `wire:model` binds. |
| `removesAtZero` | `bool` | `false` | Your application takes the line out when its quantity reaches zero. With `stepper="buttons"` and `:min="0"`, the − of the last piece draws the remove glyph and is named "Remove …", and it still dispatches `wirekit:cart-decrement`. |
| `totalSize` | `xs\|sm\|md\|lg\|xl\|null` | `null` | The size of the line total, on the scale of [price](/components/price). Without it the total takes the size `price` is configured with. |
| `fitTotal` | `bool\|string\|null` | `null` | Shrinks the line total down to a floor so it fits its line, as `fit` on [price](/components/price): `true` for the `--text-wk-fit-floor` token, or a length. In the compact line the name keeps eight characters before the total shrinks. See [A total that fits its line](#a-total-that-fits-its-line). |
| `scope` | `string\|null` | `null` | Theme scope. |

## Slots

| Slot | Component | Description |
|------|-----------|-------------|
| default | `cart-list` | The lines. |
| default | `cart-item` | A meta line under the name — variant, size, delivery note. |
| `remove` | `cart-item` | Replaces the built-in remove control entirely. |
| `actions` | `cart-item` | A line's own actions beside the quantity, such as a menu. |
| `details` | `cart-item` | Further lines under the unit price, such as the saving or a promotion. |
| `media` | `cart-item` | The thumbnail column's content in place of `image`, such as a painted placeholder for an article without a photo, so the names of a list start in one column. Hide a purely decorative placeholder from assistive technology; the name already says what the line is. |

## Accessibility

- The list is a real `<ul>` with an explicit `role="list"` and a name, so it survives the
  marker removal that otherwise costs it its semantics in WebKit.
- The remove control names its product — "Remove Merino crew", not "Remove". Ten controls that
  all announce the same word cannot be told apart by anyone using voice control, and they read
  as ten identical buttons in a screen-reader element list.
- The line total sits in a polite live region, so a quantity change is announced once the new
  total arrives rather than silently. With `stepper="buttons"` the quantity is the region
  instead: a press moves both, and the reader pressed for the quantity.
- The − and + of `stepper="buttons"` name their product, "Decrease the quantity of Butter
  croissant", for the reason the remove control does, and sit in a group named "Quantity of
  Butter croissant". The number field of the default stepper carries the same name, so its
  − and + say which line they change as well.
- The − and + of `stepper="buttons"` and the remove control show their name in a
  [tooltip](/components/tooltip) on hover and on keyboard focus, since each carries a glyph and no
  word. The tooltip repeats the name rather than describing the button, so a screen reader reads it
  once.
- The unit price forwards to [price](/components/price), which already renders the struck
  previous price with a screen-reader-only label. This component does not re-solve that.
- The stepper is a native `<input type="number">`. It carries no `aria-valuenow`: that property
  belongs to `role="spinbutton"`, and adding it to a native number field overrides the value
  the browser already exposes with a second copy that can disagree.
- With `readonly` the quantity is text, and the word the stepper's label carried, "Quantity",
  names it for a screen reader in a visually hidden prefix.

## Keyboard Interaction

| Key | Action |
|-----|--------|
| <kbd>Tab</kbd> | Move to the quantity field, then to the remove control, then to the next line. |
| <kbd>↑</kbd> / <kbd>↓</kbd> | Increment / decrement the quantity, when the field has focus. |
| <kbd>Enter</kbd> / <kbd>Space</kbd> | Activate the remove control. |

The list itself takes no focus and has no keyboard model of its own — it is a container, and
every control inside it is reached in document order.

## Customization

Set a project-wide floor for the quantity, or a different stepper size. `php artisan
wirekit:install` published `config/wirekit.php`, and the `cart-item` entry is already in its
`components` array; change it there:

```php
// config/wirekit.php
'components' => [
    'cart-item' => ['min' => 1, 'size' => 'lg'],
],
```

`min` is the lowest quantity a line can reach before the decrement stops. Leaving it at 1 is
usually right — a line holding none of something is a removal, not a quantity — but a shop
selling by weight or in pairs may want its own floor.

`size` is the stepper's touch target. The default is `lg` rather than the `md` used elsewhere,
which is the point made above: `md` clears the AA target size and misses the AAA one, and `lg`
clears both. Lower it only where you know the surface is not touched.

## Further Reading

- [product-card](/components/product-card) — the grid the cart is filled from
- [price](/components/price) — currency formatting, compare-at, minor units
- [number-input](/components/number-input) — the stepper, its bounds and its suffix
