List rendering + composition
Cart view
Iterates over the cart items and embeds the same-framework QuantityStepper for each row.
Each panel header shows the source size of that component file (raw, gzipped, line count). Source-only — framework runtime (React ~45 KB, Vue ~35 KB, Svelte ~5 KB gzipped) is shared across every island and isn't included in these numbers.
How this works
Object.values($cart).map(...) with a key prop on each row. Empty state is an early return. No animation — for a slide-out on remove or FLIP on reorder, you would reach for framer-motion or similar.
import { removeFromCart } from '../stores/cart';
import { useCart } from '../hooks/useCartReact';
import QuantityStepperReact from './QuantityStepperReact';
export default function CartViewReact() {
const { items, subtotal: total } = useCart();
if (items.length === 0) {
return <p className="text-sm text-slate-500 dark:text-slate-400">Cart is empty.</p>;
}
return (
<div className="flex flex-col gap-3">
{items.map((item) => (
<div key={item.id} className="flex flex-wrap items-center gap-3 rounded border border-slate-200 dark:border-slate-800 p-2">
<img src={item.image} alt={item.title} className="h-12 w-12 shrink-0 rounded object-cover" />
<div className="min-w-0 flex-1 truncate text-sm">{item.title}</div>
<span className="shrink-0 tabular-nums text-blue-700 dark:text-blue-400">${item.price}</span>
<div className="ml-auto flex shrink-0 items-center gap-3">
<QuantityStepperReact id={item.id} />
<button
type="button"
onClick={() => removeFromCart(item.id)}
className="text-xs text-slate-500 dark:text-slate-400 hover:text-red-600"
>
remove
</button>
</div>
</div>
))}
<div className="mt-2 flex justify-end gap-3 text-sm">
<span className="text-slate-500 dark:text-slate-400">Total</span>
<span className="tabular-nums font-semibold">${total.toFixed(2)}</span>
</div>
</div>
);
} How this works
computed for items and total. v-for with :key on the row, v-if/v-else for the empty state. <TransitionGroup> wraps the list and a small <style scoped> block defines cart-row-enter/leave/move classes for slide-and-fade animation on add/remove.
<script setup lang="ts">
import { removeFromCart } from '../stores/cart';
import { useCart } from '../composables/useCartVue';
import QuantityStepperVue from './QuantityStepperVue.vue';
const { items, subtotal: total } = useCart();
</script>
<template>
<p v-if="items.length === 0" class="text-sm text-slate-500 dark:text-slate-400">Cart is empty.</p>
<div v-else class="flex flex-col gap-3">
<TransitionGroup name="cart-row" tag="div" class="flex flex-col gap-3">
<div
v-for="item in items"
:key="item.id"
class="cart-row flex flex-wrap items-center gap-3 rounded border border-slate-200 dark:border-slate-800 p-2"
>
<img :src="item.image" :alt="item.title" class="h-12 w-12 shrink-0 rounded object-cover" />
<div class="min-w-0 flex-1 truncate text-sm">{{ item.title }}</div>
<span class="shrink-0 tabular-nums text-blue-700 dark:text-blue-400">${{ item.price }}</span>
<div class="ml-auto flex shrink-0 items-center gap-3">
<QuantityStepperVue :id="item.id" />
<button
type="button"
@click="removeFromCart(item.id)"
class="text-xs text-slate-500 dark:text-slate-400 hover:text-red-600"
>remove</button>
</div>
</div>
</TransitionGroup>
<div class="mt-2 flex justify-end gap-3 text-sm">
<span class="text-slate-500 dark:text-slate-400">Total</span>
<span class="tabular-nums font-semibold">${{ total.toFixed(2) }}</span>
</div>
</div>
</template>
<style scoped>
.cart-row-move,
.cart-row-enter-active,
.cart-row-leave-active {
transition: all 200ms ease;
}
.cart-row-enter-from,
.cart-row-leave-to {
opacity: 0;
transform: translateX(20px);
}
.cart-row-leave-active {
position: absolute;
}
</style> How this works
$derived for items and total. The interesting bit: animate:flip={{ duration: 200 }} on each row gives free FLIP animation when items reorder or the list shifts after a remove. transition:fade handles the entry/exit. Both come from svelte/animate and svelte/transition — no library, no CSS.
<script lang="ts">
import { flip } from 'svelte/animate';
import { fade } from 'svelte/transition';
import { removeFromCart } from '../stores/cart';
import { useCart } from '../lib/useCartSvelte.svelte';
import QuantityStepperSvelte from './QuantityStepperSvelte.svelte';
const totals = useCart();
const items = $derived(totals.items);
const total = $derived(totals.subtotal);
</script>
{#if items.length === 0}
<p class="text-sm text-slate-500 dark:text-slate-400">Cart is empty.</p>
{:else}
<div class="flex flex-col gap-3">
{#each items as item (item.id)}
<div
class="flex flex-wrap items-center gap-3 rounded border border-slate-200 dark:border-slate-800 p-2"
animate:flip={{ duration: 200 }}
transition:fade={{ duration: 150 }}
>
<img src={item.image} alt={item.title} class="h-12 w-12 shrink-0 rounded object-cover" />
<div class="min-w-0 flex-1 truncate text-sm">{item.title}</div>
<span class="shrink-0 tabular-nums text-blue-700 dark:text-blue-400">${item.price}</span>
<div class="ml-auto flex shrink-0 items-center gap-3">
<QuantityStepperSvelte id={item.id} />
<button
type="button"
onclick={() => removeFromCart(item.id)}
class="text-xs text-slate-500 dark:text-slate-400 hover:text-red-600"
>remove</button>
</div>
</div>
{/each}
<div class="mt-2 flex justify-end gap-3 text-sm">
<span class="text-slate-500 dark:text-slate-400">Total</span>
<span class="tabular-nums font-semibold">${total.toFixed(2)}</span>
</div>
</div>
{/if}