← Back to guides
ExperienceShopify SectionsOS 2.0 · Published 2026-06-15 · 11 min read

Shopify Cart Drawer Liquid Implementation Guide (Patterns That Survive Production)

Cart drawers break in subtle ways—stale counts, wrong free-shipping math, apps fighting AJAX. Here is how we implement and debug cart drawer Liquid on Dawn-style and custom themes.

Share with Shopify developers — useful guides spread faster in theme dev communities.

Cart drawers that show zero items while the checkout button says three are not rare — they are a Tuesday on client stores with custom headers, app snippets, and AJAX cart fragments fighting the same DOM. Dhruv Goyani wrote this guide after fixing drawer state bugs on four Dawn forks in one quarter.

The cart drawer is the highest-anxiety UI on a Shopify storefront. Merchants stare at it during BFCM; customers abandon when counts lie or checkout feels stuck. Implementing a cart drawer is half Liquid—rendering line items, discounts, notes—and half JavaScript orchestration against Shopify's Cart AJAX API and section rendering endpoints. We have shipped drawer carts on Dawn forks, premium themes, and fully custom builds. This guide documents Liquid patterns that survive production, JavaScript integration points you must not break, and debugging steps when apps enter the mix.

Architecture: section, snippet, and global JS

On modern themes, cart-drawer.liquid (section) holds markup for the slide-out panel: line item loop, totals, checkout button, empty state, optional upsell region. Snippets like cart-drawer-item.liquid isolate row markup. theme.liquid renders the section once globally so it is always in DOM—hidden off-canvas until opened. JavaScript in cart-drawer.js listens for add-to-cart events, fetches updated cart JSON, requests rendered HTML for cart-drawer section id, and swaps inner HTML. If you only customize Liquid without testing JS integration, you will ship beautiful broken carts. Study Dawn's file trio before customizing.

Core Liquid: cart object and line items

{%- if cart != empty -%}
  
{%- for item in cart.items -%}
{% if item.image %} {{ item.image.alt | escape }} {% endif %} {{ item.product.title | escape }}
{{ item.final_line_price | money }}
{%- endfor -%}
{%- else -%}

{{ 'sections.cart.empty' | t }}

{%- endif -%}

Use item.final_line_price after discounts, not raw line_price, unless you intentionally show strike pricing. Quantity inputs must align with JavaScript quantity update handlers—data-index attributes are the contract. Escape titles; product titles with quotes break attribute handlers. For translation keys, match locale files—hardcoded English in a multilingual store is a post-launch ticket.

Routes and URLs—never hardcode /cart

{{ routes.cart_url }}
{{ routes.cart_add_url }}
{{ routes.root_url }}
{{ routes.checkout_url }}

Markets and subfolder locales change paths. routes object keeps links valid. We grep client themes for href="/cart" on every audit—legacy hardcoding from HTML exports is common when teams skip Liquid basics review.

Free shipping progress bar Liquid

Merchants love "You are $12 away from free shipping." Implement threshold as theme setting—section.settings.free_shipping_threshold or global settings—and compare cart.total_price in cents. Render bar width as percentage capped at 100. Critical: mirror identical math in JavaScript after AJAX cart updates or the bar lies until full page refresh.

{%- assign threshold = settings.free_shipping_threshold | times: 100 -%}
{%- assign remaining = threshold | minus: cart.total_price -%}
{%- if remaining > 0 -%}
  

Spend {{ remaining | money }} more for free shipping.

{%- assign pct = cart.total_price | times: 100 | divided_by: threshold | at_most: 100 -%}
{%- else -%}

You qualify for free shipping!

{%- endif -%}

Sections API refresh pattern (conceptual)

After add to cart, JavaScript fetches cart.js for counts, then requests something like /?sections=cart-drawer,cart-icon-bubble to get fresh HTML partials. Your Liquid does not call this—the JS does. When customizing drawer markup, preserve id hooks the JS expects: CartDrawer, cart-icon-bubble, data-cart-count. Diff your theme against Dawn before renaming ids. If counts update but line items do not, the sections list in fetch likely omitted cart-drawer.

