component-cart-discount Snippet
snippets/component-cart-discount.liquid renders a discount code form with applied discount pills. It uses the <cart-discount-form> custom element to handle discount application, removal, and dynamic UI updates without page reloads.
What It Does
- Renders a discount code input form
- Displays applied discount codes as removable pills
- Shows error messages for invalid discount codes
- Updates dynamically via Liquid Ajax Cart integration
- Provides loading states during API calls
Parameters
This snippet does not accept any parameters. It uses the global cart object available in Liquid context.
Dependencies & Assets
| Type | Files / Components |
|---|---|
| JavaScript | component-cart-discount.js (defines <cart-discount-form> custom element) |
| CSS | cart.css (contains discount form styles) |
| Icons | icon-error.svg, icon-close.svg (inline via inline_asset_content) |
| Data | Requires cart object with cart_level_discount_applications |
| External | Liquid Ajax Cart library for cart updates |
Markup Structure
<cart-discount-form>
<div class="cart-discount">
<form id="cart-discount-form" class="cart-discount__form">
<input
id="discount-code-input"
name="discount_code"
type="text"
class="cart-discount__input"
placeholder="{{ 'cart.discount_code' | t }}"
required
>
<button type="submit" class="cart-discount__button">
{{ 'cart.apply' | t }}
</button>
</form>
<ul class="cart-discount__codes">
{%- for discount_application in cart.cart_level_discount_applications -%}
{%- if discount_application.type == 'discount_code' -%}
<li class="cart-discount__pill" data-discount-code="{{ discount_application.title | escape }}">
<p class="cart-discount__pill-code">{{ discount_application.title | escape }}</p>
<button type="button" class="cart-discount__pill-remove" aria-label="{{ 'cart.remove_discount' | t }} {{ discount_application.title | escape }}">
{{- 'icon-close.svg' | inline_asset_content -}}
</button>
</li>
{%- endif -%}
{%- endfor -%}
</ul>
<div class="cart-discount__error" style="display: none;" role="alert">
<span class="svg-wrapper">
{{- 'icon-error.svg' | inline_asset_content -}}
</span>
<p class="cart-discount__error-text"></p>
</div>
</div>
</cart-discount-form>Component Structure
Custom Element Wrapper
<cart-discount-form>The <cart-discount-form> custom element wraps the entire component and handles all JavaScript functionality.
Form Element
<form id="cart-discount-form" class="cart-discount__form">- ID requirement: Must be
cart-discount-formfor JavaScript targeting - Class:
cart-discount__formfor styling - Contains input and submit button
Input Field
<input
id="discount-code-input"
name="discount_code"
type="text"
class="cart-discount__input"
placeholder="{{ 'cart.discount_code' | t }}"
required
>- ID requirement: Must be
discount-code-inputfor JavaScript targeting - Required attribute: HTML5 validation
- Placeholder: Uses translation key
cart.discount_code
Submit Button
<button type="submit" class="cart-discount__button">
{{ 'cart.apply' | t }}
</button>- Button text changes to "Applying..." during loading
- Disabled during API calls
- Uses translation key
cart.apply
Discount Codes List
<ul class="cart-discount__codes">
{%- for discount_application in cart.cart_level_discount_applications -%}
{%- if discount_application.type == 'discount_code' -%}
<li class="cart-discount__pill" data-discount-code="{{ discount_application.title | escape }}">
<!-- Pill content -->
</li>
{%- endif -%}
{%- endfor -%}
</ul>- Empty state: List is hidden when empty (CSS
:has()selector) - Discount filtering: Only shows discount codes (not automatic discounts)
- Data attribute: Each pill has
data-discount-codefor JavaScript targeting
Discount Pill
<li class="cart-discount__pill" data-discount-code="{{ discount_application.title | escape }}">
<p class="cart-discount__pill-code">
{{ discount_application.title | escape }}
</p>
<button
type="button"
class="cart-discount__pill-remove"
aria-label="{{ 'cart.remove_discount' | t }} {{ discount_application.title | escape }}"
>
{{- 'icon-close.svg' | inline_asset_content -}}
</button>
</li>- Code display: Shows discount code in uppercase (via CSS)
- Remove button: Click removes the discount
- Loading state: Pill gets
cart-discount__pill--removingclass during removal - Accessibility: Proper ARIA labels
Error Container
<div class="cart-discount__error" style="display: none;" role="alert">
<span class="svg-wrapper">
{{- 'icon-error.svg' | inline_asset_content -}}
</span>
<p class="cart-discount__error-text"></p>
</div>- Hidden by default: Uses inline
display: none - ARIA role:
role="alert"for screen reader announcements - Dynamic content: Error text is set by JavaScript
- Icon: Error icon for visual indication
Behavior
Discount Application
- User enters discount code and submits form
- JavaScript validates input (non-empty, not duplicate)
- Button shows "Applying..." and is disabled
- Input is disabled during API call
- Discount is applied via
/cart/update.js - Cart state is fetched from
/cart.js - Pills are updated dynamically
- Liquid Ajax Cart updates cart sections
- Input is cleared and loading state is reset
Discount Removal
- User clicks remove button on a pill
- Pill gets loading class (opacity animation)
- Remove button is disabled
- Discount is removed via API
- Cart state is fetched
- Pills are updated (removed pill disappears)
- Liquid Ajax Cart updates cart sections
Error Handling
- Invalid discount codes show error message
- Network errors are caught and displayed
- Error messages are automatically hidden on new attempts
- Error state is restored on removal failures
Loading States
Button Loading:
- Text: "Applying..."
- Disabled state
- Reduced opacity (60%)
Pill Removal:
- Class:
cart-discount__pill--removing - Opacity: 50%
- Pointer events disabled
Usage Example
{% render 'component-cart-discount' %}Typically used in:
- Cart page (
sections/cart.liquid) - Cart drawer (
snippets/component-cart-drawer.liquid)
Implementation Notes
Discount Code Extraction
Discount codes are extracted from the cart object:
{%- for discount_application in cart.cart_level_discount_applications -%}
{%- if discount_application.type == 'discount_code' -%}
{{ discount_application.title }}
{%- endif -%}
{%- endfor -%}This filters for discount code type and extracts the title (code).
Multiple Discount Codes
The component supports multiple discount codes:
- Codes are combined with commas:
"CODE1,CODE2" - All codes are sent together in a single API call
- Each code is displayed as a separate pill
Duplicate Prevention
JavaScript prevents duplicate codes:
- Checks existing codes before submission
- Clears input if duplicate is detected
- No API call is made for duplicates
Liquid Ajax Cart Integration
The component triggers Liquid Ajax Cart updates:
- Updates all
data-ajax-cart-sectionelements - Keeps cart totals and other cart UI in sync
- No page reload required
Dynamic Pill Updates
Pills are updated dynamically:
- New pills are created when codes are applied
- Pills are removed when codes are removed
- Existing pills are preserved if still valid
- Updates happen immediately after API calls
Translation Keys
All user-facing text uses translation filters:
cart.discount_code- Input placeholdercart.apply- Submit button textcart.remove_discount- Remove button aria-label
Icon Dependencies
Requires these icons in assets/:
icon-error.svg- Error message iconicon-close.svg- Remove button icon
CSS Class Dependencies
Relies on CSS classes defined in cart.css:
.cart-discount- Container.cart-discount__form- Form wrapper.cart-discount__input- Input field.cart-discount__button- Submit button.cart-discount__button:disabled- Loading state.cart-discount__codes- Codes list.cart-discount__pill- Individual pill.cart-discount__pill--removing- Removal loading state.cart-discount__pill-code- Code text.cart-discount__pill-remove- Remove button.cart-discount__error- Error container.cart-discount__error-text- Error message.svg-wrapper- Icon wrapper
Data Attributes
JavaScript uses these data attributes:
data-discount-code- Discount code value on pills
Form IDs
JavaScript requires these specific IDs:
cart-discount-form- Form elementdiscount-code-input- Input field
No Page Reloads
Unlike older implementations, this component:
- Does not reload the page
- Updates UI dynamically
- Uses Liquid Ajax Cart for cart synchronization
- Provides smooth user experience
Accessibility
- Proper ARIA labels on interactive elements
- Error messages use
role="alert" - Button states properly disabled
- Keyboard navigation support
- Screen reader friendly
Script Loading
The JavaScript must be loaded as a module:
<script src="{{ 'component-cart-discount.js' | asset_url }}" type="module"></script>Ensure this is included on pages where the discount form appears.
Cart Object Dependency
The snippet requires the cart object to be available in Liquid context. This is automatically available on:
- Cart page (
/cart) - Cart drawer (via Liquid Ajax Cart)
- Any page with cart context
Error Scenarios
Invalid Discount Code
- Error message: "That discount code is not valid."
- Input remains enabled
- User can try again
Network Error
- Error message: "Something went wrong while applying the code."
- Loading state is reset
- User can retry
Removal Failure
- Error message: "Something went wrong while removing the discount."
- Pill state is restored
- Cart state is refreshed