A sticky ATC bar is one of the highest-impact product page changes you can make.
Create another snippet named sticky-add-to-cart. This is the main file — it contains all the CSS, the Liquid inventory check, the HTML bar, and the JavaScript for visibility and variant sync.
<style>
.sticky-add-to-cart {
background: #fff;
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
padding: 10px 0;
border-top: 0.5px solid rgba(0,0,0,0.1);
box-shadow: 0 -4px 24px rgba(0,0,0,0.07);
border-radius: 12px 12px 0 0;
transform: translateY(110%);
opacity: 0;
pointer-events: none;
transition: transform 0.38s cubic-bezier(0.32, 0.72, 0, 1),
opacity 0.25s ease;
}
.sticky-add-to-cart.is-visible {
transform: translateY(0);
opacity: 1;
pointer-events: auto;
}
.sticky-add-to-cart-content {
display: flex;
align-items: center;
gap: 16px;
}
.sticky-add-to-cart__img {
width: 48px;
height: 48px;
border-radius: 8px;
border: 0.5px solid rgba(0,0,0,0.08);
object-fit: cover;
flex-shrink: 0;
}
.sticky-add-to-cart__info {
flex: 1;
min-width: 0;
}
.sticky-add-to-cart .product__title {
margin-bottom: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sticky-add-to-cart .product__title h3 {
font-size: 14px !important;
margin: 0 0 2px !important;
}
.sticky-add-to-cart .price {
font-size: 14px !important;
}
.sticky-add-to-cart__trust-wrap {
position: relative;
flex-shrink: 0;
width: 182px;
height: 26px;
overflow: hidden;
}
.sticky-add-to-cart__trust-wrap .sticky-add-to-cart__trust {
position: absolute;
inset: 0;
opacity: 0;
animation: atc-trust-cycle 20s ease infinite;
}
.sticky-add-to-cart__trust-wrap .sticky-add-to-cart__trust:nth-child(1) { animation-delay: 0s; }
.sticky-add-to-cart__trust-wrap .sticky-add-to-cart__trust:nth-child(2) { animation-delay: 4s; }
.sticky-add-to-cart__trust-wrap .sticky-add-to-cart__trust:nth-child(3) { animation-delay: 8s; }
.sticky-add-to-cart__trust-wrap .sticky-add-to-cart__trust:nth-child(4) { animation-delay: 12s; }
.sticky-add-to-cart__trust-wrap .sticky-add-to-cart__trust:nth-child(5) { animation-delay: 16s; }
@keyframes atc-trust-cycle {
0% { opacity: 0; }
2% { opacity: 1; }
18% { opacity: 1; }
20% { opacity: 0; }
100% { opacity: 0; }
}
.sticky-add-to-cart__trust {
font-size: 11px;
font-weight: 500;
color: #1a7a45;
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
background: #f0faf4;
border: 1px solid #c3e6d0;
border-radius: 20px;
padding: 4px 10px 4px 7px;
}
.sticky-add-to-cart__trust svg { width: 14px; height: 14px; flex-shrink: 0; }
.sticky-add-to-cart__trust.is-urgent {
color: #b91c1c;
background: #fff5f5;
border-color: #fecaca;
}
.sticky-atc-pulse {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: #ef4444;
flex-shrink: 0;
animation: atcPulse 1.4s ease infinite;
}
@keyframes atcPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.7); }
}
.sticky-add-to-cart__variant-picker {
display: flex;
align-items: center;
gap: 10px;
margin-left: auto;
flex-shrink: 0;
}
.sticky-add-to-cart__variant-picker .product-form__input { margin: 0 !important; }
.sticky-add-to-cart__variant-picker .form__label { display: none !important; }
.sticky-add-to-cart__variant-picker .product-form__submit { margin-bottom: 0 !important; }
@media (max-width: 749px) {
.sticky-add-to-cart { padding: 10px 0 16px; }
.sticky-add-to-cart__trust-wrap { display: none; }
.sticky-add-to-cart-content {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.sticky-atc-mobile-top {
display: flex;
align-items: center;
gap: 10px;
}
.sticky-add-to-cart__img {
width: 44px;
height: 44px;
flex-shrink: 0;
}
.sticky-add-to-cart__info {
flex: 1;
min-width: 0;
}
.sticky-atc-mobile-bottom {
display: flex;
align-items: center;
gap: 8px;
}
.sticky-add-to-cart__variant-picker {
margin-left: 0;
flex: 1;
gap: 8px;
}
.sticky-add-to-cart__variant-picker select {
width: 100%;
}
.sticky-add-to-cart__variant-picker .product-form__submit {
flex: 1;
white-space: nowrap;
}
}
@media (min-width: 750px) {
.sticky-atc-mobile-top { display: contents; }
.sticky-atc-mobile-bottom { display: contents; }
}
</style>
Next, the Liquid inventory check. This runs on the server and finds the first available variant's stock quantity:
{%- assign sticky_inventory = nil -%} {%- for variant in product.variants -%} {%- if variant.available -%} {%- assign sticky_inventory = variant.inventory_quantity -%} {%- break -%} {%- endif -%} {%- endfor -%}
Then the HTML. The trust badges are stacked absolutely and animated by CSS — no JavaScript involved:
<div class="sticky-add-to-cart" id="stickyATC" aria-hidden="true">
<div class="page-width">
<div class="sticky-add-to-cart-content">
<div class="sticky-atc-mobile-top">
<img
class="sticky-add-to-cart__img"
src="{{ product.featured_media | image_url: width: 150 }}"
alt="{{ product.featured_media.alt | escape }}"
width="48"
height="{{ 48 | divided_by: product.featured_media.preview_image.aspect_ratio }}"
loading="eager"
>
<div class="sticky-add-to-cart__info">
<div class="product__title" {{ block.shopify_attributes }}>
<h3 class="h3">{{ product.title | escape }}</h3>
</div>
{%- render 'price', product: product, use_variant: true,
show_badges: true, price_class: 'price--large' -%}
</div>
</div>
{%- if product.available -%}
<div class="sticky-add-to-cart__trust-wrap" aria-hidden="true">
{%- if sticky_inventory != nil and sticky_inventory > 0 and sticky_inventory <= 5 -%}
<div class="sticky-add-to-cart__trust is-urgent">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
<span><span class="sticky-atc-pulse"></span> Only {{ sticky_inventory }} left</span>
</div>
{%- endif -%}
<div class="sticky-add-to-cart__trust">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<span>Order by 3 pm — ships today</span>
</div>
<div class="sticky-add-to-cart__trust">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="1" y="3" width="15" height="13" rx="1"/><path d="M16 8h4l3 5v3h-7V8z"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>
<span>Free shipping over $50</span>
</div>
<div class="sticky-add-to-cart__trust">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><polyline points="9 12 11 14 15 10"/></svg>
<span>Secure checkout</span>
</div>
<div class="sticky-add-to-cart__trust">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.95"/></svg>
<span>30-day free returns</span>
</div>
</div>
{%- endif -%}
<div class="sticky-atc-mobile-bottom">
<div class="sticky-add-to-cart__variant-picker">
{%- render 'sticky-add-to-cart-variant-picker',
product: product,
product_form_id: product_form_id -%}
{%- render 'buy-buttons',
block: block,
product: product,
product_form_id: product_form_id,
section_id: section.id,
show_pickup_availability: false -%}
</div>
</div>
</div>
</div>
</div>
Finally, the JavaScript. Just two responsibilities — showing/hiding the bar and syncing the sticky select to the main form:
<script>
(function () {
const stickyBar = document.getElementById('stickyATC');
const mainButtons = document.querySelector('.product-form__buttons');
if (!stickyBar || !mainButtons) return;
// 1. VISIBILITY
new IntersectionObserver(function (entries) {
const visible = !entries[0].isIntersecting;
stickyBar.classList.toggle('is-visible', visible);
stickyBar.setAttribute('aria-hidden', String(!visible));
}, { threshold: 0, rootMargin: '0px' }).observe(mainButtons);
// 2. VARIANT SYNC
stickyBar.querySelectorAll('select').forEach(function (sel) {
console.log('Select:');
console.log(sel);
sel.addEventListener('change', function (e) {
console.log('Target:');
console.log(e.target.value);
const target = document.querySelector(`product-info [value="${e.target.value}"]`);
console.log('target:');
console.log(target);
if (target) target.click();
});
});
})();
</script>
IntersectionObserver watches the main .product-form__buttons element. When it leaves the viewport the bar slides up; when it re-enters the bar slides back down. The CSS transition handles the animation — no JS animation needed.
Go to Online Store → Themes → Edit code → Snippets → Add a new snippet.
Name it sticky-add-to-cart-variant-picker and paste the code below.
This renders a dropdown for every option except Color (which is typically shown as swatches on the main form).
{%- comment -%} Renders a variant picker for the sticky ATC bar. Skips Color — handled by swatches on the main form. Accepts: - product: {Object} product object. - product_form_id: {String} product form id. Usage: {% render 'sticky-add-to-cart-variant-picker', product: product, product_form_id: product_form_id %} {%- endcomment -%} {%- unless product.has_only_default_variant -%} <variant-selects id="variant-selects-sticky" data-section="sticky" > {%- for option in product.options_with_values -%} {%- unless option.name == 'Color' -%} {%- assign picker_type = 'dropdown' -%} <div class="product-form__input product-form__input--dropdown"> <label class="form__label" for="Option-sticky-{{ forloop.index0 }}"> {{ option.name }} </label> <div class="select"> <select id="Option-sticky-{{ forloop.index0 }}" class="select__select" name="options[{{ option.name | escape }}]" form="{{ product_form_id }}" > {%- render 'product-variant-options', product: product, option: option, picker_type: picker_type -%} </select> <span class="svg-wrapper"> {{- 'icon-caret.svg' | inline_asset_content -}} </span> </div> </div> {%- endunless -%} {%- endfor -%} <script type="application/json" data-selected-variant> {{ product.selected_or_first_available_variant | json }} </script> </variant-selects> {%- endunless -%}
id="variant-selects-sticky" avoids a duplicate ID in the DOM since the main form already has variant-selects-{{ section.id }}. The form attribute on the select ties it to the same product_form_id as the main form so the correct variant is submitted.
Open snippets/buy-buttons.liquid. Find the submit <button> and add the SVG inline inside the <span> — before the button label text. This adds the bag icon to every add-to-cart button site-wide (PDP, quick-add, and the sticky bar) from one place, with no JavaScript needed.
<button id="ProductSubmitButton-{{ section_id }}" type="submit" name="add" class="product-form__submit button button--full-width {%- if show_dynamic_checkout -%}button--secondary{%- else -%}button--primary{%- endif -%}" {% if product.selected_or_first_available_variant.available == false or quantity_rule_soldout or product.selected_or_first_available_variant == null %}disabled{% endif %} > <span> <svg style="width:16px;height:16px;vertical-align:middle;margin-right:6px;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/> <line x1="3" y1="6" x2="21" y2="6"/> <path d="M16 10a4 4 0 0 1-8 0"/> </svg> {%- if product.selected_or_first_available_variant == null -%} {{ 'products.product.unavailable' | t }} {%- elsif product.selected_or_first_available_variant.available == false or quantity_rule_soldout -%} {{ 'products.product.sold_out' | t }} {%- else -%} {{ 'products.product.add_to_cart' | t }} {%- endif -%} </span> {%- render 'loading-spinner' -%} </button>
stroke="currentColor" so it automatically inherits the button's text colour — white on primary, dark on secondary. No extra CSS needed.
Open layout/theme.liquid. Find the closing </main> tag and add the render call just before it, wrapped in a product page template check so it only loads on product pages:
<main id="MainContent" class="content-for-layout focus-none" role="main" tabindex="-1"> {{ content_for_layout }} {% if template.name == 'product' %} {% render 'sticky-add-to-cart' %} {% endif %} </main>
template.name == 'product' check is important — without it the snippet would try to render on every page but product, block, and product_form_id would be undefined, causing Liquid errors.
When a customer changes a variant on the main form, Dawn fetches updated HTML and re-renders the page. We hook into that to keep the sticky bar's hidden id input in sync. Open assets/product-info.js and add the method below inside the ProductInfo class, then call it from handleUpdateProductInfo:
handleUpdateStickyAddToCart(variant) {
if (!variant) return;
// 1. Keep the hidden variant ID input in sync
const stickyVariantIdInput = document.querySelector('.sticky-add-to-cart [name="id"]');
if (stickyVariantIdInput) stickyVariantIdInput.value = variant.id;
// 2. Sync every select in the sticky bar to its matching variant option
document.querySelectorAll('.sticky-add-to-cart select').forEach((select) => {
Array.from(select.options).forEach((opt) => {
opt.selected = variant.options.includes(opt.value);
});
});
}
Then call it from handleUpdateProductInfo, right after getSelectedVariant:
handleUpdateProductInfo(productUrl) {
return (html) => {
const variant = this.getSelectedVariant(html);
this.handleUpdateStickyAddToCart(variant); // ← add this line
this.pickupAvailability?.update(variant);
this.updateOptionValues(html);
// ... rest of the method unchanged
};
}
Open any product page, scroll past the add-to-cart button, and the sticky bar should slide up from the bottom. Change a variant — the sticky select updates. Change the sticky select — the main form updates. Low stock products show the pulsing red badge automatically.
I implement this kind of custom Shopify development daily — no apps, clean code, done fast. Tell me what you need and I'll get it done right.