Skip to main content
Copy for LLM

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, price, number-input and button — so nothing here behaves differently from the way it behaves everywhere else.

A cart with three lines
  • Brass desk lamp Warm white · Dimmable Previous price €59.00 €39.00
    €39.00
  • Walnut notebook A5 · Dotted €12.00
    €24.00
  • Merino crew Charcoal · M €89.00
    €89.00

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

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. 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.

A line on sale
  • Brass desk lamp Previous price €59.00 €39.00
    €39.00

Quantity

The stepper is 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.

Stock ceiling and a unit suffix
  • Merino crew €89.00
    €267.00
  • Loose-leaf tea €14.50
    €29.00

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.

<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>

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.

A line that cannot be removed, and one with a custom control
  • Gift wrapping €5.00
    €5.00
  • Merino crew €89.00
    €89.00

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

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:

{{-- 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.

A cart that is only shown
  • Olive oil, 500 ml Previous price €7.99 €7.49 (€14.98 / 1 l)
    You save €1.00 · Spring offer
    Quantity 2 €14.98
  • Coffee beans €15.98
    Quantity 0.532 kg €8.50

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, from xs to xl, so a screen read from across a counter can show what a line costs above its name:

Lines with and without a picture, the total above the name
  • Desk lamp €49.00
    Quantity 1 €49.00
  • Gift wrapping €3.50
    Quantity 2 €7.00

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 included. 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.

<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.

Compact lines with a press stepper and line actions
  • Oat flat white, large €8.40
    Art. 1042 · Oat milk €4.20
    2
  • Butter croissant €2.90
    Art. 2210 €2.90
    1

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: 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.

A compact line whose total fits beside the name
  • Espresso machine, dual boiler €12,495.00
    Art. 3310 €2,499.00

Without separators

Lines in cards instead of a divided list
  • Brass desk lamp €39.00
    €39.00
  • Walnut notebook €12.00
    €24.00

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.
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 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. 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: 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.
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 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, 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
Tab Move to the quantity field, then to the remove control, then to the next line.
↑ / ↓ Increment / decrement the quantity, when the field has focus.
Enter / Space 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:

// 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 — the grid the cart is filled from
  • price — currency formatting, compare-at, minor units
  • number-input — the stepper, its bounds and its suffix

Updated in WireKit v2.67.0 (2026-10-15)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store