Where to paste this in Shopify
- Online Store → Themes → Customize
- Open the template you want (Home, Product, Collection, Blog, Article, etc.)
- Add section → Custom HTML (or Custom Liquid)
- Paste the entire snippet below
- Save and preview on desktop and mobile
If you want editor controls later, convert it to a Liquid section and expose settings in the schema; the HTML/CSS/JS stays the same.
Copy-Paste Example — Sticky Footer on Scroll
Paste everything from <!DOCTYPE html> to </html>.
<body>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Optional SEO tags for this article page -->
<title>Shopify Sticky Footer on Scroll — Lightweight HTML/CSS/JS (No Apps)</title>
<meta name="title" content="Shopify Sticky Footer on Scroll — Lightweight HTML/CSS/JS (No Apps)">
<meta name="description" content="Add a clean sticky footer bar that appears on scroll for Shopify. Tiny, dependency-free HTML/CSS/JS with show-on-scroll-up, thresholds, dismiss persistence, and reduced-motion support. No apps, Liquid-ready.">
<style>
:root{
/* Brand + motion tokens (tune to your theme) */
--sf-bg: #0f1115;
--sf-fg: #e8ecf1;
--sf-muted:#aeb8c6;
--sf-accent:#00d084;
--sf-duration: 420ms;
--sf-ease: cubic-bezier(.2,.65,.2,1);
--sf-shadow: 0 10px 30px rgba(0,0,0,.32);
--sf-border: #1f2636;
--sf-z: 9999;
}
html,body{ margin:0; background:#0f1115; color:var(--sf-fg); font-family: Arial, sans-serif; }
.demo-content{ min-height:160vh; padding: 24px 16px 120px; max-width: 1100px; margin: 0 auto; }
.demo-content h1{ font-size: clamp(28px, 5.5vw, 54px); margin: 18px 0 6px; }
.demo-content p{ color: var(--sf-muted); max-width: 70ch; }
/* ===== Sticky Footer System ===== */
.sf-wrap{
position: fixed;
left: 0; right: 0; bottom: 0;
z-index: var(--sf-z);
transform: translateY(100%);
transition: transform var(--sf-duration) var(--sf-ease);
will-change: transform;
/* iOS safe area */
padding-bottom: env(safe-area-inset-bottom);
}
.sf-in{ transform: translateY(0); }
.sf-bar{
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
align-items: center;
background: var(--sf-bg);
color: var(--sf-fg);
border-top: 1px solid var(--sf-border);
box-shadow: var(--sf-shadow);
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
}
.sf-lead{
line-height:1.25;
font-size: 15px;
}
.sf-lead strong{ color: var(--sf-fg); }
.sf-lead span{ color: var(--sf-muted); }
.sf-actions{
display:flex; gap: 10px; align-items:center; flex-wrap: wrap;
}
.sf-btn{
appearance: none; border:1px solid var(--sf-border); background: transparent; color: var(--sf-fg);
padding: 10px 14px; border-radius: 10px; font-size: 14px; cursor: pointer;
}
.sf-btn--primary{
background: var(--sf-accent); color:#071410; border:1px solid transparent;
font-weight: 700;
}
.sf-dismiss{
appearance:none; background: transparent; border: none; color: var(--sf-muted);
cursor:pointer; padding: 8px; border-radius: 8px; line-height: 0;
}
.sf-dismiss:focus-visible, .sf-btn:focus-visible{
outline: 2px solid var(--fi-accent, var(--sf-accent)); outline-offset: 2px;
}
/* Reduced motion: no slide animation */
@media (prefers-reduced-motion: reduce){
.sf-wrap{ transition-duration: 1ms !important; }
}
/* Example: avoid overlapping theme drawers if any (optional) */
.sf-avoid-drawer [aria-hidden="false"][data-drawer]{ margin-bottom: 64px; }
</style>
<!-- Demo page content (safe to remove) -->
<main class="demo-content">
<h1>Sticky Footer on Scroll (Demo)</h1>
<p>This bar appears after you scroll a bit, or when you scroll up—tune behavior with data attributes.</p>
<p>Use it for promotions, free shipping thresholds, signups, or a quick “Add to cart”.</p>
</main>
<!-- Sticky Footer: paste once and reuse everywhere -->
<div class="sf-wrap" role="region" aria-label="Site offer" data-sf-after="280" show after n px scrolled>
data-sf-up-only="true" <!-- Only show when user scrolls up (true/false) -->
data-sf-hide-near="footer" <!-- CSS selector to hide when near real footer -->
data-sf-near-margin="320" <!-- Distance in px from selector before hiding -->
data-sf-storage="promo-q4" <!-- Dismiss persistence key in localStorage -->
data-sf-session="false" <!-- true = sessionStorage, false = localStorage -->
>
<div class="sf-bar" aria-live="polite">
<div class="sf-lead">
<strong>Free shipping over $75</strong> — <span>Ends Sunday. Use code <b>SHIPFAST</b>.</span>
</div>
<div class="sf-actions">
<a class="sf-btn sf-btn--primary" href="/collections/all" aria-label="Shop now">Shop Now</a>
<button class="sf-btn" onclick="document.querySelector('html,body'); window.scrollTo({top:0, behavior:'smooth'});" aria-label="Back to top">Top</button>
<button class="sf-dismiss" type="button" aria-label="Dismiss offer" title="Dismiss">
✕
</button>
</div>
</div>
</div>
<script>
(function(){
const wrap = document.querySelector('.sf-wrap');
if(!wrap) return;
const prefersReduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Config from data-attributes
const after = parseInt(wrap.getAttribute('data-sf-after') || '200', 10);
const upOnly = (wrap.getAttribute('data-sf-up-only') || 'false') === 'true';
const nearSel = wrap.getAttribute('data-sf-hide-near') || '';
const nearMargin = parseInt(wrap.getAttribute('data-sf-near-margin') || '260', 10);
const storageKey = wrap.getAttribute('data-sf-storage') || '';
const sessionMode = (wrap.getAttribute('data-sf-session') || 'false') === 'true';
// Storage helpers
const store = sessionMode ? window.sessionStorage : window.localStorage;
const isDismissed = () => storageKey ? store.getItem('sf-dismiss:' + storageKey) === '1' : false;
const setDismissed = () => { if(storageKey) try{ store.setItem('sf-dismiss:' + storageKey, '1'); }catch(e){} };
// Dismiss button
const dismissBtn = wrap.querySelector('.sf-dismiss');
if(dismissBtn){
dismissBtn.addEventListener('click', () => {
wrap.classList.remove('sf-in');
setDismissed();
});
}
// If previously dismissed, keep hidden
if(isDismissed()){
wrap.style.display = 'none';
return;
}
// Visibility state
let lastY = window.scrollY || window.pageYOffset;
let shown = false;
function show(){
if(!shown){
wrap.classList.add('sf-in');
shown = true;
}
}
function hide(){
if(shown){
wrap.classList.remove('sf-in');
shown = false;
}
}
// Near-footer hiding (optional)
let nearEl = null;
if(nearSel){
try{ nearEl = document.querySelector(nearSel); }catch(e){}
}
function nearFooter(){
if(!nearEl) return false;
const rect = nearEl.getBoundingClientRect();
const vh = window.innerHeight || document.documentElement.clientHeight;
// If the top of the target is within nearMargin of viewport bottom, consider "near"
return (rect.top - vh) < nearMargin;
}
// Core logic: reveal after threshold and optionally only when scrolling up
function onScroll(){
const y = window.scrollY || window.pageYOffset;
// Hide if near footer to avoid overlap
if(nearFooter()){ hide(); lastY = y; return; }
const passed = y > after;
const scrollingUp = y < lastY - 2; // small hysteresis
const scrollingDown = y > lastY + 2;
if(!upOnly){
// Basic: show after threshold, hide if above
if(passed) show(); else hide();
} else {
// Up-only: show when user scrolls up past threshold; hide on down
if(passed && scrollingUp) show();
if(scrollingDown) hide();
if(!passed) hide();
}
lastY = y;
}
// Throttle (animation frame)
let ticking = false;
function rafScroll(){
if(!ticking){
window.requestAnimationFrame(() => {
onScroll();
ticking = false;
});
ticking = true;
}
}
// First paint
onScroll();
// Listeners
window.addEventListener('scroll', rafScroll, { passive: true });
window.addEventListener('resize', rafScroll);
// Reduced motion: still handles show/hide instantly
if(prefersReduced){
wrap.style.transitionDuration = '1ms';
}
})();
</script>
</body>
How it works
- A fixed .sf-wrap sits at the bottom, initially translated out of view.
- The script listens for scroll and toggles .sf-in based on:data-sf-after (pixels scrolled before eligible),data-sf-up-only (only reveal on upward scroll), andproximity to a selector (e.g., footer) to avoid overlapping the real footer.
- A dismiss button persists the choice via localStorage (or sessionStorage).
Quick customization
Per-bar (data attributes):
- data-sf-after="280" — show after 280px scrolled.
- data-sf-up-only="true" — only reveal when user scrolls up.
- data-sf-hide-near="footer" — CSS selector to hide when near that element.
- data-sf-near-margin="320" — how close (px) to the selector before hiding.
- data-sf-storage="promo-q4" — persistence key for dismiss.
- data-sf-session="true" — use sessionStorage instead of localStorage.
Content & actions:
- Replace the lead copy, link the primary button to a collection or product.
- Add any Shopify Liquid content inside .sf-bar (cart total, dynamic code, etc.).
Global feel (CSS tokens):
- --sf-duration, --sf-ease, --sf-shadow, --sf-border, --sf-accent.
Performance & accessibility notes
- Uses transform animations (GPU-friendly) and passive scroll listeners + rAF throttling.
- Honors prefers-reduced-motion by effectively removing the slide animation.
- aria-live="polite" announces copy changes gently if you swap content dynamically.
- Avoid too-tall footers on mobile; keep actions concise and tap-targets ≥44px.
- Compress images/icons if you add them; don’t overdo box-shadows on low-end devices.
Turning this into a Liquid section (optional)
Create /sections/sticky-footer.liquid, move the markup inside, and add a JSON schema to expose fields for copy, buttons, thresholds, up-only toggle, and storage key. The CSS/JS can remain co-located or be moved to theme assets; behavior is identical.