Threshold-Driven Free Shipping, Deluxe Sampling and a First-Order Gift Notice
Merchandiser-set spend thresholds evaluated in Liquid on every render of the cart section: free-shipping and deluxe-sampling messaging with a remaining-amount placeholder, a first-order gift notice that can tell a guest from a customer, and a free add-on whose control posts to the Cart AJAX API and asks the server for the section again.
Shopify's cart knows its total and nothing about what that total unlocks. This cart section reads spend thresholds and their copy from theme settings and resolves them in Liquid on every render: how far a shopper is from free shipping, how far from the deluxe sampling tier, and what a first order earns. A merchandiser changes a campaign by editing a setting, not by shipping a deploy.
The Problem
Shopify's cart knows its total and nothing about what that total unlocks. A free-shipping threshold lives in shipping settings, where a shopper can't see it until checkout. A gift with purchase has no native primitive the Online Store theme can call. Each of those is a merchandising rule that needs a place in the theme, a way for a merchandiser to change it per campaign without a developer, and a way to be recomputed the moment the cart changes.
Nudestix runs promotions continuously. The cart page had to say how far a shopper is from free shipping, how far from the deluxe sampling tier and what a first order earns, and to offer the one free add-on a campaign includes — every number and every sentence of it from settings a merchandiser edits between campaigns.
The Constraint
A theme gets no discount primitive for a free item. A free add-on has to be a real product with a real variant, added as a real line item through the Cart AJAX API. Thresholds move with every quantity edit, so nothing about them could be computed once at page load; they had to be recomputed inside the cart section's re-render, in Liquid, from cart.total_price.
That section already carried other readers of the same line items — a loyalty-points column, gift lines the shopper can't edit, a cross-sell rail, all covered on the cart page's own build. The incentive rows are one more reader of the same render. And every rule had to be a theme setting, because campaigns on this storefront are switched by merchandising: the thresholds, the copy around them, the gift card's image and text and the add-on product are all global settings, and none of them is a literal in the section.
What We Built
Everything lives in main-cart-items.liquid, the storefront's cart section, and cart.js, the module that re-renders it.
Threshold rows. At the top of the cart, a .main-cart-banners block reads settings.free_shipping_value, multiplies it by 100 to match cart.total_price in cents, and compares. Over the line, it prints settings.free_shipping_txt beside a check icon. Under it, it subtracts, formats the remainder with money, and substitutes it into a merchandiser-written sentence through replace: '${addMoreForFreeShip}' — the placeholder is documented in the setting's own help text, so the copy is the merchandiser's and only the number is ours. The deluxe sampling tier is the same pattern a second time: settings.deluxe_sampling_value, settings.deluxe_sampling_txt, and a ${addMoreForDeluxeSampling} placeholder in settings.deluxe_sampling_left.
First-order gift notice. Behind settings.enable_gwp_block, the section checks customer. A signed-in customer with orders_count == 0 sees the gift image, title and description from settings. A guest sees the same card with the description swapped for routes.account_register_url and routes.account_login_url links, asking them to confirm first-order eligibility rather than being promised a gift the theme can't verify. The block is a notice. It adds nothing to the cart.
The free add-on row. Behind settings.recycle_product, the section renders one more card below the line items: the product's featured image, a heading, a title and a description, each from its own setting and each skipped when the setting is blank, and an "add for free" control carrying data-var with the variant id of selected_or_first_available_variant. When that variant isn't available the card stays where it is with the control disabled and relabeled. The product is a setting, so which item a campaign gives away is a merchandising decision.
The add path. In cart.js, the CartItems element binds the add-on's control. The handler calls preventDefault, locks the control, shows its spinner, posts {id, quantity: 1} to /cart/add.js, and then calls refreshCartSections(), which fetches /cart?sections= with the ids from getSectionsToRender() — the cart items section, the header bubble and the live region — and swaps the returned HTML in. The Liquid that comes back reads the new total and prints both threshold rows again from it.
Why This Way
Putting the threshold arithmetic in the section render is the point. cart.total_price is already there, in cents, on every render; comparing it against a number a merchandiser typed into settings is two filters of Liquid. The browser never learns what a threshold is — it receives a sentence with the number already in it. When a quantity changes or the free item is added, the section is fetched again and the same comparison runs against the new total.
Expressing every rule as a theme setting is what makes a campaign a merchandising task rather than a ticket: change the number, rewrite the sentence around it, point the add-on at a different product. The placeholder convention is what makes the sentence theirs and not ours — the merchandiser writes the whole line and marks the spot where the remaining amount goes.
Three things come with it. The thresholds are one number each for the whole store, compared against the cart total in cents, with no per-market or per-customer variant in this design. A free item is a real product, so merchandising maintains its SKU and its stock like any other, and the card follows the variant's availability rather than a campaign flag. And the placeholder convention — a ${...} token inside a merchandiser's sentence — is one more thing a new team member has to be told.
Why Not an App
Gift-with-purchase and free-shipping-bar apps are a mature category, and this is the textbook case for one. The reason not to is what else is in this cart section. The same render computes a loyalty-points column, marks gift lines the shopper can't edit and feeds a cross-sell rail; every one of those reads the cart's line items in Liquid on the same pass. The threshold rows are one more reader of that object, and their copy is edited in the same theme settings as the rest of the cart's copy.
A shipping-bar app is a smaller version of the same trade: it draws the bar in the browser from its own copy of the threshold, so the number then exists twice — once where the merchandiser edits it, once where the app keeps it. Keeping the rows in the section costs Liquid in a file that is already long, and keeps the number in the settings the rest of the cart already reads.
Implementation Notes
- The threshold compare converts a plain number in settings to cents once,
settings.free_shipping_value | times: 100, and the setting's help text tells the merchandiser to enter it without currency formatting. - The remaining amount is run through
moneybefore it is substituted into the merchandiser's sentence, so the currency formatting is Shopify's rather than the copy's. - The two tiers are two independent settings groups:
free_shipping_valuewith its success and remaining-amount copy, anddeluxe_sampling_valuewith its own, each carrying its own placeholder token. - Both rows are gated on
cart.item_count > 0, so the empty cart renders its empty state and nothing else. - The gift notice resolves its two states —
show_first_order_gwpfor a signed-in customer with no orders,show_first_order_gwp_account_promptfor a visitor — into flags before the card renders, so the card markup has one shape and the description is the only thing that changes. - The add-on control carries
data-cart-pageon the cart template, so the handler incart.jsknows which render target it is refreshing. - The add-on's image, heading, title and description are four separate settings and each is skipped when blank, so a campaign can run the card with a title and no description.
Edge Cases
- An empty cart shows the empty-state copy and no threshold rows; the banner block is gated on
cart.item_count > 0. - A guest is never told they have a first-order gift. The card asks them to create an account or log in so the theme can read
orders_countbefore it promises anything. - Over the line, each tier prints its success sentence beside a check mark; under it, the same row prints the merchandiser's remaining-amount sentence with the number substituted in. There is no third state.
- The two tiers are switched independently. A campaign can run free-shipping messaging with the deluxe tier off, or the reverse, without touching the section.
- An add-on whose variant is unavailable keeps its card, with the control disabled and the label switched, rather than disappearing from the cart.
- The add request is a plain
/cart/add.jspost with the variant id; if it fails, the loading overlays are hidden and the control stays locked until the next render.
Platform Primitives Used
cart-ajax-api—/cart/add.jsfor the free add-on, with{id, quantity: 1}and the variant id read from the control.section-rendering-api—refreshCartSections()fetches/cart?sections=for the cart section, the header bubble and the live region after every add, so all threshold state comes back as Liquid.liquid-schema-settings— every rule is a global theme setting: two thresholds with their copy and placeholders, the gift-block toggle and its content, and the add-on product.
Where It Runs
The cart page, on the storefront's one cart template. The rows render inside main-cart-items, which is the section the Cart AJAX handlers already re-render after every mutation, so the incentive state refreshes with the line items rather than on its own timer.
What This Demonstrates
- Spend-threshold incentives and free-shipping meters — the primary capability: thresholds in theme settings, remaining-amount messaging computed in Liquid, re-rendered with the cart.
- Gift with purchase and free-sample mechanics — a first-order gift notice that tells a customer from a guest, and a free add-on that goes into the cart as a real line item through the Cart AJAX API.
How We Know
One section file and one script from the theme, read directly, plus a live check of the storefront's cart on 2026-09-07 with an item in it: both threshold rows render with the copy and the numbers configured in settings, and a signed-out visitor sees the first-order card with the sign-in prompt rather than the gift. Documented from the code; no client-side record of the decision survives.
Related Builds
- Cart page rebuilt around loyalty points, protected gifts and a dark order summary — the cart section this stack renders inside: the two-column layout, the points column and the gift lines a shopper can't edit.
- Inline size picker on cart and PDP upsell cards — Three Ships. A different storefront's cart-level add path, where the card has to choose a variant before it can post.
- Rotating announcement carousel above the header, with pause control — the same storefront's promo calendar one level up, where the threshold is announced before the cart proves it.
The Buy-vs-Build Question
A free-shipping bar app buys a bar and its own copy of your threshold. A gift app buys a rule that lives outside the theme the cart is rendered by. Building this kept both numbers in the settings a merchandiser already edits, and cost a cart section that is long and has to be read carefully. Both halves of that trade have their own decision pages: Free-shipping thresholds and logic: buy or build? and Gift with purchase: buy or build?
Provenance & Evidence
- Client: Nudestix — nudestix.com
- Surface: Cart
- Templates served: one —
cart - Complexity: High — the incentive rows, the gift notice and the add path across one cart section and its module
- Attribution: Deploi-authored. The threshold rows, the gift notice, the free add-on and the add path are ours. They render inside the storefront's cart section, which is built on Dawn 6.0.2 and documented on its own page.
- Status: Live, verified 2026-09-07
- Evidence: One section file and one script from the theme, plus a live cart check
- Confidence: Moderate — documented from the code; no client-side record of the decision survives
- Primary capability: Spend-threshold incentives and free-shipping meters
Ready for a Cart That Sells the Next Item Itself?
You dream it. We build it. If your promotions live in three apps and none of them can see the others, Contact us today and we'll put the rules where the cart already is.