Accessibility requirements we enforce

  • trap focus inside open drawer; restore focus on close
  • aria-hidden toggles on backdrop and panel
  • Escape key closes drawer
  • visible focus states on remove and quantity controls
  • announce cart updates to screen readers via aria-live region

Dawn improved significantly here—regressions happen when agencies strip attributes for CSS convenience. Accessibility bugs are production bugs.

Empty cart and upsell states

Empty state should link to catalog routes—collections.all or a featured collection setting—not a dead end. Optional upsell region can render a collection picker setting and loop 2–4 products with quick add—coordinate quick add JS with drawer refresh. Do not upsell slow movers merchants flagged internally; use collection setting defaults per vertical.

Cart notes, attributes, and line item properties

Line item properties from product forms appear on item.properties—loop and display gift message, engraving, etc. Cart note field uses name="note" in cart form. Validate maxlength in JS; merchants paste essays. Properties affect fulfillment—do not hide them in drawer only to surprise warehouse on packing slips.

{%- for property in item.properties -%}
  {%- unless property.last == blank -%}
    
{{ property.first }}: {{ property.last }}
{%- endunless -%} {%- endfor -%}

Discounts, scripts, and Shopify Scripts legacy

Show cart.cart_level_discount_applications and line level discounts so totals match checkout psychology—surprise at checkout still happens but transparency helps. Automatic discounts change totals after AJAX; ensure refresh pulls full cart object not cached stale JSON. Plus Function discounts may not display identically in drawer versus checkout—document known gaps for Plus clients.

Cart API endpoints developers actually use

Beyond /cart.js, know /cart/add.js, /cart/change.js, and /cart/update.js for quantity and note changes. Drawer implementations should use the same endpoints Dawn uses—mixing APIs causes inconsistent line item indices. After change.js, re-fetch sections HTML rather than patching DOM by hand unless you enjoy off-by-one line errors. Read Shopify's Cart AJAX API reference when debugging; parameter names are strict.

fetch('/cart/change.js', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ line: 1, quantity: 2 })
})
  .then(() => fetch('/?sections=cart-drawer,cart-icon-bubble'))
  .then(res => res.json())
  .then(data => { /* swap section HTML */ });

App collisions in the drawer

Upsell apps inject widgets into drawer footer or replace checkout button. Read app blocks vs sections. Test with client's bundle app enabled—duplicate checkout buttons are embarrassing. Some apps hijack fetch; if drawer breaks only with app installed, file vendor ticket with reproduction on duplicate theme.

Drawer versus notification cart patterns

Some brands prefer mini notification toasts on add-to-cart instead of opening the full drawer. If you implement toasts, still refresh cart-icon-bubble counts. Hybrid UX—toast then optional "View cart" opening drawer—needs clear JS state machine or users get double animations. Pick one primary pattern per theme; A/B testing cart UX without analytics discipline wastes dev time.

Selling plans and dynamic checkout buttons

Subscription apps replace or augment checkout buttons in product forms; drawer checkout must remain valid for one-time purchases. Test mixed carts: subscription line plus one-time add-on. Dynamic checkout buttons (Shop Pay, Apple Pay) in drawer footer require Shopify payment settings enabled and correct form structure—broken wallet buttons erode trust on mobile. Follow Shopify docs for accelerated checkout placement; do not nest wallet buttons inside generic divs with pointer-events:none.

Custom section cart icon bubble

Header cart icon often lives in header section with bubble count separate from drawer section. Both must refresh together via sections API list. When we build custom headers, we extract cart-icon-bubble snippet shared between header and AJAX refresh targets—single source of truth for count markup.

Animating cart count changes without annoyance

Subtle scale or bounce on cart count updates gives feedback without opening the drawer—some brands prefer that for add-to-cart on collection pages. Keep animations under 200ms and respect prefers-reduced-motion. Do not animate on every quantity tweak inside the drawer or power users will disable JS. Match animation style to theme motion tokens if they exist.

