Cart line item

Cart row with thumb, variant chip, stepper, line price with compare-at and a hover-reveal remove.

componentrowanycartquantitycompare-at
$npx shadcn@latest add @storekit/cart-line-item
cart-line-item

Live and interactive. Switch the brand in the frame header to see the same source under another token file.

Installation

$npx shadcn@latest add @storekit/cart-line-item
$pnpm dlx shadcn@latest add @storekit/cart-line-item
$yarn dlx shadcn@latest add @storekit/cart-line-item
$bunx --bun shadcn@latest add @storekit/cart-line-item

Source

components/storekit/cart/cart-line-item.tsx
71 lines
"use client"

import { X } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { Price } from "@/components/storekit/ui/price"
import { ProductImage } from "@/components/storekit/ui/product-image"
import { Stepper } from "@/components/storekit/ui/stepper"

/* One cart row: thumb, title, variant chip, quantity stepper, line price with
   compare-at, and a remove button. On pointer devices the remove button
   reveals on hover or focus; on touch it is always visible. */

export function CartLineItem({
  title = "Daily Greens",
  variant = "30 servings",
  image = "/brands/morrow/greens.webp",
  price = 5900,
  compareAt,
  quantity = 1,
  max = 10,
  onQuantityChange,
  onRemove,
  className,
}: {
  title?: string
  variant?: string
  image?: string
  /** Unit price in cents. */
  price?: number
  /** Unit compare-at in cents. */
  compareAt?: number
  quantity?: number
  max?: number
  onQuantityChange?: (next: number) => void
  onRemove?: () => void
  className?: string
}) {
  return (
    <div data-sk-section="cart" data-sk-variant="cart-line-item" className={cn("group relative flex gap-3 py-4 sm:gap-4", className)}>
      <ProductImage src={image} alt={title} className="h-[72px] w-[72px] shrink-0 rounded-sk-card border border-sk-line sm:h-24 sm:w-24" fit="cover" zoomOnHover={false} />
      <div className="flex min-w-0 flex-1 flex-col justify-between gap-3">
        <div className="flex items-start justify-between gap-3 pr-10">
          <div className="min-w-0">
            <p className="truncate text-[15px] font-medium text-sk-ink">{title}</p>
            {variant ? (
              <span className="mt-1 inline-flex rounded-sk-chip border border-sk-line px-2 py-0.5 text-xs text-sk-muted">{variant}</span>
            ) : null}
          </div>
        </div>
        <div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2">
          <Stepper label={`Quantity, ${title}`} value={quantity} onChange={(q) => onQuantityChange?.(q)} max={max} size="sm" className="shrink-0" />
          <Price amount={price * quantity} compareAt={compareAt ? compareAt * quantity : undefined} size="sm" className="ml-auto min-w-0 justify-end text-right" />
        </div>
      </div>
      <button
        type="button"
        aria-label={`Remove ${title}`}
        onClick={onRemove}
        className={cn(
          "absolute right-0 top-2 grid h-11 w-11 place-items-center rounded-sk-btn text-sk-muted",
          "transition-[opacity,transform,background-color,color] duration-200 ease-sk-out hover:bg-sk-ink/[0.05] hover:text-sk-ink active:scale-[0.97]",
          "focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper",
          "[@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100 [@media(hover:hover)]:group-focus-within:opacity-100"
        )}
      >
        <X size={16} />
      </button>
    </div>
  )
}

Usage

Import from "@/components/storekit/cart/cart-line-item" and pass your product data. The demo above exercises the full prop surface with sample values.

Cart line item, StoreKit