Skip to content

← all comparisons

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.

React 622 B gzip · 38 lines
CartViewReact.tsx
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>
  );
}
Vue 759 B gzip · 53 lines
CartViewVue.vue
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>
Svelte 680 B gzip · 42 lines
CartViewSvelte.svelte
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}