{%- comment -%} MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections {%- endcomment -%} {%- comment -%} Sticky add to cart: a bar fixed to the bottom (or top) of the screen that appears once the page's own buy button has scrolled out of view. Product image, title, price with compare-at price, variant select, optional quantity and an add to cart button in a native product form. Uses the product picked in the settings, or the product of the product template. While visible it pads the bottom of the page so it never covers the last content. No snippets needed. {%- endcomment -%} {%- liquid assign s = section.settings assign p = s.product if p == blank assign p = product endif assign render_bar = true if p == blank and request.design_mode == false assign render_bar = false endif assign title = p.title | default: 'Example product' assign current = p.selected_or_first_available_variant assign has_select = false if p != blank and p.has_only_default_variant == false and p.variants.size > 1 assign has_select = true endif assign option_names = p.options_with_values | map: 'name' | join: ' / ' if option_names == blank assign option_names = 'Options' endif assign uid = 'lib-sticky-atc-' | append: section.id assign hidden_on_load = true if s.always_visible assign hidden_on_load = false endif -%} {%- if render_bar -%} {%- endif -%} {% stylesheet %} .lib-sticky-add-to-cart { display: block; } .lib-sticky-add-to-cart__bar { position: fixed; inset-inline: 0; bottom: 0; z-index: 40; padding: 0; color: var(--lib-text); visibility: hidden; transform: translateY(calc(100% + 24px)); } .lib-sticky-add-to-cart[data-position='top'] .lib-sticky-add-to-cart__bar { top: 0; bottom: auto; transform: translateY(calc(-100% - 24px)); } .lib-sticky-add-to-cart__bar.is-visible { visibility: visible; transform: none; } .lib-sticky-add-to-cart[data-position='top'] .lib-sticky-add-to-cart__bar.is-visible { transform: none; } .lib-sticky-add-to-cart[data-animate] .lib-sticky-add-to-cart__bar { transition: transform 0.35s cubic-bezier(0.2, 0.7, 0.2, 1), visibility 0s linear 0.35s; } .lib-sticky-add-to-cart[data-animate] .lib-sticky-add-to-cart__bar.is-visible { transition: transform 0.35s cubic-bezier(0.2, 0.7, 0.2, 1), visibility 0s; } @media (max-width: 749px) { .lib-sticky-add-to-cart[data-show='desktop'] { display: none; } } @media (min-width: 750px) { .lib-sticky-add-to-cart[data-show='mobile'] { display: none; } } /* The visible surface: an edge-to-edge bar, or a floating card inset from the edges. */ .lib-sticky-add-to-cart__inner { display: grid; gap: 0.75rem; align-items: center; margin: 0 auto; padding: var(--lib-py-m) 1rem calc(var(--lib-py-m) + env(safe-area-inset-bottom, 0px)); background: var(--lib-bg); border-top: 1px solid var(--lib-border); } .lib-sticky-add-to-cart[data-position='top'] .lib-sticky-add-to-cart__inner { padding-bottom: var(--lib-py-m); border-top: 0; border-bottom: 1px solid var(--lib-border); } .lib-sticky-add-to-cart[data-shadow='soft'] .lib-sticky-add-to-cart__inner { box-shadow: 0 -6px 24px rgb(0 0 0 / 0.08); } .lib-sticky-add-to-cart[data-shadow='strong'] .lib-sticky-add-to-cart__inner { box-shadow: 0 -10px 40px rgb(0 0 0 / 0.18); } .lib-sticky-add-to-cart[data-position='top'][data-shadow='soft'] .lib-sticky-add-to-cart__inner { box-shadow: 0 6px 24px rgb(0 0 0 / 0.08); } .lib-sticky-add-to-cart[data-position='top'][data-shadow='strong'] .lib-sticky-add-to-cart__inner { box-shadow: 0 10px 40px rgb(0 0 0 / 0.18); } .lib-sticky-add-to-cart[data-style='floating'] .lib-sticky-add-to-cart__bar { padding: 0.5rem 0.5rem calc(0.5rem + env(safe-area-inset-bottom, 0px)); } .lib-sticky-add-to-cart[data-style='floating'][data-position='top'] .lib-sticky-add-to-cart__bar { padding: 0.5rem; } .lib-sticky-add-to-cart[data-style='floating'] .lib-sticky-add-to-cart__inner { max-width: 1200px; padding: var(--lib-py-m) 0.75rem; border: 1px solid var(--lib-border); border-radius: var(--lib-radius); } .lib-sticky-add-to-cart[data-style='floating'][data-width='full'] .lib-sticky-add-to-cart__inner { max-width: none; } @media (min-width: 750px) { .lib-sticky-add-to-cart__inner { grid-template-columns: minmax(0, 1fr) auto; gap: 1.5rem; padding: var(--lib-py) max(1.5rem, calc((100% - 1200px) / 2)) calc(var(--lib-py) + env(safe-area-inset-bottom, 0px)); } .lib-sticky-add-to-cart[data-position='top'] .lib-sticky-add-to-cart__inner { padding-bottom: var(--lib-py); } .lib-sticky-add-to-cart[data-width='full'] .lib-sticky-add-to-cart__inner { padding-inline: 1.5rem; } .lib-sticky-add-to-cart[data-style='floating'] .lib-sticky-add-to-cart__bar { padding: 0.75rem 1.5rem calc(0.75rem + env(safe-area-inset-bottom, 0px)); } .lib-sticky-add-to-cart[data-style='floating'][data-position='top'] .lib-sticky-add-to-cart__bar { padding: 0.75rem 1.5rem; } .lib-sticky-add-to-cart[data-style='floating'] .lib-sticky-add-to-cart__inner { padding: var(--lib-py) 1rem var(--lib-py) var(--lib-py); } } .lib-sticky-add-to-cart__product { display: flex; align-items: center; gap: 0.75rem; min-width: 0; } .lib-sticky-add-to-cart__media { flex: none; width: 3rem; aspect-ratio: 1; border-radius: min(var(--lib-radius), 10px); overflow: hidden; background: color-mix(in srgb, var(--lib-text) 6%, transparent); } .lib-sticky-add-to-cart__img { display: block; width: 100%; height: 100%; object-fit: cover; } .lib-sticky-add-to-cart__img--ph { fill: color-mix(in srgb, var(--lib-text) 25%, transparent); } .lib-sticky-add-to-cart__info { min-width: 0; } .lib-sticky-add-to-cart__title { margin: 0; font-size: 0.95rem; font-weight: 600; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .lib-sticky-add-to-cart__price { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin: 0.15rem 0 0; font-size: 0.9rem; line-height: 1.3; } .lib-sticky-add-to-cart__now.is-sale { color: var(--lib-accent); font-weight: 600; } .lib-sticky-add-to-cart__was { opacity: 0.6; } .lib-sticky-add-to-cart__was[hidden], .lib-sticky-add-to-cart__sr[hidden] { display: none; } .lib-sticky-add-to-cart__form { margin: 0; min-width: 0; } .lib-sticky-add-to-cart__controls { display: flex; align-items: stretch; gap: 0.5rem; min-width: 0; } .lib-sticky-add-to-cart__select-wrap { position: relative; flex: 1 1 8rem; min-width: 0; } .lib-sticky-add-to-cart__select, .lib-sticky-add-to-cart__qty, .lib-sticky-add-to-cart__button { min-height: 2.75rem; border-radius: min(var(--lib-radius), 999px); font: inherit; font-size: 0.9rem; } .lib-sticky-add-to-cart__select { width: 100%; height: 100%; padding: 0 2.25rem 0 0.9rem; color: inherit; background: transparent; border: 1px solid color-mix(in srgb, var(--lib-text) 22%, transparent); appearance: none; -webkit-appearance: none; text-overflow: ellipsis; cursor: pointer; } .lib-sticky-add-to-cart__select option { color: #111; background: #fff; } .lib-sticky-add-to-cart__chevron { position: absolute; inset-inline-end: 0.85rem; top: 50%; translate: 0 -50%; pointer-events: none; } .lib-sticky-add-to-cart__qty { display: flex; flex: none; align-items: center; border: 1px solid color-mix(in srgb, var(--lib-text) 22%, transparent); } .lib-sticky-add-to-cart__step { display: grid; place-items: center; width: 2.25rem; height: 100%; padding: 0; color: inherit; background: none; border: 0; border-radius: inherit; cursor: pointer; } .lib-sticky-add-to-cart__qty-input { width: 2rem; padding: 0; color: inherit; background: none; border: 0; font: inherit; text-align: center; -moz-appearance: textfield; } .lib-sticky-add-to-cart__qty-input::-webkit-outer-spin-button, .lib-sticky-add-to-cart__qty-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .lib-sticky-add-to-cart__button { flex: 1 1 auto; padding: 0 1.5rem; color: var(--lib-accent-text); background: var(--lib-accent); border: 1px solid var(--lib-accent); font-weight: 600; white-space: nowrap; cursor: pointer; transition: filter 0.2s; } .lib-sticky-add-to-cart__button:hover:not(:disabled) { filter: brightness(1.12); } .lib-sticky-add-to-cart__button:disabled { opacity: 0.5; cursor: not-allowed; } .lib-sticky-add-to-cart__select:focus-visible, .lib-sticky-add-to-cart__step:focus-visible, .lib-sticky-add-to-cart__qty-input:focus-visible, .lib-sticky-add-to-cart__button:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 2px; } @media (min-width: 750px) { .lib-sticky-add-to-cart__title { font-size: 1rem; } .lib-sticky-add-to-cart__select-wrap { flex: 0 1 15rem; } .lib-sticky-add-to-cart__button { flex: none; min-width: 11rem; } } .lib-sticky-add-to-cart__sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } @media (prefers-reduced-motion: reduce) { .lib-sticky-add-to-cart[data-animate] .lib-sticky-add-to-cart__bar, .lib-sticky-add-to-cart[data-animate] .lib-sticky-add-to-cart__bar.is-visible, .lib-sticky-add-to-cart__button { transition: none; } } {% endstylesheet %} {% javascript %} if (!customElements.get('lib-sticky-add-to-cart-bar')) { customElements.define( 'lib-sticky-add-to-cart-bar', class extends HTMLElement { connectedCallback() { this.bar = this.querySelector('.lib-sticky-add-to-cart__bar'); if (!this.bar) return; this.bottom = this.dataset.position !== 'top'; this.forced = false; this.shown = this.bar.classList.contains('is-visible'); this.cleanup = []; this.setupForm(); this.setupSpace(); if (this.hasAttribute('data-always')) { this.setShown(true); } else { this.setupTrigger(); } // Theme editor: show the bar while its section is selected. const onSelect = (e) => { if (!this.closest('.shopify-section') || e.target !== this.closest('.shopify-section')) return; this.forced = e.type === 'shopify:section:select'; this.update(); }; document.addEventListener('shopify:section:select', onSelect); document.addEventListener('shopify:section:deselect', onSelect); this.cleanup.push(() => { document.removeEventListener('shopify:section:select', onSelect); document.removeEventListener('shopify:section:deselect', onSelect); }); } disconnectedCallback() { (this.cleanup || []).forEach((fn) => fn()); this.cleanup = []; this.setSpace(0); } /* Show when the page's own buy form is above the viewport; otherwise after a scroll distance. */ setupTrigger() { this.wanted = false; let target = null; try { const selector = this.dataset.selector || ''; if (selector) target = [...document.querySelectorAll(selector)].find((el) => !this.contains(el)) || null; } catch (err) { target = null; } this.target = target; if (target && 'IntersectionObserver' in window) { const io = new IntersectionObserver((entries) => { const entry = entries[entries.length - 1]; this.wanted = !entry.isIntersecting && entry.boundingClientRect.bottom < 0; this.update(); }); io.observe(target); this.cleanup.push(() => io.disconnect()); return; } const offset = parseInt(this.dataset.offset, 10) || 0; let ticking = false; const check = () => { ticking = false; this.wanted = window.scrollY > offset; this.update(); }; const onScroll = () => { if (!ticking) { ticking = true; requestAnimationFrame(check); } }; window.addEventListener('scroll', onScroll, { passive: true }); this.cleanup.push(() => window.removeEventListener('scroll', onScroll)); check(); } update() { if (this.hasAttribute('data-always')) return; this.setShown(Boolean(this.wanted || this.forced)); } setShown(show) { this.shown = show; this.bar.classList.toggle('is-visible', show); this.bar.toggleAttribute('inert', !show); if (show) this.bar.removeAttribute('aria-hidden'); else this.bar.setAttribute('aria-hidden', 'true'); this.setSpace(show ? this.bar.offsetHeight : 0); } /* Pad the page by the bar's height while it is visible, so it never covers the last content. */ setupSpace() { if (!this.bottom) return; const body = document.body; if (!body) return; const before = body.style.paddingBottom; const base = getComputedStyle(body).paddingBottom || '0px'; body.style.paddingBottom = `calc(${base} + var(--lib-sticky-atc-space, 0px))`; this.cleanup.push(() => { body.style.paddingBottom = before; body.style.removeProperty('--lib-sticky-atc-space'); }); if ('ResizeObserver' in window) { const ro = new ResizeObserver(() => this.shown && this.setSpace(this.bar.offsetHeight)); ro.observe(this.bar); this.cleanup.push(() => ro.disconnect()); } } setSpace(px) { if (!this.bottom || !document.body) return; document.body.style.setProperty('--lib-sticky-atc-space', `${px}px`); } setupForm() { const select = this.querySelector('[data-variant-select]'); const button = this.querySelector('[data-add]'); const qty = this.querySelector('.lib-sticky-add-to-cart__qty-input'); if (qty) { this.querySelectorAll('[data-step]').forEach((btn) => { btn.addEventListener('click', () => { const next = (parseInt(qty.value, 10) || 1) + parseInt(btn.dataset.step, 10); qty.value = String(Math.max(1, next)); qty.dispatchEvent(new Event('change', { bubbles: true })); }); }); } if (!select || !button) return; const price = this.querySelector('[data-price]'); const compare = this.querySelector('[data-compare]'); const saleLabel = this.querySelector('[data-sale-label]'); const wasLabel = this.querySelector('[data-was-label]'); const render = () => { const option = select.options[select.selectedIndex]; if (!option) return; const available = !option.disabled; button.disabled = !available; button.textContent = available ? button.dataset.label : button.dataset.soldOut; if (price) price.textContent = option.dataset.price || price.textContent; const was = compare ? option.dataset.compare || '' : ''; if (compare) { compare.textContent = was; compare.hidden = !was; } if (price) price.classList.toggle('is-sale', Boolean(was)); if (saleLabel) saleLabel.hidden = !was; if (wasLabel) wasLabel.hidden = !was; }; select.addEventListener('change', render); // Follow the variant chosen in the page's own buy form. const onMainChange = (e) => { const field = e.target; if (!field || field.name !== 'id' || this.contains(field)) return; const match = [...select.options].find((o) => o.value === String(field.value)); if (match && !match.disabled && select.value !== match.value) { select.value = match.value; render(); } }; document.addEventListener('change', onMainChange); this.cleanup.push(() => document.removeEventListener('change', onMainChange)); } } ); } {% endjavascript %} {% schema %} { "name": "Lib · Sticky add to cart", "tag": "section", "class": "lib-section", "settings": [ { "type": "header", "content": "Content" }, { "type": "product", "id": "product", "label": "Product", "info": "Optional on product pages, where the page's own product is used. Pick one to use the bar on any other page." }, { "type": "checkbox", "id": "show_image", "label": "Show product image", "default": true }, { "type": "checkbox", "id": "show_compare_price", "label": "Show compare-at price", "default": true }, { "type": "checkbox", "id": "show_quantity", "label": "Show quantity selector", "default": true }, { "type": "text", "id": "button_label", "label": "Button label", "default": "Add to cart" }, { "type": "text", "id": "sold_out_label", "label": "Sold out label", "info": "Shown on the button and next to sold-out variants.", "default": "Sold out" }, { "type": "header", "content": "When it appears" }, { "type": "select", "id": "position", "label": "Position", "options": [ { "value": "bottom", "label": "Bottom of the screen" }, { "value": "top", "label": "Top of the screen" } ], "default": "bottom" }, { "type": "checkbox", "id": "always_visible", "label": "Always visible", "info": "Shows the bar from the start instead of waiting for the buy button to scroll away.", "default": false }, { "type": "text", "id": "trigger_selector", "label": "Main buy form selector", "info": "CSS selector of the page's own buy form. The bar appears once it has scrolled above the screen.", "default": "form[action*=\"/cart/add\"]", "visible_if": "{{ section.settings.always_visible == false }}" }, { "type": "range", "id": "scroll_offset", "label": "Fallback scroll distance", "info": "Used when no buy form matches the selector.", "min": 0, "max": 1500, "step": 50, "unit": "px", "default": 600, "visible_if": "{{ section.settings.always_visible == false }}" }, { "type": "header", "content": "Layout" }, { "type": "select", "id": "bar_style", "label": "Style", "options": [ { "value": "bar", "label": "Edge-to-edge bar" }, { "value": "floating", "label": "Floating card" } ], "default": "bar" }, { "type": "select", "id": "content_width", "label": "Content width", "options": [ { "value": "page", "label": "Page width" }, { "value": "full", "label": "Full width" } ], "default": "page" }, { "type": "range", "id": "radius", "label": "Corner radius", "min": 0, "max": 28, "step": 2, "unit": "px", "default": 8 }, { "type": "header", "content": "Colors" }, { "type": "color", "id": "color_bg", "label": "Background", "default": "#FFFFFF" }, { "type": "color", "id": "color_text", "label": "Text", "default": "#1C1917" }, { "type": "color", "id": "color_accent", "label": "Button and sale price", "default": "#1C1917" }, { "type": "color", "id": "color_accent_text", "label": "Button text", "default": "#FFFFFF" }, { "type": "color", "id": "color_border", "label": "Border", "default": "#E7E5E4" }, { "type": "select", "id": "shadow", "label": "Shadow", "options": [ { "value": "none", "label": "None" }, { "value": "soft", "label": "Soft" }, { "value": "strong", "label": "Strong" } ], "default": "soft" }, { "type": "header", "content": "Spacing and visibility" }, { "type": "range", "id": "padding_y", "label": "Top and bottom padding on desktop", "min": 4, "max": 32, "step": 2, "unit": "px", "default": 14 }, { "type": "range", "id": "padding_y_mobile", "label": "Top and bottom padding on mobile", "min": 4, "max": 32, "step": 2, "unit": "px", "default": 10 }, { "type": "select", "id": "show_on", "label": "Show on", "options": [ { "value": "all", "label": "All devices" }, { "value": "desktop", "label": "Desktop only" }, { "value": "mobile", "label": "Mobile only" } ], "default": "all" }, { "type": "header", "content": "Motion" }, { "type": "checkbox", "id": "animate", "label": "Slide in and out", "default": true } ], "presets": [ { "name": "Lib · Sticky add to cart" } ] } {% endschema %}