Offer Container
data-next-upsell="offer" activates a container-based enhancer that owns the offer card itself — the add and skip buttons live inside the container and the enhancer manages all of their state. Use this when the offer card is a self-contained widget with its own quantity controls, options, or in-card display elements.
<div data-next-upsell="offer" data-next-package-id="42">
<h2>Add Extra Battery Pack?</h2>
<p>Extend your flight time with a spare battery.</p>
<p>Price: <span data-next-display="package.price">$29.99</span></p>
<button data-next-upsell-action="add">Yes, add to my order</button>
<button data-next-upsell-action="skip">No thanks</button>
</div>Action Buttons
Place one or more buttons inside the container with data-next-upsell-action:
| Value | Behavior |
|---|---|
add (or accept) | Submits the upsell to the order API and navigates to the accept URL |
skip (or decline) | Marks the offer as skipped, applies next-skipped to the container, and navigates to the decline URL |
Both values resolve their redirect URL in this order:
data-next-urlon the button itself<meta name="next-upsell-accept-url">(foradd/accept) or<meta name="next-upsell-decline-url">(forskip/decline)- If neither is present, the loading overlay is dismissed and the page stays put.
Display Elements Inside the Container
Any data-next-display element inside the container reflects the current package's data. When using a selector (see Selectors), the displays update as the customer changes their choice.
<div data-next-upsell="offer" data-next-package-id="42">
<img data-next-display="package.image" alt="">
<h3 data-next-display="package.name">Battery Pack</h3>
<p>
<span data-next-display="package.price">$29.99</span>
<s data-next-display="package.originalPrice" data-hide-if-zero="true"></s>
</p>
<p data-next-show="package.hasSavings">
Save <span data-next-display="package.savingsPercentage">0%</span>
</p>
<button data-next-upsell-action="add">Yes, add it!</button>
<button data-next-upsell-action="skip">No thanks</button>
</div>Skip Behavior
When the customer clicks a skip button:
- The container gets the
next-skippedCSS class. - All action buttons are disabled and get the
next-disabledclass. - The package is recorded in
orderStore.upsellJourneywith actionskipped. - The
upsell:skippedevent fires. - The page navigates to the decline URL if one is configured.
The next-skipped class lets you fade or hide the offer in CSS without JavaScript:
[data-next-upsell].next-skipped { opacity: 0.5; pointer-events: none; }Processing State
While the API call is in flight:
- The container gets
next-processing. - All action buttons are disabled and get
next-disabled. - A full-page loading overlay is shown until the call resolves.
After a successful submit:
- The container gets
next-success. - The page navigates to the accept URL after a 100 ms delay.
If the API call fails:
- The error message is rendered into a
[data-next-error-message]element inside the container, if one exists. upsell:erroris emitted with the error message.- The loading overlay is dismissed.
- If a redirect URL is configured, the page still navigates after a 1 s delay so the customer is not stranded.
Programmatic Skip Without Button
There is no programmatic accept method on this enhancer. To accept programmatically, use the Accept Button pattern with triggerAcceptUpsell() instead.
Conflicts
- Do not nest a
data-next-action="accept-upsell"button inside adata-next-upsell="offer"container for the same package. Both enhancers will respond to the click and submit the upsell twice. - Do not use
data-next-upsell-action="add"anddata-next-upsell-action="skip"on the same button —addalways wins. - Do not write to
cartStorefrom inside an upsell container. The cart is settled at this point and is not the source of truth for accepted upsells.
Next Steps
- Add variant or quantity options: Selectors
- Add per-quantity pricing: Quantity
- All container attributes: Reference → Attributes