Skip to main content

Add-Ons

Overviewโ€‹

Some products have optional add-ons that the customer may elect for, including scarves and accompanying products (ex. shirts/vests on suits PDPs via the QuickView modal).

Personalizationโ€‹

See Personalization on PDPs for more details.

Add-On Upsell Tilesโ€‹

The PDP loader fetches add-on product data via getAddOns in app/lib/api/storefront/product.ts, which reads the assoc.addons metafield on the main product and resolves each ID via getProductById. The result is exposed on the route as a deferred addOnsPromise and rendered inside a <Suspense><Await> boundary in products.$handle.tsx.

Each tile is rendered by AddOnUpsell (app/components/AddOnUpsell/) and behaves differently based on the add-on's variant count:

  • Single-variant add-ons (e.g. accessories with one SKU): Clicking Add directly updates the addOns map in PdpContext โ€” no modal.
  • Multi-variant add-ons (e.g. Suits shirts and vests): Clicking Add sets quickViewProduct and opens the QuickView aside via useAside().open('addOnQuickView').

The selector currently uses isSuitsVendor(vendor) to decide between the two paths. There's a TODO in AddOnUpsell to extend the QuickView path to any add-on with >1 variant regardless of vendor.

QuickView Modalโ€‹

app/components/AddOnQuickView/ renders an aside drawer for configuring multi-variant add-ons. Layout: image carousel on the left, product details + options + shipping notice + Save/View links on the right.

Component layoutโ€‹

  • addOnQuickViewAside.component.tsx โ€” thin wrapper that reads quickViewProduct from PdpContext and renders AddOnQuickView inside <Aside type="addOnQuickView">.
  • addOnQuickView.component.tsx โ€” main UI. Local selectedOptions state, resolves the selected variant via findSelectedVariant, builds the shipping notice via the helper below, and renders title/color/price + final-sale badge + options + arrival notification + Save Selections + View Full Details.
  • utils.tsx โ€” pure helper exporting getAddOnShippingNotice. See "Shipping notice" below.

Title handlingโ€‹

Shopify titles for add-ons typically follow "{Name} - {Color}". The modal strips the trailing color so the heading reads cleanly (e.g. "Ben Dress Shirt"), and renders the color separately in the subtitle. Implemented as title.split(' - ')[0].

Variant resolution & save buttonโ€‹

  • selectedOptions is local state inside the modal, keyed by option name.
  • resolvedVariant = findSelectedVariant({ variants }, selectedOptions) when all option groups are selected.
  • Save Selections is enabled only when resolvedVariant?.availableForSale is true.

Size-only OOS strikethroughโ€‹

The modal's isValueAvailable callback only flags Size options as OOS โ€” and only once every non-Size option has been selected. Non-Size options (Length, Fit, etc.) always render as available; the Save button is the gate for whether the resolved combination is actually purchasable. This matches PDP behavior so customers don't see misleading strikethroughs on initial open.

Shipping notice (getAddOnShippingNotice)โ€‹

A pure helper in utils.tsx that takes:

  • the add-on product (inner shape โ€” id, productType, variants, etc.),
  • the resolved variant (or null if the user hasn't selected all options),
  • SiteSettings,

and returns { productionStatus, preorderShipDate, tooltipText, alertBody }. It mirrors the per-status branching in ShippingNotice (RTS / WAITLIST / OOS / PREORDER) but is decoupled from usePDPContext, so it can be reused outside the PDP route.

Behavior:

  • Falls back to the first variant when resolvedVariant is null, so the notice always renders.
  • Uses SUIT_PRODUCTION_GROUP.includes(productType) to decide between siteSettings.suitsProducts.productionDays and siteSettings.dualSale.productionDays.
  • For non-suit products with siteSettings.dualSale.productionWeeks set, the RTS tooltip uses RTS_WEEKS(productionWeeks); otherwise RTS_DAYS(productionDays).

Tooltip text and alert body for each status:

StatusTooltipAlert body
RTS (in stock)RTS_DAYS(productionDays) (or RTS_WEEKS)Please allow ~24-48 hours before shipping. Shipping options available at checkout.
WAITLIST / OOStooltipMessaging.WAITLISTThis selection is currently unavailable. Please choose a different size.
PREORDER (with date)tooltipMessaging.PREORDERThis item is for preorder only and is expected to arrive by {date} via standard shipping.
PREORDER (no date yet)tooltipMessaging['ONLOAD-PREORDER']Please select your size for an estimated delivery.

The modal renders the result via <ArrivalNotification tooltipMessage={tooltipText} alertMessage={<p>{alertBody}</p>} /> so the copy lives in a beige info alert with the calendar icon. Callers should keep alertBody inline-safe (strings or inline JSX) since it's wrapped in a <p> at the call site.

Final-sale badgeโ€‹

Computed as isFinalSale = findTag('finalsale', tags) and rendered as <TextBadge variant="finalsale" /> directly below the price โ€” same call/placement as ProductInfo on the PDP.

Bundle-aware Add-to-Cart Quantityโ€‹

When the main PDP product is a bundle (listedProducts.length > 1), the Add-to-Cart button counts both the bundled products and the add-ons:

quantityToBeAdded = Object.keys(addOns).length + listedProducts.length

So a jacket+pants bundle with one add-on shows add to bag (3). Non-bundles with one add-on show add to bag (2). Implemented in app/components/AddToCart/addToCart.component.tsx.

Cart attributesโ€‹

Add-on cart lines get _READY-TO-SHIP and _SHIPPING_SPEED attributes via prepareReadyToShipProperty() โ€” add-ons (non-dress products) never carry MTO/Standard Production attributes since they don't have the Estimated Arrival option. See the Cart Line Attribute Assignment section for the full decision tree.