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

ProductDetails

componentBeginnerserverbrowserlive demo

The information half of a product page: brand, name, rating, price, description, tags, share and wishlist, SKU and stock counters.

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

In plain words

Everything a product page says about the product, beside the gallery: brand logo, name, subtitle, rating stars, price, description (with "Read more" when it is long), tags, a share button and the sold and remaining counts.

It is lazy, so it goes inside <Suspense>.

Signature

const ProductDetails: React.LazyExoticComponent<(props: {
  product: Product;
  showTags?: boolean;   // true
}) => JSX.Element>
// The props type is not exported: use React.ComponentProps<typeof ProductDetails>.

Try it live

The text half of a product page for a real product: brand, name, price, description, share and wishlist buttons.Try this: step through the products: a long description gets a "Read more" button, and a product with tags shows them unless showTags is off.
Storefront canvas · ar · RTL

Loading products…

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

export function ProductInfo({ product }: { product: Product }) {
  return (
    <Suspense fallback={<div className="h-40 animate-pulse" />}>
      <ProductDetails product={product} />
    </Suspense>
  );
}

Example

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

export function ProductInfo({ product }: { product: Product }) {
  const { settings } = useTheme();
  return (
    <Suspense fallback={<div className="h-40 animate-pulse" />}>
      <ProductDetails product={product} showTags={settings.show_tags} />
    </Suspense>
  );
}

How it behaves

  • It renders three hook slots with context={{ product }}: product:single.description.start before everything, product:single.description after the availability section, and product:single.description.end at the end.

  • The description is inserted as HTML (dangerouslySetInnerHTML), after replacing every &nbsp; with a newline character, which HTML then shows as an ordinary space. With has_read_more it is clamped to 5.25rem until "Read more" is pressed; the button expands only, and stays visible afterwards.

  • Prices are printed as the API sends them with CurrencySymbol, as in AddToCartForm, not through useMoney.

  • Also shown when present: brand logo (engine Link to the brand), is_taxable note, sold_quantity (the pages.products.sold_times translation, in Laravel plural format), can_show_remained_quantity, sku, branch availability (show_availability, dispatches scopes::open), SallaInstallment and SallaMetadata.

Gotchas

  • On the engine's product page every product:single.description* handler renders twice: ProductPage renders slots with the same three names around ProductDetails (without context), and ProductDetails renders its own (with { product }). In one of the two, product is undefined, so a handler that reads it without a check throws there.

  • The wishlist button calls window.Salla.wishlist.toggle(product.id) directly and has no "added" state, unlike the card's heart, which follows useWishlist. It is hidden below the sm breakpoint.

  • The Read more elements use fixed ids (more-content, btn-show-more), duplicated if two render on one page.

Related

Source and docs