{%- if cart == empty -%} {{ 'templates.cart.cart' | t }} {%- else -%} {%- endif -%}

Debugging production drawer bugs

Symptom checklist: count wrong—check cart-icon-bubble refresh; line items stale—sections param; free shipping wrong—JS math drift; drawer will not open—JS error from renamed id; checkout 404—routes hardcoded. Use duplicate theme, Chrome network tab on add-to-cart XHR, compare against Dawn behavior. Full debugging workflow in debugging production Liquid.

Performance considerations

Drawer HTML refetches should not pull entire header/footer—limit sections list. Debounce quantity changes. Lazy load upsell images. Avoid rendering twenty cross-sell products in drawer—CPU and DOM weight matter on mobile. Align with performance checklist.

Theme editor preview quirks for cart drawer

Theme editor preview may not fire identical JavaScript events as live storefront—test drawer on published duplicate with password protection. Editor cart sometimes uses synthetic data; empty cart states look fine in customize while live store has stale items from session. Always verify on incognito live URL after editor looks good. Cart note and attribute fields may not persist in preview the same way—another reason duplicate theme QA beats editor-only sign-off.

Checklist: cart drawer ship gate

  • Add from PDP, collection quick-add, and recommended product modules
  • Change quantity, remove line, empty cart messaging
  • Free shipping bar math matches /cart.js total_price
  • Checkout button reaches checkout with correct line items
  • Drawer focus trap and Escape close on keyboard
  • Works with top installed upsell/reviews app enabled
  • Mobile Safari: address bar show/hide does not hide sticky footer
If you change one id in cart-drawer.liquid, grep the entire theme for JavaScript references before you deploy. Drawer bugs are id renames.

Quantity rules, inventory, and edge cases

Line items for products with quantity rules, minimums, or increment steps must respect cart API validation—drawer UI that allows arbitrary quantities will error at checkout. Sold-out variants should disable add-to-cart at product form level, not only hide in drawer after failed add. Multi-box bundles from apps sometimes render as single line items with complex properties—test remove and quantity change on those rows. Inventory tracking "continue selling when out of stock" affects messaging in drawer; align copy with merchant policy.

Internationalization and drawer copy

All customer-facing strings in drawer Liquid should use translation filters—{{ 'sections.cart.checkout' | t }}—with keys added to locale files. Merchants on Translate & Adapt expect drawer strings to appear in translation UI. Hardcoded English "Your cart" blocks localization workflows. Money formatting must use money filters with cart.currency awareness for Markets. Test drawer in secondary language with longer strings—German checkout labels overflow narrow drawers if CSS assumes English width.

Sticky checkout and trust badges

Many merchants want sticky checkout footers inside the drawer on mobile. Implement with CSS position sticky inside the drawer panel, not fixed to viewport—viewport-fixed buttons conflict with iOS Safari chrome and cookie banners. Trust badges and payment icons in drawer footer should be theme settings, not baked-in images, so merchants can update compliance marks. Keep icon sprites optimized; drawer is already heavy.

Migrating from page cart to drawer mid-project

Switching cart UX mid-build is common and painful. Audit every add-to-cart entry point: product form, collection quick add, upsell modules, sticky ATC bars, complementary products. Each must trigger the same cart update event Dawn expects. Page-cart-only apps need replacement or reconfiguration. Schedule QA sprint solely for cart after migration—do not bundle with homepage launch. Document rollback to page cart in theme settings if you expose that toggle.

Allocate at least one full dev day for cart migration QA alone. We have never seen a mid-project cart switch ship cleanly in under that budget—the integration surface is too wide, and stakeholders always test add-to-cart once instead of twelve times.

Building drawer sections from scratch

Greenfield builds: start from Dawn cart-drawer as reference, not from zero. If you markup HTML in Figma, convert shell via converter but import Dawn's JS behaviors or rewrite fetch handlers explicitly—do not assume forms submit traditionally. Register drawer section in theme.liquid with {% sections 'cart-drawer' %} or render tag per your theme pattern.

