Sticky Add-to-Cart Bar in Chat: PDP-Aware Conversion on Every Format
A sticky in-chat ATC bar that follows the visitor across the conversation when they're on a product page — JSON-LD-driven detection, full variant grid with OOS handling, native + headless cart support.
In-chat add-to-cart on a product card works when the AI just recommended that product. But what about when the visitor is already on the PDP, opens the chat to ask a question, and wants to buy the product they were already looking at? The recommendation card might never appear in the conversation. The visitor has the product on screen but no obvious way to add it without leaving chat.
The sticky ATC bar fixes that. When the visitor is on a product page, a persistent bar pins to the top of the chat surface with the product image, name, price, variant picker, and an Add to Cart button. It stays visible across the entire conversation, regardless of how the chat scrolls or what the AI is currently saying.
How the widget knows it's on a PDP
The bar only renders when the page is a product detail page. Detection runs a three-source cascade:
- JSON-LD Product schema — the most reliable source. Shopify themes ship Product schema in
<script type="application/ld+json">on every PDP. The widget parsesname,offers, and per-variantavailabilityfrom this single block. - Open Graph tags — fallback when JSON-LD isn't present. Reads
og:type="product"plusog:title/og:description/product:price:amount. - ShopifyAnalytics.meta.product — Shopify-specific window object that's present on most theme installs as a final fallback.
The first source that returns a product wins. The bar renders if and only if a product was detected; on collection pages, the homepage, the cart page, anywhere that isn't a PDP, the bar stays hidden.
The bar layout
The bar is intentionally simple. Left to right: product thumbnail (from the Open Graph image or the JSON-LD image field), product name (truncated if long), price (from offers.price), and an Add to Cart button.
The button has four states the user can see:
- Add to Cart — default state when the product is single-variant or has only the Default Title variant. One click adds.
- Select Option — multi-variant products with real options. Clicking opens the variant grid inline.
- Adding... — request in flight (briefly visible).
- Added ✓ — success state, persistent until the visitor changes pages or the conversation resets.
Error states (cart API failure, headless callback returning failure) flip the button to a red error state instead of the green success — the visitor sees the failure rather than a false confirmation.
Variant picker mirrors the PDP
For multi-variant products, the picker behaves the way a well-designed PDP picker does:
- All variants render in the grid, including out-of-stock variants. The visitor sees the full color or size landscape — they don't assume the product is unavailable when one variant happens to be sold out.
- OOS variants are disabled with a strikethrough. Visually present but unclickable. Mirrors the standard ecommerce pattern across major platforms.
- The first available variant is selected by default, so the "just click Add" path works without the visitor explicitly picking.
- Variant availability comes from JSON-LD — each
offerentry has anavailabilityfield; the widget parses "https://schema.org/InStock" vs other states and disables OOS rows accordingly.
The picker matches the behavior of the in-chat ATC button on product cards — same component, same rules — so the visitor sees consistent picker UX whether they got to the Add action through a chat recommendation or through the sticky PDP bar.
Across all four widget formats
The chat surface has four formats: chat bubble, search bar, slideout panel, smart banner. The sticky ATC bar renders in all four. The implementation lives in a shared StickyATCBar component that's mounted at the top of the message area regardless of format.
That coverage matters because the format choice is independent of the visitor's page context. A merchant might use chat bubble on the homepage and search bar on collection pages and slideout on PDPs. Whichever format the visitor encounters, if they're on a PDP and they open the chat surface, they see the bar. The merchant doesn't have to think about which format gets ATC — they all do.
Native + headless cart support
The bar uses the same cart-mode contract as in-chat ATC on product cards:
- Native (
cart_mode: ajax) — POSTs to/cart/add.jswith the_millyChatAssistedattribution flag, dispatchescart:build+cart:updatedDOM events for theme-side cart UIs to refresh. - Headless (
cart_mode: callback) — emits anadd_to_cartcallback viawindow.MillyChat.on()with the variant data, and waits for the host page to callMillyChat.addToCartResult()before flipping to the success or error state.
Same contract, same data shape, same callback API. The sticky bar isn't a different cart path — it's a different entry point into the same path. Native and headless merchants get identical PDP-aware ATC behavior; the merchant just configures cart-mode once.
Frequently Asked Questions
What is the sticky add-to-cart bar and when does it appear?
It's a persistent bar that pins to the top of the chat surface whenever the visitor is on a product page, showing the product image, name, price, a variant picker, and an Add to Cart button. It stays visible across the whole conversation, so a shopper who opened chat to ask a question about the product they're viewing can buy it even if the AI never surfaces a recommendation card. On non-product pages it stays hidden.
How does the widget know the visitor is on a product page?
It runs a three-source cascade: Shopify's JSON-LD Product schema first (the most reliable), then Open Graph product tags, then the ShopifyAnalytics.meta.product window object as a final fallback. The first source that returns a product wins, and the bar renders only when a product is detected — on collection pages, the homepage, or the cart it stays hidden.
Does the sticky bar work across widget formats and on headless stores?
Yes. It renders in all four formats — chat bubble, search bar, slideout panel, and smart banner — from one shared component, and it uses the same cart contract as in-chat add to cart. Native stores POST to /cart/add.js; headless stores get the add_to_cart callback and report back with addToCartResult. It's a different entry point into the same cart path, so native and headless merchants get identical behavior.