(()=>{
document.querySelectorAll('[data-free-shipping]').forEach((component)=>{const format =newIntl.NumberFormat(document.documentElement.lang ||'en',{style:'currency',currency:'USD'});const threshold =Number(component.dataset.threshold);const bar = component.querySelector('[data-free-shipping-bar]');const text = component.querySelector('[data-free-shipping-text]');const form = component.querySelector('[data-free-shipping-add]');constupdate=()=>{const subtotal =Number(component.dataset.subtotal);const missing = threshold - subtotal;const unlocked = missing <=0;
bar.max = threshold;
bar.value = Math.min(subtotal, threshold);
bar.textContent =`${format.format(subtotal)} of ${format.format(threshold)}`;
component.classList.toggle('free-shipping--unlocked', unlocked);
text.innerHTML = unlocked
?'You’ve unlocked <strong>free shipping</strong>!':`You’re <strong>${format.format(missing)}</strong> away from free shipping.`;};// Call this when the cart changes, for example from the `cart:add` event of the product forms.
component.addEventListener('cart:update',(event)=>{
component.dataset.subtotal = event.detail.subtotal;update();});
form.addEventListener('submit',(event)=>{
event.preventDefault();const button = form.querySelector('button');
component.dispatchEvent(newCustomEvent('cart:update',{detail:{subtotal:Number(component.dataset.subtotal)+Number(button.dataset.price)}}));
button.disabled =true;
button.firstChild.textContent ='Added';});update();});})();
Free Shipping Progress
On this page
A progress bar toward the free shipping threshold for the cart, with the missing amount and a product suggestion that fills the gap.
Anatomy
.free-shipping: the wrapper with data-threshold and data-subtotal; it gets --unlocked once the threshold is reached.
.free-shipping__message: the icon and the text with the missing amount.
.free-shipping__bar: a native progress element.
.free-shipping__suggestion: an optional add-on product with its own "Add" form.
Accessibility and Usability
The message is a polite live region: when the cart changes, the new missing amount (or "You’ve unlocked free shipping!") is announced.
The progress element has a label ("Progress toward free shipping"), and its fallback text is the amount.
The "Add" button has hidden context ("Add Steel Water Bottle"); after adding, it's disabled and says "Added".
Say the exact amount ("$11.00 away"); it's more motivating and clearer than a bar alone.
Technical Details
Update the component with a cart:update event on it, with the new subtotal in detail ({ subtotal: 113 }), for example after your add-to-cart request. The demo dispatches it from the suggestion's form.
Without JavaScript, the message and the bar are rendered by the server, and the "Add" form posts normally.