Kosmesis
Components

Cart Item

A single cart row with a quantity stepper. Purely presentational — no Morphos equivalent.

Installation

npx kosmesis add cart-item
pnpm dlx kosmesis add cart-item
yarn dlx kosmesis add cart-item
bunx kosmesis add cart-item

Copy and paste the following code into your project.

cart-item.tsx
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { cn } from "@/lib/utils";export interface CartItemProps {  image?: string;  name: string;  variant?: string;  price: string;  quantity: number;  onQuantityChange?: (quantity: number) => void;  onRemove?: () => void;  class?: string;}@Component()export class CartItem extends StatelessComponent<CartItemProps> {  render() {    const { image, name, variant, price, quantity, onQuantityChange, onRemove, class: cls } = this.props;    return (      <div data-slot="cart-item" class={cn("flex items-center gap-3 py-3", cls)}>        {image && <img src={image} alt={name} class="size-14 shrink-0 rounded-md object-cover" />}        <div class="flex min-w-0 flex-1 flex-col gap-0.5">          <p class="truncate text-sm font-medium">{name}</p>          {variant && <p class="text-xs text-muted-foreground">{variant}</p>}          <div class="mt-1 flex items-center gap-1">            <button              type="button"              aria-label="Decrease quantity"              disabled={quantity <= 1}              class="flex size-6 items-center justify-center rounded border text-xs hover:bg-accent disabled:pointer-events-none disabled:opacity-30"              onClick={() => { onQuantityChange?.(Math.max(1, quantity - 1)); }}            >            </button>            <span class="w-6 text-center text-xs">{quantity}</span>            <button              type="button"              aria-label="Increase quantity"              class="flex size-6 items-center justify-center rounded border text-xs hover:bg-accent"              onClick={() => { onQuantityChange?.(quantity + 1); }}            >              +            </button>          </div>        </div>        <div class="flex flex-col items-end gap-2">          <span class="text-sm font-medium">{price}</span>          {onRemove && (            <button type="button" aria-label="Remove item" class="text-xs text-muted-foreground hover:text-destructive" onClick={onRemove}>              Remove            </button>          )}        </div>      </div>    );  }}

Install the following dependencies:

npm install @praxisjs/css
pnpm add @praxisjs/css
yarn add @praxisjs/css
bun add @praxisjs/css

Copy and paste the following code into your project.

cart-item.tsx
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class CartItemStyles extends Stylesheet {  $root = this.css({ display: "flex", alignItems: "center", gap: "0.75rem", padding: "0.75rem 0" });  $image = this.css({ height: "3.5rem", width: "3.5rem", flexShrink: 0, borderRadius: "0.375rem", objectFit: "cover" });  $body = this.css({ display: "flex", minWidth: 0, flex: "1 1 0%", flexDirection: "column", gap: "0.125rem" });  $name = this.css({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: "0.875rem", fontWeight: 500 });  $variant = this.css({ fontSize: "0.75rem", color: t.mutedForeground });  $stepper = this.css({ marginTop: "0.25rem", display: "flex", alignItems: "center", gap: "0.25rem" });  $stepperButton = this.css({    display: "flex",    height: "1.5rem",    width: "1.5rem",    alignItems: "center",    justifyContent: "center",    borderRadius: "0.25rem",    border: `1px solid ${t.border}`,    fontSize: "0.75rem",    cursor: "pointer",  })    .on("&:hover", { backgroundColor: t.accent })    .on("&:disabled", { pointerEvents: "none", opacity: 0.3 });  $quantity = this.css({ width: "1.5rem", textAlign: "center", fontSize: "0.75rem" });  $right = this.css({ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: "0.5rem" });  $price = this.css({ fontSize: "0.875rem", fontWeight: 500 });  $remove = this.css({ fontSize: "0.75rem", color: t.mutedForeground, cursor: "pointer" }).on("&:hover", {    color: t.destructive,  });}export interface CartItemProps {  image?: string;  name: string;  variant?: string;  price: string;  quantity: number;  onQuantityChange?: (quantity: number) => void;  onRemove?: () => void;  class?: string;}@Component()export class CartItem extends StatelessComponent<CartItemProps> {  @Styled(CartItemStyles) $s!: CartItemStyles;  render() {    const { image, name, variant, price, quantity, onQuantityChange, onRemove, class: cls } = this.props;    return (      <div data-slot="cart-item" class={cx(this.$s.$root, cls)}>        {image && <img src={image} alt={name} class={this.$s.$image} />}        <div class={this.$s.$body}>          <p class={this.$s.$name}>{name}</p>          {variant && <p class={this.$s.$variant}>{variant}</p>}          <div class={this.$s.$stepper}>            <button              type="button"              aria-label="Decrease quantity"              disabled={quantity <= 1}              class={this.$s.$stepperButton}              onClick={() => { onQuantityChange?.(Math.max(1, quantity - 1)); }}            >            </button>            <span class={this.$s.$quantity}>{quantity}</span>            <button              type="button"              aria-label="Increase quantity"              class={this.$s.$stepperButton}              onClick={() => { onQuantityChange?.(quantity + 1); }}            >              +            </button>          </div>        </div>        <div class={this.$s.$right}>          <span class={this.$s.$price}>{price}</span>          {onRemove && (            <button type="button" aria-label="Remove item" class={this.$s.$remove} onClick={onRemove}>              Remove            </button>          )}        </div>      </div>    );  }}

Examples

About

Purely presentational — no Morphos equivalent. The remove button only renders when onRemove is given; the quantity stepper's decrease button disables at quantity === 1 (use onRemove to go below that).

Usage

import { CartItem } from "@/components/ui/cart-item";

<CartItem
  image="/headphones.jpg"
  name="Wireless headphones"
  variant="Black"
  price="$89.00"
  quantity={1}
  onQuantityChange={(quantity) => updateQuantity(quantity)}
  onRemove={() => removeItem()}
/>

Props

PropTypeDefault
imagestring
namestring
variantstring
pricestring
quantitynumber
onQuantityChange(quantity: number) => void
onRemove() => void
classstring

On this page