Skip to content
Brok UI

Loading…

No results

Shopping Cart

Open source

Cart pages — a list of editable line items with quantity steppers beside a sticky order-summary card with a promo field and totals that recompute live.

Preview

resources/views/blocks/shopping-cart-two-column.blade.php Blade
@props([
    'heading' => 'Your cart',
    'currency' => '$',
    'shipping' => 'Free',
    'taxRate' => 0.0825,
    'items' => null,
])

@php
    $items ??= [
        [
            'name' => 'Aerolight Running Shoes',
            'variant' => 'Slate / US 10',
            'price' => 128.0,
            'qty' => 1,
        ],
        [
            'name' => 'Merino Crew Socks (3-pack)',
            'variant' => 'Charcoal',
            'price' => 27.0,
            'qty' => 2,
        ],
        [
            'name' => 'Everyday Tech Backpack',
            'variant' => 'Olive / 22L',
            'price' => 149.0,
            'qty' => 1,
        ],
    ];
@endphp

<x-ui.section space="lg" {{ $attributes }}>
    <div
        class="mx-auto max-w-6xl px-4"
        x-data="{
            currency: @js($currency),
            taxRate: @js($taxRate),
            cart: @js(collect($items)->map(fn ($i) => [
                'name' => __($i['name']),
                'variant' => __($i['variant']),
                'price' => (float) $i['price'],
                'qty' => max(1, min(10, (int) $i['qty'])),
            ])->values()),
            remove(i) { this.cart.splice(i, 1) },
            money(n) { return this.currency + n.toFixed(2) },
            get subtotal() { return this.cart.reduce((sum, item) => sum + item.price * item.qty, 0) },
            get tax() { return this.subtotal * this.taxRate },
            get total() { return this.subtotal + this.tax },
            get count() { return this.cart.reduce((sum, item) => sum + item.qty, 0) },
        }"
    >
        <header class="flex items-baseline gap-4">
            <h1 class="text-3xl font-bold tracking-tight sm:text-4xl">{{ __($heading) }}</h1>
            <x-ui.badge variant="secondary" aria-live="polite">
                <span x-text="count"></span>
                <span class="ms-2" x-text="count === 1 ? @js(__('item')) : @js(__('items'))"></span>
            </x-ui.badge>
        </header>

        <div class="mt-10 grid grid-cols-1 gap-10 lg:grid-cols-3 lg:items-start">
            {{-- Left column: line items --}}
            <div class="lg:col-span-2">
                <template x-if="cart.length === 0">
                    <x-ui.card class="flex flex-col items-center gap-4 p-10 text-center">
                        <svg class="size-10 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                            <circle cx="9" cy="21" r="1" />
                            <circle cx="20" cy="21" r="1" />
                            <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" />
                        </svg>
                        <p class="text-base font-medium text-foreground">{{ __('Your cart is empty') }}</p>
                        <a href="#" class="text-sm font-medium text-link hover:underline focus-visible:outline-none focus-visible:underline">
                            {{ __('Continue shopping') }}
                        </a>
                    </x-ui.card>
                </template>

                <ul role="list" x-show="cart.length > 0" class="divide-y divide-border rounded-lg border border-border bg-card">
                    <template x-for="(item, i) in cart" :key="item.name + i">
                        <li class="flex items-start gap-4 p-4 sm:gap-6 sm:p-6">
                            {{-- Media thumbnail --}}
                            <div class="w-20 shrink-0 sm:w-24">
                                <x-ui.aspect-ratio :ratio="1" class="overflow-hidden rounded-lg bg-muted">
                                    <svg
                                        class="absolute inset-0 size-full text-muted-foreground/30"
                                        viewBox="0 0 100 100" fill="none" stroke="currentColor"
                                        stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
                                        preserveAspectRatio="xMidYMid slice" aria-hidden="true"
                                    >
                                        <rect x="22" y="30" width="56" height="40" rx="4" />
                                        <path d="m22 56 16-14 14 12 10-8 16 14" />
                                        <circle cx="62" cy="44" r="5" />
                                    </svg>
                                </x-ui.aspect-ratio>
                            </div>

                            {{-- Details --}}
                            <div class="flex min-w-0 flex-1 flex-col gap-4">
                                <div class="flex items-start justify-between gap-4">
                                    <div class="min-w-0">
                                        <h3 class="truncate text-sm font-semibold text-foreground sm:text-base">
                                            <a href="#" class="hover:text-link focus-visible:outline-none focus-visible:text-link" x-text="item.name"></a>
                                        </h3>
                                        <p class="mt-0.5 truncate text-sm text-muted-foreground" x-text="item.variant"></p>
                                        <p class="mt-2 text-sm text-muted-foreground">
                                            <span x-text="money(item.price)"></span>
                                            <span class="ms-2">{{ __('each') }}</span>
                                        </p>
                                    </div>
                                    <p class="shrink-0 text-sm font-semibold text-foreground sm:text-base" x-text="money(item.price * item.qty)"></p>
                                </div>

                                <div class="flex items-center justify-between gap-4">
                                    {{-- Quantity stepper: the number-field root is x-modelable, so the
                                         x-for row's `item.qty` drives it and the totals stay live. --}}
                                    <x-ui.number-field
                                        variant="stepper"
                                        size="sm"
                                        x-model="item.qty"
                                        :min="1"
                                        :max="10"
                                        ::aria-label="{{ \Illuminate\Support\Js::from(__('Quantity for')) }} + ' ' + item.name"
                                    />

                                    <x-ui.button
                                        type="button"
                                        variant="ghost"
                                        size="sm"
                                        class="gap-2 text-muted-foreground hover:text-destructive"
                                        x-on:click="remove(i)"
                                    >
                                        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                            <path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
                                        </svg>
                                        {{ __('Remove') }}
                                    </x-ui.button>
                                </div>
                            </div>
                        </li>
                    </template>
                </ul>
            </div>

            {{-- Right column: sticky order summary --}}
            <aside class="lg:sticky lg:top-8" aria-labelledby="cart-summary-title">
                <x-ui.card class="p-6">
                    <h2 id="cart-summary-title" class="text-base font-semibold tracking-tight">{{ __('Order summary') }}</h2>

                    <dl class="mt-6 space-y-4 text-sm">
                        <div class="flex items-center justify-between">
                            <dt class="text-muted-foreground">{{ __('Subtotal') }}</dt>
                            <dd class="font-medium text-foreground" x-text="money(subtotal)"></dd>
                        </div>
                        <div class="flex items-center justify-between">
                            <dt class="text-muted-foreground">{{ __('Shipping') }}</dt>
                            <dd class="font-medium text-foreground">{{ __($shipping) }}</dd>
                        </div>
                        <div class="flex items-center justify-between">
                            <dt class="text-muted-foreground">{{ __('Estimated tax') }}</dt>
                            <dd class="font-medium text-foreground" x-text="money(tax)"></dd>
                        </div>
                    </dl>

                    <x-ui.separator class="my-6" />

                    <div class="space-y-2">
                        <label for="cart-promo" class="text-sm font-medium text-foreground">{{ __('Promo code') }}</label>
                        <div class="flex items-stretch gap-2">
                            <x-ui.input
                                id="cart-promo"
                                name="promo_code"
                                autocomplete="off"
                                class="flex-1"
                                placeholder="{{ __('Enter code') }}"
                            />
                            <x-ui.button type="button" variant="outline">
                                {{ __('Apply') }}
                            </x-ui.button>
                        </div>
                    </div>

                    <x-ui.separator class="my-6" />

                    <div class="flex items-center justify-between text-base">
                        <span class="font-semibold text-foreground">{{ __('Total') }}</span>
                        <span class="font-semibold text-foreground" x-text="money(total)"></span>
                    </div>

                    <x-ui.button type="button" class="mt-6 w-full" x-bind:disabled="cart.length === 0">
                        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                            <rect x="5" y="11" width="14" height="10" rx="2" />
                            <path d="M8 11V7a4 4 0 0 1 8 0v4" />
                        </svg>
                        {{ __('Checkout') }}
                    </x-ui.button>

                    <p class="mt-4 text-center text-sm">
                        <a href="#" class="text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:text-foreground">
                            {{ __('or continue shopping') }}
                        </a>
                    </p>
                </x-ui.card>
            </aside>
        </div>
    </div>
</x-ui.section>

Installation

terminal
php artisan ui:add blocks/shopping-cart-two-column

Registry contract

Install confidence

php artisan ui:add blocks/shopping-cart-two-column writes only the generated targets below. The CLI validates each file hash before writing and prompts before replacing local changes unless --force is used.

Version
1.1.2
License
open
Stability
stable
Contract
v1
Foundation
≥ 1.0.0
Type Generated target
blade resources/views/blocks/shopping-cart-two-column.blade.php

Registry dependencies

aspect-ratio badge button card input number-field section separator

Package dependencies

composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.

shopping-cart-two-column.md
# Brok UI block: Shopping Cart (`shopping-cart-two-column`)

A two-column shopping cart page with a heading and live item count, a wider list of line items each with a square media thumbnail, name, variant, unit price, a number-field stepper clamped 1–10, live line total, and remove button, beside a sticky order-summary card with subtotal, shipping, estimated tax, a promo-code input with apply button, a bold total, a full-width checkout button, and a continue-shopping link — all totals recompute live as quantities change.

Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.

## Install

```bash
php artisan ui:add blocks/shopping-cart-two-column
```

## Render it

```blade
<x-blocks.shopping-cart-two-column />
```

## Props

- `heading` (string, default `Your cart`) — Section heading.
- `currency` (string, default `$`) — Currency symbol or code used to format prices.
- `shipping` (string, default `Free`) — Shipping line label shown in the order summary.
- `tax-rate` (float, default `0.0825`) — Tax rate applied to the subtotal.
- `items` (mixed|null, default `null`) — Cart line items with name, variant, price and quantity; null falls back to sample data.

## Use when

- Building a full two-column shopping cart page with line items, quantity steppers and a sticky order-summary card.
- Wanting the cart to be its own page rather than a slide-over drawer.

## Avoid when

- Needing a slide-over mini-cart instead of a full page; use shopping-cart-drawer instead.

## Rules

- Render the installed block with `<x-blocks.shopping-cart-two-column />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.

## Links

- Docs: https://brokui.dev/blocks/shopping-cart-two-column
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/shopping-cart-two-column.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Guidance

Use when

  • Building a full two-column shopping cart page with line items, quantity steppers and a sticky order-summary card.
  • Wanting the cart to be its own page rather than a slide-over drawer.

Avoid when

  • Needing a slide-over mini-cart instead of a full page; use shopping-cart-drawer instead.

Anti-patterns

  • Do not replace semantic props with conflicting utility classes.
  • Do not remove labels, focus styles, or state attributes.

Usage

Render the block as a component, passing data where useful:

resources/views/welcome.blade.php Blade
<x-blocks.shopping-cart-two-column />

Built from primitives

Composes <brok:*> Semantic tokens Re-themeable RTL-ready

This block composes installed <brok:*> primitives and semantic design tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by editing CSS variables; flip the preview to RTL to confirm it mirrors.

Source

The exact, editable file ui:add writes into your app. The preview above renders this same source — there are no preview-only blocks.

resources/views/blocks/shopping-cart-two-column.blade.php Blade
@props([
    'heading' => 'Your cart',
    'currency' => '$',
    'shipping' => 'Free',
    'taxRate' => 0.0825,
    'items' => null,
])

@php
    $items ??= [
        [
            'name' => 'Aerolight Running Shoes',
            'variant' => 'Slate / US 10',
            'price' => 128.0,
            'qty' => 1,
        ],
        [
            'name' => 'Merino Crew Socks (3-pack)',
            'variant' => 'Charcoal',
            'price' => 27.0,
            'qty' => 2,
        ],
        [
            'name' => 'Everyday Tech Backpack',
            'variant' => 'Olive / 22L',
            'price' => 149.0,
            'qty' => 1,
        ],
    ];
@endphp

<x-ui.section space="lg" {{ $attributes }}>
    <div
        class="mx-auto max-w-6xl px-4"
        x-data="{
            currency: @js($currency),
            taxRate: @js($taxRate),
            cart: @js(collect($items)->map(fn ($i) => [
                'name' => __($i['name']),
                'variant' => __($i['variant']),
                'price' => (float) $i['price'],
                'qty' => max(1, min(10, (int) $i['qty'])),
            ])->values()),
            remove(i) { this.cart.splice(i, 1) },
            money(n) { return this.currency + n.toFixed(2) },
            get subtotal() { return this.cart.reduce((sum, item) => sum + item.price * item.qty, 0) },
            get tax() { return this.subtotal * this.taxRate },
            get total() { return this.subtotal + this.tax },
            get count() { return this.cart.reduce((sum, item) => sum + item.qty, 0) },
        }"
    >
        <header class="flex items-baseline gap-4">
            <h1 class="text-3xl font-bold tracking-tight sm:text-4xl">{{ __($heading) }}</h1>
            <x-ui.badge variant="secondary" aria-live="polite">
                <span x-text="count"></span>
                <span class="ms-2" x-text="count === 1 ? @js(__('item')) : @js(__('items'))"></span>
            </x-ui.badge>
        </header>

        <div class="mt-10 grid grid-cols-1 gap-10 lg:grid-cols-3 lg:items-start">
            {{-- Left column: line items --}}
            <div class="lg:col-span-2">
                <template x-if="cart.length === 0">
                    <x-ui.card class="flex flex-col items-center gap-4 p-10 text-center">
                        <svg class="size-10 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                            <circle cx="9" cy="21" r="1" />
                            <circle cx="20" cy="21" r="1" />
                            <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" />
                        </svg>
                        <p class="text-base font-medium text-foreground">{{ __('Your cart is empty') }}</p>
                        <a href="#" class="text-sm font-medium text-link hover:underline focus-visible:outline-none focus-visible:underline">
                            {{ __('Continue shopping') }}
                        </a>
                    </x-ui.card>
                </template>

                <ul role="list" x-show="cart.length > 0" class="divide-y divide-border rounded-lg border border-border bg-card">
                    <template x-for="(item, i) in cart" :key="item.name + i">
                        <li class="flex items-start gap-4 p-4 sm:gap-6 sm:p-6">
                            {{-- Media thumbnail --}}
                            <div class="w-20 shrink-0 sm:w-24">
                                <x-ui.aspect-ratio :ratio="1" class="overflow-hidden rounded-lg bg-muted">
                                    <svg
                                        class="absolute inset-0 size-full text-muted-foreground/30"
                                        viewBox="0 0 100 100" fill="none" stroke="currentColor"
                                        stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
                                        preserveAspectRatio="xMidYMid slice" aria-hidden="true"
                                    >
                                        <rect x="22" y="30" width="56" height="40" rx="4" />
                                        <path d="m22 56 16-14 14 12 10-8 16 14" />
                                        <circle cx="62" cy="44" r="5" />
                                    </svg>
                                </x-ui.aspect-ratio>
                            </div>

                            {{-- Details --}}
                            <div class="flex min-w-0 flex-1 flex-col gap-4">
                                <div class="flex items-start justify-between gap-4">
                                    <div class="min-w-0">
                                        <h3 class="truncate text-sm font-semibold text-foreground sm:text-base">
                                            <a href="#" class="hover:text-link focus-visible:outline-none focus-visible:text-link" x-text="item.name"></a>
                                        </h3>
                                        <p class="mt-0.5 truncate text-sm text-muted-foreground" x-text="item.variant"></p>
                                        <p class="mt-2 text-sm text-muted-foreground">
                                            <span x-text="money(item.price)"></span>
                                            <span class="ms-2">{{ __('each') }}</span>
                                        </p>
                                    </div>
                                    <p class="shrink-0 text-sm font-semibold text-foreground sm:text-base" x-text="money(item.price * item.qty)"></p>
                                </div>

                                <div class="flex items-center justify-between gap-4">
                                    {{-- Quantity stepper: the number-field root is x-modelable, so the
                                         x-for row's `item.qty` drives it and the totals stay live. --}}
                                    <x-ui.number-field
                                        variant="stepper"
                                        size="sm"
                                        x-model="item.qty"
                                        :min="1"
                                        :max="10"
                                        ::aria-label="{{ \Illuminate\Support\Js::from(__('Quantity for')) }} + ' ' + item.name"
                                    />

                                    <x-ui.button
                                        type="button"
                                        variant="ghost"
                                        size="sm"
                                        class="gap-2 text-muted-foreground hover:text-destructive"
                                        x-on:click="remove(i)"
                                    >
                                        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                            <path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
                                        </svg>
                                        {{ __('Remove') }}
                                    </x-ui.button>
                                </div>
                            </div>
                        </li>
                    </template>
                </ul>
            </div>

            {{-- Right column: sticky order summary --}}
            <aside class="lg:sticky lg:top-8" aria-labelledby="cart-summary-title">
                <x-ui.card class="p-6">
                    <h2 id="cart-summary-title" class="text-base font-semibold tracking-tight">{{ __('Order summary') }}</h2>

                    <dl class="mt-6 space-y-4 text-sm">
                        <div class="flex items-center justify-between">
                            <dt class="text-muted-foreground">{{ __('Subtotal') }}</dt>
                            <dd class="font-medium text-foreground" x-text="money(subtotal)"></dd>
                        </div>
                        <div class="flex items-center justify-between">
                            <dt class="text-muted-foreground">{{ __('Shipping') }}</dt>
                            <dd class="font-medium text-foreground">{{ __($shipping) }}</dd>
                        </div>
                        <div class="flex items-center justify-between">
                            <dt class="text-muted-foreground">{{ __('Estimated tax') }}</dt>
                            <dd class="font-medium text-foreground" x-text="money(tax)"></dd>
                        </div>
                    </dl>

                    <x-ui.separator class="my-6" />

                    <div class="space-y-2">
                        <label for="cart-promo" class="text-sm font-medium text-foreground">{{ __('Promo code') }}</label>
                        <div class="flex items-stretch gap-2">
                            <x-ui.input
                                id="cart-promo"
                                name="promo_code"
                                autocomplete="off"
                                class="flex-1"
                                placeholder="{{ __('Enter code') }}"
                            />
                            <x-ui.button type="button" variant="outline">
                                {{ __('Apply') }}
                            </x-ui.button>
                        </div>
                    </div>

                    <x-ui.separator class="my-6" />

                    <div class="flex items-center justify-between text-base">
                        <span class="font-semibold text-foreground">{{ __('Total') }}</span>
                        <span class="font-semibold text-foreground" x-text="money(total)"></span>
                    </div>

                    <x-ui.button type="button" class="mt-6 w-full" x-bind:disabled="cart.length === 0">
                        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                            <rect x="5" y="11" width="14" height="10" rx="2" />
                            <path d="M8 11V7a4 4 0 0 1 8 0v4" />
                        </svg>
                        {{ __('Checkout') }}
                    </x-ui.button>

                    <p class="mt-4 text-center text-sm">
                        <a href="#" class="text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:text-foreground">
                            {{ __('or continue shopping') }}
                        </a>
                    </p>
                </x-ui.card>
            </aside>
        </div>
    </div>
</x-ui.section>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.1.2
Status
Stable
License
open
Deprecation
Not deprecated
Contract
v1
Foundation
≥ 1.0.0