Quantity Controls and Line-Item Properties
Build quantity selectors with increment/decrement buttons, handle min/max limits, and add custom line-item properties for personalization.
Quantity controls let customers order multiple items, while line-item properties enable personalization and customization. Together, they create flexible purchasing options.
Basic Quantity Input
The simplest quantity selector:
<input type="number" name="quantity" value="1" min="1" class="quantity-input">Enhanced Quantity Selector
Add increment/decrement buttons for better UX:
{# snippets/quantity-selector.liquid #}
{%- assign input_id = id | default: 'quantity' -%}{%- assign input_value = value | default: 1 -%}{%- assign input_min = min | default: 1 -%}{%- assign input_max = max | default: 99 -%}
<quantity-selector class="quantity-selector"> <button type="button" class="quantity-selector__button" data-action="decrease" aria-label="Decrease quantity" {% if input_value <= input_min %}disabled{% endif %} > <span aria-hidden="true">−</span> </button>
<input type="number" id="quantity-{{ input_id }}" name="quantity" class="quantity-selector__input" value="{{ input_value }}" min="{{ input_min }}" max="{{ input_max }}" step="1" aria-label="Quantity" >
<button type="button" class="quantity-selector__button" data-action="increase" aria-label="Increase quantity" {% if input_value >= input_max %}disabled{% endif %} > <span aria-hidden="true">+</span> </button></quantity-selector>Quantity Selector Styles
.quantity-selector { display: inline-flex; align-items: center; border: 1px solid var(--color-border); border-radius: var(--border-radius);}
.quantity-selector__button { display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; padding: 0; font-size: 1.25rem; background: none; border: none; cursor: pointer; color: var(--color-text); transition: background-color 0.2s;}
.quantity-selector__button:hover:not(:disabled) { background: var(--color-background-secondary);}
.quantity-selector__button:disabled { opacity: 0.3; cursor: not-allowed;}
.quantity-selector__input { width: 60px; height: 44px; padding: 0; font-size: 1rem; text-align: center; border: none; border-left: 1px solid var(--color-border); border-right: 1px solid var(--color-border); background: transparent; -moz-appearance: textfield;}
.quantity-selector__input::-webkit-inner-spin-button,.quantity-selector__input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0;}
.quantity-selector__input:focus { outline: none; background: var(--color-background-secondary);}Quantity Selector JavaScript
class QuantitySelector extends HTMLElement { constructor() { super(); this.input = this.querySelector('.quantity-selector__input'); this.decreaseBtn = this.querySelector('[data-action="decrease"]'); this.increaseBtn = this.querySelector('[data-action="increase"]'); }
connectedCallback() { this.decreaseBtn.addEventListener('click', () => this.change(-1)); this.increaseBtn.addEventListener('click', () => this.change(1)); this.input.addEventListener('change', () => this.onInputChange()); }
change(delta) { const currentValue = parseInt(this.input.value) || 1; const newValue = currentValue + delta;
this.setValue(newValue); }
setValue(value) { const min = parseInt(this.input.min) || 1; const max = parseInt(this.input.max) || 99;
// Clamp value to min/max value = Math.max(min, Math.min(max, value));
this.input.value = value;
// Update button states this.decreaseBtn.disabled = value <= min; this.increaseBtn.disabled = value >= max;
// Dispatch change event this.dispatchEvent( new CustomEvent('quantity:changed', { detail: { quantity: value }, bubbles: true, }) ); }
onInputChange() { let value = parseInt(this.input.value);
if (isNaN(value) || value < 1) { value = 1; }
this.setValue(value); }}
customElements.define('quantity-selector', QuantitySelector);Inventory-Based Limits
Set max quantity based on stock:
{%- assign max_quantity = 99 -%}
{%- if current_variant.inventory_management and current_variant.inventory_policy == 'deny' -%} {%- if current_variant.inventory_quantity > 0 -%} {%- assign max_quantity = current_variant.inventory_quantity -%} {%- endif -%}{%- endif -%}
{% render 'quantity-selector', value: 1, min: 1, max: max_quantity%}Bundle Quantity
For products sold in multiples:
{# Product sold in packs of 6 #}{%- assign quantity_step = 6 -%}
<input type="number" name="quantity" value="{{ quantity_step }}" min="{{ quantity_step }}" step="{{ quantity_step }}">Line-Item Properties
Line-item properties add custom data to cart items:
<input type="hidden" name="properties[Gift]" value="true">
<label> <input type="checkbox" name="properties[Gift]" value="Yes"> This is a gift</label>Text Customization
Allow customers to personalize products:
<div class="product-form__customization"> <label for="engraving" class="product-form__label"> Engraving Text <span class="product-form__optional">(optional)</span> </label> <input type="text" id="engraving" name="properties[Engraving]" maxlength="20" placeholder="Enter up to 20 characters" class="product-form__text-input" > <p class="product-form__hint"> Text will appear exactly as entered </p></div>Multiple Property Inputs
<div class="product-form__properties"> {# Text input #} <div class="property-field"> <label for="prop-name">Name</label> <input type="text" id="prop-name" name="properties[Name]" required> </div>
{# Select dropdown #} <div class="property-field"> <label for="prop-font">Font Style</label> <select id="prop-font" name="properties[Font]"> <option value="Script">Script</option> <option value="Block">Block</option> <option value="Modern">Modern</option> </select> </div>
{# Checkbox #} <div class="property-field"> <label> <input type="checkbox" name="properties[Gift Wrap]" value="Yes"> Add gift wrapping (+$5) </label> </div>
{# Date picker #} <div class="property-field"> <label for="prop-date">Delivery Date</label> <input type="date" id="prop-date" name="properties[Delivery Date]"> </div>
{# Textarea #} <div class="property-field"> <label for="prop-message">Gift Message</label> <textarea id="prop-message" name="properties[Gift Message]" rows="3" maxlength="200" ></textarea> </div></div>Hidden Properties
Track internal data without showing to customer:
{# Hidden properties start with underscore #}<input type="hidden" name="properties[_source]" value="product-page"><input type="hidden" name="properties[_added_at]" value="{{ 'now' | date: '%Y-%m-%d' }}">Properties starting with underscore (_) are hidden in the cart and checkout.
File Upload Property
Allow file uploads for custom products:
<div class="product-form__upload"> <label for="custom-image">Upload Your Image</label> <input type="file" id="custom-image" name="properties[Custom Image]" accept="image/*" > <p class="product-form__hint"> Accepted formats: JPG, PNG. Max 5MB. </p></div>Note: File uploads require additional configuration.
Dynamic Properties with JavaScript
Add properties dynamically:
class ProductProperties extends HTMLElement { connectedCallback() { this.form = this.closest('form'); this.setupDynamicProperties(); }
setupDynamicProperties() { // Add property when checkbox changes const giftWrap = this.querySelector('[name="properties[Gift Wrap]"]');
giftWrap?.addEventListener('change', (e) => { if (e.target.checked) { this.showGiftOptions(); } else { this.hideGiftOptions(); } }); }
showGiftOptions() { this.querySelector('.gift-options').hidden = false; }
hideGiftOptions() { this.querySelector('.gift-options').hidden = true; // Clear values this.querySelector('[name="properties[Gift Message]"]').value = ''; }}
customElements.define('product-properties', ProductProperties);Displaying Properties in Cart
Show line-item properties in the cart:
{%- for item in cart.items -%} <div class="cart-item"> <p>{{ item.product.title }}</p>
{# Show custom properties #} {%- if item.properties.size > 0 -%} <ul class="cart-item__properties"> {%- for property in item.properties -%} {%- unless property.first contains '_' -%} <li> <strong>{{ property.first }}:</strong> {{ property.last }} </li> {%- endunless -%} {%- endfor -%} </ul> {%- endif -%} </div>{%- endfor -%}Character Counter
For text inputs with limits:
<div class="property-field"> <label for="engraving"> Engraving <span class="character-count"> <span data-current-count>0</span>/20 </span> </label> <input type="text" id="engraving" name="properties[Engraving]" maxlength="20" data-character-counter ></div>document.querySelectorAll('[data-character-counter]').forEach((input) => { const counter = input.closest('.property-field').querySelector('[data-current-count]');
input.addEventListener('input', () => { counter.textContent = input.value.length; });});Complete Quantity and Properties Example
{# snippets/product-form-extended.liquid #}
<div class="product-form__options"> {# Quantity #} <div class="product-form__group"> <label class="product-form__label">Quantity</label> {%- render 'quantity-selector', id: section.id, value: 1, max: current_variant.inventory_quantity | default: 99 -%} </div>
{# Custom properties based on product #} {%- if product.tags contains 'personalized' -%} <product-properties class="product-form__properties"> <div class="product-form__group"> <label for="custom-text-{{ section.id }}" class="product-form__label"> Personalization Text <span class="product-form__required">*</span> </label> <input type="text" id="custom-text-{{ section.id }}" name="properties[Custom Text]" maxlength="15" required class="product-form__input" placeholder="Enter your text" > <p class="product-form__hint"> 15 characters max. Will appear exactly as typed. </p> </div> </product-properties> {%- endif -%}
{# Gift options #} <div class="product-form__group"> <label class="product-form__checkbox"> <input type="checkbox" name="properties[Gift]" value="Yes" data-gift-toggle > <span>This is a gift</span> </label>
<div class="gift-options" hidden data-gift-options> <div class="product-form__group"> <label for="gift-message-{{ section.id }}" class="product-form__label"> Gift Message </label> <textarea id="gift-message-{{ section.id }}" name="properties[Gift Message]" rows="3" maxlength="200" class="product-form__textarea" placeholder="Add a personal message..." ></textarea> </div> </div> </div></div>
<script> document.querySelector('[data-gift-toggle]')?.addEventListener('change', (e) => { document.querySelector('[data-gift-options]').hidden = !e.target.checked; });</script>Practice Exercise
Build a product form with:
- Quantity selector with +/- buttons
- Max quantity based on inventory
- Text customization field with character limit
- Optional gift message checkbox
- Show gift options when checked
Test by:
- Changing quantities
- Hitting min/max limits
- Adding customization text
- Toggling gift options
- Adding to cart and checking properties
Key Takeaways
- Use increment/decrement buttons for better mobile UX
- Set min/max based on inventory
- Line-item properties use
name="properties[Name]" - Hidden properties start with underscore (
_) - Display properties in cart except hidden ones
- Validate required properties before submission
- Character counters help with limits
- Conditional properties based on checkboxes
What’s Next?
The next lesson covers Dynamic Content: Metafields and Tabs for displaying rich product information.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...