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
This is an example implementation you can adapt anywhere. If later you want editor controls, convert it into a Liquid section and expose settings in the schema; the HTML/CSS/JS stays the same.
Copy-Paste Example — Fade-In 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">
<title>Shopify Fade-In on Scroll</title>
<style>
:root{
/* Brand + motion tokens */
--fi-bg: #0f1115;
--fi-fg: #e8ecf1;
--fi-muted: #aeb8c6;
--fi-accent: #00d084;
--fi-duration: 680ms; /* default animation duration */
--fi-distance: 22px; /* default translateY start */
--fi-stagger-step: 90ms; /* default stagger gap */
}
html,body{ margin:0; background:var(--fi-bg); color:var(--fi-fg); font-family: Arial, sans-serif; }
/* Demo container (safe to change/remove) */
.fi-container{ max-width: 1200px; margin:0 auto; padding: 40px 18px 90px; }
/* Section layout for demo */
.fi-hero{
min-height: 56vh; display:grid; place-items:center; text-align:center; padding: 60px 16px 30px;
background: linear-gradient(180deg, #111624, #0f1115);
}
.fi-hero h1{ margin:0 0 10px; font-size: clamp(28px, 5.6vw, 56px); line-height: 1.06; }
.fi-hero p{ margin:0 auto; max-width: 64ch; color: var(--fi-muted); }
.fi-grid{
display:grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 26px;
}
@media (max-width: 980px){ .fi-grid{ grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px){ .fi-grid{ grid-template-columns: 1fr; } }
.fi-card{
background:#151924; border:1px solid #1f2636; border-radius:14px; overflow:hidden;
box-shadow: 0 10px 30px rgba(0,0,0,.22);
}
.fi-card img{ width:100%; height: 200px; object-fit: cover; display:block; }
.fi-card .pad{ padding: 14px; }
.fi-card h3{ margin: 6px 0 4px; font-size: 18px; }
.fi-card p{ margin:0; color: var(--fi-muted); font-size: 14px; }
/* ========= Fade-In System =========
Elements marked with [data-fi] start hidden and offset.
JS adds .fi-in when they intersect the viewport. */
[data-fi]{
opacity: 0;
transform: translate3d(0, var(--fi-distance), 0);
will-change: opacity, transform;
transition-property: opacity, transform;
transition-duration: var(--fi-duration);
transition-timing-function: cubic-bezier(.2,.65,.2,1);
transition-delay: 0ms;
}
/* Final state when in view */
.fi-in{
opacity: 1;
transform: none;
}
/* Optional presets: adjust starting transform per-effect */
.fi-up { transform: translate3d(0, 22px, 0); }
.fi-down { transform: translate3d(0,-22px, 0); }
.fi-left { transform: translate3d(22px, 0, 0); }
.fi-right { transform: translate3d(-22px,0, 0); }
.fi-scale { transform: scale(.96); }
/* Reduced motion: reveal immediately with no animation */
@media (prefers-reduced-motion: reduce){
[data-fi]{
transition-duration: 1ms !important;
transform: none !important;
}
}
</style>
<!-- Hero -->
<section class="fi-hero">
<div>
<h1 data-fi class="fi-up" data-fi-delay="80">Fade-In on Scroll for Shopify</h1>
<p data-fi class="fi-up" data-fi-delay="160">Lightweight, modern, and accessible. Paste once, reuse everywhere. Works for headings, cards, images, and buttons.</p>
</div>
</section>
<!-- Content area with staggered cards -->
<section class="fi-container" data-fi-stagger=".fi-card" data-fi-stagger-step="90">
<div class="fi-grid">
<article class="fi-card" data-fi>
<img src="https://picsum.photos/id/1000/1200/800" alt="Product 1">
<div class="pad">
<h3>Merino Crew</h3>
<p>All-season knit with soft hand feel.</p>
</div>
</article>
<article class="fi-card" data-fi>
<img src="https://picsum.photos/id/1005/1200/800" alt="Product 2">
<div class="pad">
<h3>Packable Puffer</h3>
<p>Lightweight warmth that compresses small.</p>
</div>
</article>
<article class="fi-card" data-fi>
<img src="https://picsum.photos/id/1020/1200/800" alt="Product 3">
<div class="pad">
<h3>Minimal Runner</h3>
<p>Cushioned ride, low-profile silhouette.</p>
</div>
</article>
</div>
</section>
<script>
(function(){
const supportsIO = 'IntersectionObserver' in window;
const prefersReduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// If no IO or reduced motion, just reveal everything
if(!supportsIO || prefersReduced){
document.querySelectorAll('[data-fi]').forEach(el => el.classList.add('fi-in'));
return;
}
// Apply per-element timing from data attributes
function applyTiming(el){
const dur = parseInt(el.getAttribute('data-fi-duration') || '', 10);
const del = parseInt(el.getAttribute('data-fi-delay') || '', 10);
const ease = el.getAttribute('data-fi-ease');
if(Number.isFinite(dur)) el.style.transitionDuration = dur + 'ms';
if(Number.isFinite(del)) el.style.transitionDelay = del + 'ms';
if(ease) el.style.transitionTimingFunction = ease;
}
// Container stagger support
function applyStagger(container){
const sel = container.getAttribute('data-fi-stagger');
if(!sel) return;
const step = parseInt(container.getAttribute('data-fi-stagger-step') || '90', 10);
const items = container.querySelectorAll(sel + '[data-fi], ' + sel + ' [data-fi]');
items.forEach((el, i) => {
const existing = parseInt(el.getAttribute('data-fi-delay') || '0', 10);
el.setAttribute('data-fi-delay', String(existing + i * step));
});
}
// Prepare container staggers first
document.querySelectorAll('[data-fi-stagger]').forEach(applyStagger);
// Build observers
const defaultThreshold = 0.12;
function observe(el){
applyTiming(el);
const repeat = el.getAttribute('data-fi-repeat') === 'true'; // default once
const offset = parseFloat(el.getAttribute('data-fi-offset') || '');
if(Number.isFinite(offset)){
// Custom threshold for this element
const custom = new IntersectionObserver((entries)=>{
entries.forEach(entry=>{
if(entry.intersectionRatio >= offset){
el.classList.add('fi-in');
if(!repeat) custom.unobserve(el);
}else if(repeat){
el.classList.remove('fi-in');
}
});
}, { threshold: [offset] });
custom.observe(el);
return;
}
io.observe(el);
}
const io = new IntersectionObserver((entries)=>{
entries.forEach(entry=>{
const el = entry.target;
const repeat = el.getAttribute('data-fi-repeat') === 'true';
if(entry.isIntersecting){
el.classList.add('fi-in');
if(!repeat) io.unobserve(el);
} else if(repeat){
el.classList.remove('fi-in');
}
});
}, { threshold: defaultThreshold, rootMargin: '0px 0px -8% 0px' });
document.querySelectorAll('[data-fi]').forEach(observe);
})();
</script>
</body>
How it works
- Mark any element you want to animate with [data-fi].
- Optional effect presets: add fi-up, fi-down, fi-left, fi-right, or fi-scale to adjust the starting transform.
- When the element intersects the viewport, JS adds .fi-in so it transitions to full opacity and its natural position.
- The system honors prefers-reduced-motion and reveals content instantly when users opt out of animation or when IntersectionObserver isn’t available.
Quick customization
- Per element:data-fi-delay="200" to stagger without a container.data-fi-duration="800" to lengthen a specific item.data-fi-ease="ease-out" to customize easing.data-fi-offset="0.25" to wait until 25% of the element is visible.data-fi-repeat="true" to animate again when it re-enters view.
- Per container:data-fi-stagger=".fi-card" and data-fi-stagger-step="90" to cascade children automatically.
- Global feel:Edit --fi-duration, --fi-distance, or preset transforms for stronger/subtler motion.
Performance and accessibility notes
- Animating transforms and opacity is GPU-friendly and avoids layout thrash.
- Keep durations 500–800 ms and delays 60–200 ms for a premium feel without sluggishness.
- Large images should be compressed and lazy-loaded to keep scroll smooth.
- Don’t animate everything; use motion to clarify hierarchy.
- The snippet respects reduced motion by revealing instantly.
Turning this into a Liquid section (optional)
This post uses a plain snippet so you can paste and test quickly. To make it a proper Section with settings, create /sections/fade-in.liquid, move the markup inside, and add a JSON schema to expose copy fields, toggles (repeat, offsets), and stagger controls. The CSS/JS stays the same; only content sources change.