FAQ

Section or snippet for cart drawer?

Section for AJAX refresh via Sections API; snippets for line item rows inside the section.

Liquid only possible?

Initial render yes; dynamic updates require JavaScript aligned with Shopify cart endpoints.

Should the drawer open on every add-to-cart?

Brand decision. Many stores open drawer for feedback; others show toast only. Document the chosen behavior in theme settings so merchants can toggle if you expose a checkbox in cart settings schema.

Next steps

Study Dawn customization lessons and Dawn section tutorial. Keep cheat sheet handy for cart object fields. Cart drawers are where Liquid, JS, and merchant expectations collide—test obsessively, document ids religiously, and never ship without add-to-cart from every entry point on the store.

Treat the drawer as a product surface, not a footer afterthought. Merchants measure revenue there; broken carts cost real money. Budget QA hours accordingly on every proposal line that mentions "mini cart" or "slide-out cart."

Frequently asked questions

Does the cart drawer use a section or a snippet?

On Dawn and most OS 2.0 themes, the drawer is a section (cart-drawer) rendered from theme.liquid, plus JavaScript that fetches cart state via AJAX and re-renders section HTML using the Sections API.

Why does my cart count not update after add to cart?

Usually the add-to-cart form JavaScript does not dispatch the theme's cart update event, or fetch URLs omit sections parameter to refresh drawer markup. Match the theme's existing product-form.js patterns.

Can I build a cart drawer with Liquid only?

You can render initial state in Liquid, but dynamic add-to-cart without full page reload requires JavaScript fetching /cart.js and section rendering endpoints. Liquid alone cannot listen to AJAX events.

Where should free shipping threshold logic live?

Calculate from cart.total_price in Liquid for initial render, and mirror the same math in JavaScript after AJAX updates—drift between the two causes merchant trust issues.

How do I test cart drawers thoroughly?

Test from product page, collection quick-add, upsell apps, quantity changes, remove line, empty cart, and multi-variant products. Test logged-in with automatic discounts if applicable.

Topic cluster

Shopify Sections

OS 2.0 sections, JSON templates, and section architecture.

Convert HTML to Shopify Liquid

Paste HTML & generate Liquid with schema, blocks, and scoped CSS. No signup required.

Share

Share this guide

Found this guide useful? Share it with other Shopify developers on LinkedIn, X, or Reddit.

Authors

About the authors

Practical Shopify section workflows from the developers who build and maintain this tool.

Dhruv Goyani, Shopify Developer at HTML to Liquid Converter

Dhruv Goyani

3 years web design + 3 years Shopify development experience

Dhruv combines web design and Shopify theme development — converting Figma and static HTML into merchant-editable OS 2.0 sections for client stores. He focuses on schema structure, section presets, and clean handoffs between design and Liquid.

Follow Dhruv on LinkedIn
Nishad Kikani, Lead Shopify Developer at HTML to Liquid Converter

Nishad Kikani

2 years web design + 6 years Shopify development experience

Nishad has six years of Shopify theme engineering — Dawn migrations, JSON templates, performance tuning, and complex block-based sections. He leads client delivery, code review, and the production standards behind every export from HTML to Liquid Converter.

Follow Nishad on LinkedIn

Meet our Shopify developers · Our development experience

Editorial review

Reviewed by the HTML to Liquid Converter team

Dhruv Goyani, Shopify Developer at HTML to Liquid Converter

Dhruv Goyani

3 years web design + 3 years Shopify development experience

LinkedIn profile →
Nishad Kikani, Lead Shopify Developer at HTML to Liquid Converter

Nishad Kikani

2 years web design + 6 years Shopify development experience

LinkedIn profile →

Content is reviewed by the HTML to Liquid Converter Shopify development team before publication. Technical accuracy is validated against current Shopify Online Store 2.0 conventions and active client theme work.

Last updated:

Questions or corrections? Contact us.