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
addOnsmap inPdpContextโ no modal. - Multi-variant add-ons (e.g. Suits shirts and vests): Clicking Add sets
quickViewProductand opens the QuickView aside viauseAside().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 readsquickViewProductfromPdpContextand rendersAddOnQuickViewinside<Aside type="addOnQuickView">.addOnQuickView.component.tsxโ main UI. LocalselectedOptionsstate, resolves the selected variant viafindSelectedVariant, 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 exportinggetAddOnShippingNotice. 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โ
selectedOptionsis 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?.availableForSaleis 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
resolvedVariantis null, so the notice always renders. - Uses
SUIT_PRODUCTION_GROUP.includes(productType)to decide betweensiteSettings.suitsProducts.productionDaysandsiteSettings.dualSale.productionDays. - For non-suit products with
siteSettings.dualSale.productionWeeksset, the RTS tooltip usesRTS_WEEKS(productionWeeks); otherwiseRTS_DAYS(productionDays).
Tooltip text and alert body for each status:
| Status | Tooltip | Alert body |
|---|---|---|
| RTS (in stock) | RTS_DAYS(productionDays) (or RTS_WEEKS) | Please allow ~24-48 hours before shipping. Shipping options available at checkout. |
| WAITLIST / OOS | tooltipMessaging.WAITLIST | This selection is currently unavailable. Please choose a different size. |
| PREORDER (with date) | tooltipMessaging.PREORDER | This 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.