Cart

The Cart component displays the items in the user's cart, along with a summary of the total cost.

  • Displays the items in the user's cart
  • Displays a summary of the total cost
  • Allows the user to remove items from the cart
  • Allows the user to update the quantity of items in the cart

Installation

Add the following Soul components

The cart component uses the streamable, button-link, skeleton, section-layout, sticky-sidebar-layout, button, toaster, input, field-error and chip components. Make sure you have added them to your project.

Install the following dependencies

npm install @conform-to/react @conform-to/zod clsx lucide-react zod

Copy and paste the following code into your project

sections/cart/index.tsx

import { Stream, Streamable } from '@/vibes/soul/lib/streamable';import { ButtonLink } from '@/vibes/soul/primitives/button-link';import * as Skeleton from '@/vibes/soul/primitives/skeleton';import { SectionLayout } from '@/vibes/soul/sections/section-layout';import { StickySidebarLayout } from '@/vibes/soul/sections/sticky-sidebar-layout';import { CartClient, Cart as CartData, CartLineItem, CartProps } from './client';export { type CartLineItem } from './client';export function Cart<LineItem extends CartLineItem>({  cart: streamableCart,  decrementLineItemLabel: streamableDecrementLineItemLabel,  title = 'Cart',  summaryTitle = 'Summary',  ...props}: Omit<CartProps<LineItem>, 'cart'> & {  cart: Streamable<CartData<LineItem>>;}) {  return (    <Stream      fallback={<CartSkeleton summaryTitle={summaryTitle} title={title} />}      value={streamableCart}    >      {(cart) => <CartClient {...props} cart={cart} summaryTitle={summaryTitle} title={title} />}    </Stream>  );}export interface CartSkeletonProps {  className?: string;  placeholderCount?: number;  summaryPlaceholderCount?: number;  title?: string;  summaryTitle?: string;}export function CartSkeleton({  title = 'Cart',  summaryTitle = 'Summary',  placeholderCount = 2,  summaryPlaceholderCount = 3,}: CartSkeletonProps) {  return (    <StickySidebarLayout      className="group/cart text-[var(--cart-text,hsl(var(--foreground)))]"      sidebar={        <div>          <h2 className="mb-10 font-[family-name:var(--cart-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none @xl:text-5xl">            {summaryTitle}          </h2>          <div className="group-has-[[data-pending]]/cart:animate-pulse">            <div className="w-full" data-pending>              <div className="divide-y divide-[var(--skeleton,hsl(var(--contrast-300)/15%))]">                {Array.from({ length: summaryPlaceholderCount }).map((_, index) => (                  <div className="py-4" key={index}>                    <div className="flex items-center justify-between">                      <Skeleton.Text characterCount={10} className="rounded-md" />                      <Skeleton.Text characterCount={8} className="rounded-md" />                    </div>                  </div>                ))}              </div>              <div className="flex justify-between border-t border-[var(--skeleton,hsl(var(--contrast-300)/15%))] py-6 text-xl font-bold">                <div className="flex items-center justify-between">                  <Skeleton.Text characterCount={8} className="rounded-md" />                </div>                <div className="flex items-center justify-between">                  <Skeleton.Text characterCount={8} className="rounded-md" />                </div>              </div>            </div>          </div>          <Skeleton.Box className="mt-4 h-[58px] w-full rounded-full" />        </div>      }      sidebarPosition="after"      sidebarSize="1/3"    >      <div>        <h1 className="mb-10 font-[family-name:var(--cart-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none @xl:text-5xl">          {title}        </h1>        {/* Cart Line Items */}        <div className="group-has-[[data-pending]]/cart:animate-pulse">          <ul className="flex flex-col gap-5" data-pending>            {Array.from({ length: placeholderCount }).map((_, index) => (              <li                className="flex flex-col items-start gap-x-5 gap-y-4 @container @sm:flex-row"                key={index}              >                {/* Image */}                <Skeleton.Box className="aspect-square w-full max-w-24 rounded-xl" />                <div className="flex flex-grow flex-col flex-wrap justify-between gap-y-2 @xl:flex-row">                  <div className="flex w-full flex-1 flex-col @xl:w-1/2 @xl:pr-4">                    {/* Line Item Title */}                    <Skeleton.Text characterCount={15} className="rounded-md" />                    {/* Subtitle */}                    <Skeleton.Text characterCount={10} className="rounded-md" />                  </div>                  {/* Counter */}                  <div>                    <div className="flex w-full flex-wrap items-center gap-x-5 gap-y-2">                      {/* Price */}                      <Skeleton.Text characterCount={5} className="rounded-md" />                      {/* Counter */}                      <Skeleton.Box className="h-[44px] w-[118px] rounded-lg" />                      {/* DeleteLineItemButton */}                      <Skeleton.Box className="-ml-1 h-8 w-8 rounded-full" />                    </div>                  </div>                </div>              </li>            ))}          </ul>        </div>      </div>    </StickySidebarLayout>  );}export interface CartEmptyState {  title: string;  subtitle: string;  cta: {    label: string;    href: string;  };}export function CartEmptyState({ title, subtitle, cta }: CartEmptyState) {  return (    <SectionLayout className="text-center font-[family-name:var(--cart-font-family,var(--font-family-body))]">      <h1 className="mb-3 text-center font-[family-name:var(--cart-title-font-family,var(--font-family-heading))] text-3xl leading-none text-[var(--cart-title,hsl(var(--foreground)))] @xl:text-4xl">        {title}      </h1>      <p className="leading-normaltext-[var(--cart-subtitle,hsl(var(--contrast-500)))] mb-6 text-center @3xl:text-lg">        {subtitle}      </p>      <ButtonLink href={cta.href}>{cta.label}</ButtonLink>    </SectionLayout>  );}

