Skip to content
Twilight React Playground
ثيم رائدaren

AddToCartForm

componentBeginnerserverbrowserlive demo

The product page buy box: options, notes and files, live price, quantity and the Add to cart button, submitted through Salla's SDK.

import { AddToCartForm } from '@salla.sa/twilight-theme-engine/components/product';

In plain words

This is the part of a product page where the shopper chooses and buys: size or color options, a quantity, a note, and the Add to cart button. Hand it a product and it draws every input that product needs.

When the shopper submits, the Salla SDK sends the request and shows the "added" message; when they change an option, the SDK works out the new price.

It is lazy: its code downloads only when it first renders, so wrap it in React's <Suspense>, which shows a fallback while that happens.

Signature

const AddToCartForm: React.LazyExoticComponent<(props: {
  product: Product;             // the product details, with options
  stickyAddToCart?: boolean;    // the button's sticky bar on small screens
  giftingIntro?: string;        // subtitle of the gifting widget
  formStartSlot?: ReactNode;    // before the options
  formEndSlot?: ReactNode;      // after notes and files, before the price
}) => JSX.Element>
// The props type is not exported: use React.ComponentProps<typeof AddToCartForm>.

Try it live

The buy box for a real product. Pressing its button adds that product to your own guest cart on the demo store.Try this: step through the products: those with options get option inputs once the SDK is ready. Then type into both slot knobs to see where each lands.
Real requests to the demo store
Storefront canvas · en · LTR

Loading products…

Controls
stickyAddToCart
What a theme writes
import { Suspense } from 'react';
import { AddToCartForm } from '@salla.sa/twilight-theme-engine/components/product';
import type { Product } from '@salla.sa/twilight-theme-engine/types';

// `product` must be the details (product.queries.detail): list products carry no options.
export function BuyBox({ product }: { product: Product }) {
  return (
    <Suspense fallback={null}>
      <AddToCartForm
        product={product}
        formStartSlot={<p className="text-sm">{'Ships in 2 days'}</p>}
      />
    </Suspense>
  );
}

Example

app/components/product/BuyBox.tsx
import { Suspense } from 'react';
import { AddToCartForm } from '@salla.sa/twilight-theme-engine/components/product';
import type { Product } from '@salla.sa/twilight-theme-engine/types';

export function BuyBox({ product, sticky }: { product: Product; sticky: boolean }) {
  return (
    <Suspense fallback={null}>
      <AddToCartForm
        product={product}
        stickyAddToCart={sticky}
        formStartSlot={<p className="text-sm">Ships in 2 days</p>}
      />
    </Suspense>
  );
}

How it behaves

  • Submitting calls Salla.form.onSubmit('cart.addItem', event): the SDK reads the form's id, quantity, options, notes and files. Any change calls Salla.product.getPrice(new FormData(form)), but only after form.reportValidity() passes, which also shows the browser's messages for required options still empty.

  • What renders depends on the product: options (options), bundles (has_bundle_products), weight and size guide, note and file inputs (can_add_note, can_upload_file), a pre-order countdown (has_preorder_campaign with preorder), and gifting (giftable). The quantity input is hidden for is_hidden_quantity and bookings, and capped at max_quantity.

  • Prices are the product's own sale_price, regular_price, starting_price or price followed by CurrencySymbol, not useMoney().format: no thousands separator and no Arabic digits.

  • The button label is product.add_to_cart_label; when that is empty the Salla button uses its own.

  • notify_availability (its channels, subscribed and subscribed_options) is passed on to the Salla button, which runs the notify-me flow for an unavailable product.

  • The engine's product page renders it as <AddToCartForm product={product} stickyAddToCart={theme.settings.sticky_add_to_cart} />, between the product:single.form.start and .end hook slots.

Gotchas

  • Give it the product details (product.queries.detail(id)), not a list item: list answers carry has_options but no options, so the option inputs never appear.

  • product.queries.detail(id) first answers with the product's list card when a list on the page already loaded it (placeholderData, with isPlaceholderData: true). That stand-in has no options either, so wait for isPlaceholderData to be false before rendering the form, or the options appear a moment later (src/api/product.ts).

  • Option inputs appear only after Salla.onReady() resolves, and the form first calls Salla.config.set('page.id', product.id). Rendering it outside a product page (a quick view on a category page) changes the SDK's current page id to that product.

  • React.lazy export: without a <Suspense> of your own, the nearest one above it (often the route's pending UI) replaces its whole area while the code downloads.

Related

Source and docs