sections/cart/client.tsx

'use client';import { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';import { parseWithZod } from '@conform-to/zod';import { clsx } from 'clsx';import { ArrowRight, Minus, Plus, Trash2 } from 'lucide-react';import Image from 'next/image';import {  ComponentPropsWithoutRef,  startTransition,  useActionState,  useEffect,  useOptimistic,} from 'react';import { useFormStatus } from 'react-dom';import { Button } from '@/vibes/soul/primitives/button';import { toast } from '@/vibes/soul/primitives/toaster';import { StickySidebarLayout } from '@/vibes/soul/sections/sticky-sidebar-layout';import { CouponCodeForm, CouponCodeFormState } from './coupon-code-form';import { cartLineItemActionFormDataSchema } from './schema';import { CartEmptyState } from '.';type Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;export interface CartLineItem {  id: string;  image: { alt: string; src: string };  title: string;  subtitle: string;  quantity: number;  price: string;}export interface CartSummaryItem {  label: string;  value: string;}export interface CartState<LineItem extends CartLineItem> {  lineItems: LineItem[];  lastResult: SubmissionResult | null;}export interface Cart<LineItem extends CartLineItem> {  lineItems: LineItem[];  summaryItems: CartSummaryItem[];  total: string;  totalLabel?: string;}interface CouponCode {  action: Action<CouponCodeFormState, FormData>;  couponCodes?: string[];  ctaLabel?: string;  disabled?: boolean;  label?: string;  placeholder?: string;  removeLabel?: string;}export interface CartProps<LineItem extends CartLineItem> {  title?: string;  summaryTitle?: string;  emptyState?: CartEmptyState;  lineItemAction: Action<CartState<LineItem>, FormData>;  checkoutAction: Action<SubmissionResult | null, FormData>;  checkoutLabel?: string;  deleteLineItemLabel?: string;  decrementLineItemLabel?: string;  incrementLineItemLabel?: string;  cart: Cart<LineItem>;  couponCode?: CouponCode;}const defaultEmptyState = {  title: 'Your cart is empty',  subtitle: 'Add some products to get started.',  cta: { label: 'Continue shopping', href: '#' },};/** * This component supports various CSS variables for theming. Here's a comprehensive list, along * with their default values: * * ```css * :root { *   --cart-focus: hsl(var(--primary)); *   --cart-font-family: var(--font-family-body); *   --cart-title-font-family: var(--font-family-heading); *   --cart-text: hsl(var(--foreground)); *   --cart-subtitle-text: hsl(var(--contrast-500)); *   --cart-subtext-text: hsl(var(--contrast-300)); *   --cart-icon: hsl(var(--contrast-300)); *   --cart-icon-hover: hsl(var(--foreground)); *   --cart-border: hsl(var(--contrast-100)); *   --cart-image-background: hsl(var(--contrast-100)); *   --cart-button-background: hsl(var(--contrast-100)); *   --cart-counter-icon: hsl(var(--contrast-300)); *   --cart-counter-icon-hover: hsl(var(--foreground)); *   --cart-counter-background: hsl(var(--background)); *   --cart-counter-background-hover: hsl(var(--contast-100) / 50%); * } * ``` */export function CartClient<LineItem extends CartLineItem>({  title,  cart,  couponCode,  decrementLineItemLabel,  incrementLineItemLabel,  deleteLineItemLabel,  lineItemAction,  checkoutAction,  checkoutLabel = 'Checkout',  emptyState = defaultEmptyState,  summaryTitle,}: CartProps<LineItem>) {  const [state, formAction] = useActionState(lineItemAction, {    lineItems: cart.lineItems,    lastResult: null,  });  const [form] = useForm({ lastResult: state.lastResult });  useEffect(() => {    if (form.errors) {      form.errors.forEach((error) => {        toast.error(error);      });    }  }, [form.errors]);  const [optimisticLineItems, setOptimisticLineItems] = useOptimistic<CartLineItem[], FormData>(    state.lineItems,    (prevState, formData) => {      const submission = parseWithZod(formData, { schema: cartLineItemActionFormDataSchema });      if (submission.status !== 'success') return prevState;      switch (submission.value.intent) {        case 'increment': {          const { id } = submission.value;          return prevState.map((item) =>            item.id === id ? { ...item, quantity: item.quantity + 1 } : item,          );        }        case 'decrement': {          const { id } = submission.value;          return prevState.map((item) =>            item.id === id ? { ...item, quantity: item.quantity - 1 } : item,          );        }        case 'delete': {          const { id } = submission.value;          return prevState.filter((item) => item.id !== id);        }        default:          return prevState;      }    },  );  const optimisticQuantity = optimisticLineItems.reduce((total, item) => total + item.quantity, 0);  if (optimisticQuantity === 0) {    return <CartEmptyState {...emptyState} />;  }  return (    <StickySidebarLayout      className="font-[family-name:var(--cart-font-family,var(--font-family-body))] text-[var(--cart-text,hsl(var(--foreground)))]"      sidebar={        <div>          <h2 className="mb-10 font-[family-name:var(--cart-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none @xl:text-5xl">            {summaryTitle}          </h2>          <dl aria-label="Receipt Summary" className="w-full">            <div className="divide-y divide-[var(--cart-border,hsl(var(--contrast-100)))]">              {cart.summaryItems.map((summaryItem, index) => (                <div className="flex justify-between py-4" key={index}>                  <dt>{summaryItem.label}</dt>                  <dd>{summaryItem.value}</dd>                </div>              ))}            </div>            {couponCode && (              <CouponCodeForm                action={couponCode.action}                couponCodes={couponCode.couponCodes}                ctaLabel={couponCode.ctaLabel}                disabled={couponCode.disabled}                label={couponCode.label}                placeholder={couponCode.placeholder}                removeLabel={couponCode.removeLabel}              />            )}            <div className="flex justify-between border-t border-[var(--cart-border,hsl(var(--contrast-100)))] py-6 text-xl font-bold">              <dt>{cart.totalLabel ?? 'Total'}</dt>              <dl>{cart.total}</dl>            </div>          </dl>          <CheckoutButton action={checkoutAction} className="mt-4 w-full">            {checkoutLabel}            <ArrowRight size={20} strokeWidth={1} />          </CheckoutButton>        </div>      }      sidebarPosition="after"      sidebarSize="1/3"    >      <div className="w-full">        <h1 className="mb-10 font-[family-name:var(--cart-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none @xl:text-5xl">          {title}          <span className="ml-4 text-[var(--cart-subtext-text,hsl(var(--contrast-300)))] contrast-more:text-[var(--cart-subtitle-text,hsl(var(--contrast-500)))]">            {optimisticQuantity}          </span>        </h1>        {/* Cart Items */}        <ul className="flex flex-col gap-5">          {optimisticLineItems.map((lineItem) => (            <li              className="flex flex-col items-start gap-x-5 gap-y-4 @container @sm:flex-row"              key={lineItem.id}            >              <div className="relative aspect-square w-full max-w-24 overflow-hidden rounded-xl bg-[var(--cart-image-background,hsl(var(--contrast-100)))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] focus-visible:ring-offset-4">                <Image                  alt={lineItem.image.alt}                  className="object-cover"                  fill                  sizes="(min-width: 28rem) 9rem, (min-width: 24rem) 6rem, 100vw"                  src={lineItem.image.src}                />              </div>              <div className="flex flex-grow flex-col flex-wrap justify-between gap-y-2 @xl:flex-row">                <div className="flex w-full flex-1 flex-col @xl:w-1/2 @xl:pr-4">                  <span className="font-medium">{lineItem.title}</span>                  <span className="text-[var(--cart-subtext-text,hsl(var(--contrast-300)))] contrast-more:text-[var(--cart-subtitle-text,hsl(var(--contrast-500)))]">                    {lineItem.subtitle}                  </span>                </div>                <CounterForm                  action={formAction}                  decrementLabel={decrementLineItemLabel}                  deleteLabel={deleteLineItemLabel}                  incrementLabel={incrementLineItemLabel}                  lineItem={lineItem}                  onSubmit={(formData) => {                    startTransition(() => {                      formAction(formData);                      setOptimisticLineItems(formData);                    });                  }}                />              </div>            </li>          ))}        </ul>      </div>    </StickySidebarLayout>  );}function CounterForm({  lineItem,  action,  onSubmit,  incrementLabel = 'Increase count',  decrementLabel = 'Decrease count',  deleteLabel = 'Remove item',}: {  lineItem: CartLineItem;  incrementLabel?: string;  decrementLabel?: string;  deleteLabel?: string;  action: (payload: FormData) => void;  onSubmit: (formData: FormData) => void;}) {  const [form, fields] = useForm({    defaultValue: { id: lineItem.id },    shouldValidate: 'onBlur',    shouldRevalidate: 'onInput',    onValidate({ formData }) {      return parseWithZod(formData, { schema: cartLineItemActionFormDataSchema });    },    onSubmit(event, { formData }) {      event.preventDefault();      onSubmit(formData);    },  });  return (    <form {...getFormProps(form)} action={action}>      <input {...getInputProps(fields.id, { type: 'hidden' })} key={fields.id.id} />      <div className="flex w-full flex-wrap items-center gap-x-5 gap-y-2">        <span className="font-medium @xl:ml-auto">{lineItem.price}</span>        {/* Counter */}        <div className="flex items-center rounded-lg border border-[var(--cart-counter-border,hsl(var(--contrast-100)))]">          <button            aria-label={decrementLabel}            className={clsx(              'group rounded-l-lg bg-[var(--cart-counter-background,hsl(var(--background)))] p-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] disabled:cursor-not-allowed',              lineItem.quantity === 1                ? 'opacity-50'                : 'hover:bg-[var(--cart-counter-background-hover,hsl(var(--contrast-100)/50%))]',            )}            disabled={lineItem.quantity === 1}            name="intent"            type="submit"            value="decrement"          >            <Minus              className={clsx(                'text-[var(--cart-counter-icon,hsl(var(--contrast-300)))] transition-colors duration-300',                lineItem.quantity !== 1 &&                  'group-hover:text-[var(--cart-counter-icon-hover,hsl(var(--foreground)))]',              )}              size={18}              strokeWidth={1.5}            />          </button>          <span className="flex w-8 select-none justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))]">            {lineItem.quantity}          </span>          <button            aria-label={incrementLabel}            className={clsx(              'group rounded-r-lg bg-[var(--cart-counter-background,hsl(var(--background)))] p-3 transition-colors duration-300 hover:bg-[var(--cart-counter-background-hover,hsl(var(--contrast-100)/50%))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] disabled:cursor-not-allowed',            )}            name="intent"            type="submit"            value="increment"          >            <Plus              className="text-[var(--cart-counter-icon,hsl(var(--contrast-300)))] transition-colors duration-300 group-hover:text-[var(--cart-counter-icon-hover,hsl(var(--foreground)))]"              size={18}              strokeWidth={1.5}            />          </button>        </div>        <button          aria-label={deleteLabel}          className="group -ml-1 flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors duration-300 hover:bg-[var(--cart-button-background,hsl(var(--contrast-100)))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] focus-visible:ring-offset-4"          name="intent"          type="submit"          value="delete"        >          <Trash2            className="text-[var(--cart-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--cart-icon-hover,hsl(var(--foreground)))]"            size={20}            strokeWidth={1}          />        </button>      </div>    </form>  );}function CheckoutButton({  action,  ...props}: { action: Action<SubmissionResult | null, FormData> } & ComponentPropsWithoutRef<  typeof Button>) {  const [lastResult, formAction] = useActionState(action, null);  const [form] = useForm({ lastResult });  useEffect(() => {    if (form.errors) {      form.errors.forEach((error) => {        toast.error(error);      });    }  }, [form.errors]);  return (    <form action={formAction}>      <SubmitButton {...props} />    </form>  );}function SubmitButton(props: ComponentPropsWithoutRef<typeof Button>) {  const { pending } = useFormStatus();  return <Button {...props} disabled={pending} loading={pending} type="submit" />;}

sections/cart/schema.ts

import { z } from 'zod';export const cartLineItemActionFormDataSchema = z.discriminatedUnion('intent', [  z.object({    intent: z.literal('increment'),    id: z.string(),  }),  z.object({    intent: z.literal('decrement'),    id: z.string(),  }),  z.object({    intent: z.literal('delete'),    id: z.string(),  }),]);export const couponCodeActionFormDataSchema = ({  required_error = 'Please enter a valid promo code',}: {  required_error?: string;}) =>  z.discriminatedUnion('intent', [    z.object({      intent: z.literal('apply'),      couponCode: z.string({ required_error }),    }),    z.object({      intent: z.literal('delete'),      couponCode: z.string(),    }),  ]);

sections/cart/coupon-code-form/index.tsx

'use client';import { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';import { parseWithZod } from '@conform-to/zod';import { startTransition, useActionState, useOptimistic } from 'react';import { useFormStatus } from 'react-dom';import { FieldError } from '@/vibes/soul/form/field-error';import { Input } from '@/vibes/soul/form/input';import { Button } from '@/vibes/soul/primitives/button';import { couponCodeActionFormDataSchema } from '../schema';import { CouponChip } from './coupon-chip';type Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;export interface CouponCodeFormState {  couponCodes: string[];  lastResult: SubmissionResult | null;}export interface CouponCodeFormProps {  action: Action<CouponCodeFormState, FormData>;  couponCodes?: string[];  ctaLabel?: string;  disabled?: boolean;  label?: string;  placeholder?: string;  removeLabel?: string;  requiredErrorMessage?: string;}export function CouponCodeForm({  action,  couponCodes,  ctaLabel = 'Apply',  disabled = false,  label = 'Promo code',  placeholder,  removeLabel,  requiredErrorMessage,}: CouponCodeFormProps) {  const [state, formAction] = useActionState(action, {    couponCodes: couponCodes ?? [],    lastResult: null,  });  const [optimisticCouponCodes, setOptimisticCouponCodes] = useOptimistic<string[], FormData>(    state.couponCodes,    (prevState, formData) => {      const submission = parseWithZod(formData, {        schema: couponCodeActionFormDataSchema({ required_error: requiredErrorMessage }),      });      if (submission.status !== 'success') return prevState;      switch (submission.value.intent) {        case 'delete': {          const couponCode = submission.value.couponCode;          return prevState.filter((code) => code !== couponCode);        }        default:          return prevState;      }    },  );  const [form, fields] = useForm({    lastResult: state.lastResult,    shouldValidate: 'onBlur',    shouldRevalidate: 'onInput',    onValidate({ formData }) {      return parseWithZod(formData, {        schema: couponCodeActionFormDataSchema({ required_error: requiredErrorMessage }),      });    },    onSubmit(event, { formData }) {      event.preventDefault();      startTransition(() => {        formAction(formData);        setOptimisticCouponCodes(formData);      });    },  });  return (    <div className="border-[var(--cart-border: var(--contrast-100),hsl(var(--contrast-100)))] space-y-2 border-t pb-5 pt-4">      <form {...getFormProps(form)} action={formAction} className="space-y-2">        <label htmlFor={fields.couponCode.id}>{label}</label>        <div className="flex gap-1.5">          <Input            {...getInputProps(fields.couponCode, {              required: true,              type: 'text',            })}            disabled={disabled}            errors={fields.couponCode.errors}            id={fields.couponCode.id}            key={fields.couponCode.id}            placeholder={placeholder}          />          <SubmitButton disabled={disabled}>{ctaLabel}</SubmitButton>        </div>      </form>      {optimisticCouponCodes.length > 0 && (        <div className="flex flex-wrap gap-1.5">          {optimisticCouponCodes.map((couponCode) => (            <CouponChip              action={formAction}              couponCode={couponCode}              key={couponCode}              onSubmit={(formData) => {                startTransition(() => {                  formAction(formData);                  setOptimisticCouponCodes(formData);                });              }}              removeLabel={removeLabel}            />          ))}        </div>      )}      {form.errors?.map((error, index) => <FieldError key={index}>{error}</FieldError>)}    </div>  );}function SubmitButton({ disabled, ...props }: React.ComponentPropsWithoutRef<typeof Button>) {  const { pending } = useFormStatus();  return (    <Button      {...props}      className="shrink-0"      disabled={disabled ?? pending}      loading={pending}      name="intent"      size="small"      type="submit"      value="apply"      variant="secondary"    />  );}

sections/cart/coupon-code-form/coupon-chip.tsx

import { getFormProps, getInputProps, useForm } from '@conform-to/react';import { parseWithZod } from '@conform-to/zod';import { Chip } from '@/vibes/soul/primitives/chip';import { couponCodeActionFormDataSchema } from '../schema';export interface CouponChipProps {  action: (payload: FormData) => void;  onSubmit: (formData: FormData) => void;  couponCode: string;  removeLabel?: string;}export function CouponChip({  couponCode,  removeLabel = 'Remove promo code',  onSubmit,  action,}: CouponChipProps) {  const [form, fields] = useForm({    onValidate({ formData }) {      return parseWithZod(formData, {        schema: couponCodeActionFormDataSchema({}),      });    },    onSubmit(event, { formData }) {      event.preventDefault();      onSubmit(formData);    },  });  return (    <form {...getFormProps(form)} action={action}>      <input        {...getInputProps(fields.couponCode, {          type: 'hidden',        })}        value={couponCode}      />      <Chip name="intent" removeLabel={removeLabel} value="delete">        {couponCode.toUpperCase()}      </Chip>    </form>  );}

Usage

Refer to the example above for a complete reference on how to use the Cart component.

API Reference

CartProps

NameTypeDefault
title
string
summaryTitle
string
emptyState
CartEmptyState
lineItemAction*
Action<CartState<LineItem>, FormData>
checkoutAction*
Action<SubmissionResult | null, FormData>
checkoutLabel
string
deleteLineItemLabel
string
decrementLineItemLabel
string
incrementLineItemLabel
string
cart*
Cart<LineItem>
couponCode
CouponCode

Cart

NameTypeDefault
lineItems*
LineItem[]
summaryItems*
CartSummaryItem[]
total*
string
totalLabel
string

CartState

NameTypeDefault
lineItems*
LineItem[]
lastResult*
SubmissionResult | null

CouponCode

NameTypeDefault
action*
Action<CouponCodeFormState, FormData>
couponCodes
string[]
ctaLabel
string
disabled
boolean
label
string
placeholder
string
removeLabel
string

CouponCodeFormState

NameTypeDefault
couponCodes*
string[]
lastResult
SubmissionResult | null

CartEmptyState

NameTypeDefault
title*
string
subtitle*
string
cta*
CTA

LineItem

NameTypeDefault
id*
string
image*
Image
title*
string
subtitle*
string
quantity*
number
price*
string

CartSummaryItem

NameTypeDefault
label*
string
value*
string

Image

NameTypeDefault
src*
string
alt*
string

CTA

NameTypeDefault
label*
string
href*
string

CSS Variables

This component supports various CSS variables for theming. Here's a comprehensive list.

:root {  --cart-focus: hsl(var(--primary));  --cart-font-family: var(--font-family-body);  --cart-title-font-family: var(--font-family-heading);  --cart-text: hsl(var(--foreground));  --cart-subtitle-text: hsl(var(--contrast-500));  --cart-subtext-text: hsl(var(--contrast-300));  --cart-icon: hsl(var(--contrast-300));  --cart-icon-hover: hsl(var(--foreground));  --cart-border: hsl(var(--contrast-100));  --cart-image-background: hsl(var(--contrast-100));  --cart-button-background: hsl(var(--contrast-100));  --cart-counter-icon: hsl(var(--contrast-300));  --cart-counter-icon-hover: hsl(var(--foreground));  --cart-counter-background: hsl(var(--background));  --cart-counter-background-hover: hsl(var(--contrast-100) / 50%);}