Shopify Scroll & product storytelling
Arrange layers, chapters and supporting visuals in order. Keep a natural reading path on phones.
Layered parallax scene
Move a supporting background more slowly than the message to create limited depth.
Paused — press Play to interact.
Abstract illustrations and sample text. Scroll effects use a local preview area; page integration uses the Section’s own scroll interval.
Where does this belong in Shopify?
A brand-story or product-introduction Section with separately rendered copy and imagery.
How does the interaction work?
This preview reads its local scrollTop, coalesces updates with requestAnimationFrame and caps displacement at ±30px. For a page Section, map its viewport progress instead of copying the preview scroller.
How do touch, keyboard and reduced motion work?
Reduced motion keeps the background still. Lower or disable displacement on phones. Copy stays in normal flow and page scrolling remains native.
What should be checked before launch?
Check covered image edges, readable copy, no pending frames after pause and listener cleanup on Section removal.
Example source · parallax-scene.html
<section class="fx" data-effect="parallax-scene"><small>09 / LAYERS WITH A PURPOSE</small><div class="scroller" tabindex="0" aria-label="Scroll the layered scene"><div class="spacer">Scroll inside this preview ↓</div><div class="scene"><div class="art scene-art" aria-hidden="true"></div><h2>LESS, BUT<br>BETTER.</h2></div><div class="spacer">The message remains in normal document flow.</div></div></section>
Example source · parallax-scene.css
*{box-sizing:border-box}body{margin:0;background:#11251f;color:#eef5e6;font:15px/1.5 system-ui,sans-serif}button,input{font:inherit}button{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,[tabindex]:focus-visible{outline:3px solid #d5f875;outline-offset:4px}button{border:1px solid #819a8a;border-radius:30px;padding:10px 18px;color:inherit;background:transparent}button:hover{background:#294638}.fx{position:relative;min-height:350px;padding:30px;overflow:hidden;display:grid;align-content:center;gap:20px}.fx h2,.fx p{margin:0}.fx h2{font-size:clamp(32px,7vw,68px);letter-spacing:-.05em;line-height:1.08}.fx small{letter-spacing:.12em;color:#bed0c1}.art{position:relative;min-height:180px;border-radius:14px;overflow:hidden;background:radial-gradient(circle at 70% 25%,#d5f875 0 14%,transparent 14.5%),linear-gradient(135deg,#087e75,#183c30)}.art:before{content:'';position:absolute;width:130px;height:130px;border:25px solid #cde4d0;border-radius:30px;left:20%;top:25%;transform:rotate(-25deg)}.art:after{content:'';position:absolute;inset:16px;border:1px solid #cde4d044;border-radius:8px}.art[data-art="2"]{background:radial-gradient(circle at 25% 70%,#a7b7ff 0 20%,transparent 20.5%),linear-gradient(45deg,#102943,#687ab1)}.art[data-art="3"]{background:radial-gradient(circle at 60% 50%,#ffd9a9 0 24%,transparent 24.5%),linear-gradient(135deg,#492e30,#a97652)}.hint{font-size:12px;color:#b9cbbd}.controls{display:flex;gap:8px;flex-wrap:wrap}.scroller{height:270px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#b5cc91 #18382d;border-radius:10px;position:relative;outline-offset:2px}.spacer{height:100px;display:grid;place-items:center;color:#b9cbbd;font-size:12px}.fx [hidden]{display:none!important}.fx[data-playing="false"] *{animation-play-state:paused!important}.fx button[aria-pressed="true"]{background:#d5f875;color:#11251f}.fx a{color:inherit;text-decoration:underline;text-underline-offset:4px}
@media(max-width:500px){.fx{padding:20px;min-height:330px}.fx h2{font-size:36px}.art{min-height:160px}}
@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
.fx[data-reduced-motion=true] *,.fx[data-reduced-motion=true] *:before,.fx[data-reduced-motion=true] *:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}
.fx[data-reduced-motion=true] .stack-card{position:relative;top:0!important}.fx[data-reduced-motion=true] .horizontal-story{height:270px}.fx[data-reduced-motion=true] .horizontal-pin{overflow-x:auto}.fx[data-reduced-motion=true] .horizontal-track{transform:none!important}.fx[data-reduced-motion=true] .story-grid{grid-template-columns:1fr}.fx[data-reduced-motion=true] .story-image{position:static}.fx[data-reduced-motion=true] .loop-window{overflow-x:auto}.fx[data-reduced-motion=true] .loop-set[aria-hidden=true]{display:none}
.scene{min-height:260px;position:relative;display:grid;place-items:center;overflow:hidden}.scene-art{position:absolute;inset:-35px 0;min-height:0;will-change:transform}.scene h2{position:relative;text-align:center;text-shadow:0 2px 20px #0009}.scroller{height:290px}
Example source · parallax-scene.js
// WESWOO original educational example; no third-party runtime required.
export function mountEffect(root, {reducedMotion=false}={}) {
const reduced=reducedMotion||matchMedia('(prefers-reduced-motion: reduce)').matches;
root.dataset.reducedMotion=String(reduced);
const life=new AbortController(), animations=[];
let active=false, started=false;
const on=(node,type,fn,options={})=>node.addEventListener(type,fn,{...options,signal:life.signal});
const animate=(node,frames,options)=>{
if(reduced)return null;
const animation=node.animate(frames,options);animations.push(animation);return animation;
};
let start=()=>{}, stop=()=>{}, cleanup=()=>{};
const scroller=root.querySelector('.scroller'),art=root.querySelector('.scene-art');let frame=0;
function render(){frame=0;if(active&&!reduced)art.style.transform=`translateY(${Math.max(-30,Math.min(30,(scroller.scrollTop-80)*.18))}px)`;}
on(scroller,'scroll',()=>{if(active&&!frame)frame=requestAnimationFrame(render);},{passive:true});start=render;stop=()=>{cancelAnimationFrame(frame);frame=0;};cleanup=()=>{cancelAnimationFrame(frame);art.style.transform='';};
root.dataset.playing='false';
return {
resume(){active=true;root.dataset.playing='true';if(!started){started=true;start();}animations.forEach(a=>a.play());root.dispatchEvent(new Event('ww:resume'));},
pause(){active=false;root.dataset.playing='false';animations.forEach(a=>a.pause());stop();},
destroy(){active=false;life.abort();animations.forEach(a=>a.cancel());cleanup();}
};
}
Reference effect & official sources
View the reference on the WESWOO case siteA newly written, lightweight implementation of the same interaction family. It does not reproduce the reference theme or its bundled source code.
- Shopify: Section architecture
- Shopify: theme editor events and lifecycle
- Shopify: theme performance
- MDN: Web Animations API
- MDN: reduced-motion preference
- MDN: iframe sandboxing
WESWOO articles:Further reading will be added after a matching article is reviewed.
Stacked story chapters
Layer short chapters in order to explain materials, construction or a design process.
Paused — press Play to interact.
Abstract illustrations and sample text. Scroll effects use a local preview area; page integration uses the Section’s own scroll interval.
Where does this belong in Shopify?
A product-story Section with one short chapter per block. Keep visual and source order aligned and purchase forms outside the stack.
How does the interaction work?
Local CSS sticky positioning with increasing top offsets ends at the containing block. No scroll locking or pinning dependency is required. Pausing the preview returns ordinary flow.
How do touch, keyboard and reduced motion work?
Reduced motion uses normal flow. Long phone content should also use a vertical layout or the existing theme carousel.
What should be checked before launch?
Check full text visibility, the final chapter boundary, matching keyboard order and untrapped phone scrolling.
Example source · pinned-stack.html
<section class="fx" data-effect="pinned-stack"><small>10 / A STORY IN LAYERS</small><div class="scroller" tabindex="0" aria-label="Scroll the stacked story"><div class="spacer">Scroll to stack the chapters ↓</div><div class="stack"><article class="stack-card"><div class="art" data-art="1"></div><strong>01 — The idea</strong></article><article class="stack-card"><div class="art" data-art="2"></div><strong>02 — The material</strong></article><article class="stack-card"><div class="art" data-art="3"></div><strong>03 — The detail</strong></article></div><div class="spacer">Keep each chapter short enough to read.</div></div></section>
Example source · pinned-stack.css
*{box-sizing:border-box}body{margin:0;background:#11251f;color:#eef5e6;font:15px/1.5 system-ui,sans-serif}button,input{font:inherit}button{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,[tabindex]:focus-visible{outline:3px solid #d5f875;outline-offset:4px}button{border:1px solid #819a8a;border-radius:30px;padding:10px 18px;color:inherit;background:transparent}button:hover{background:#294638}.fx{position:relative;min-height:350px;padding:30px;overflow:hidden;display:grid;align-content:center;gap:20px}.fx h2,.fx p{margin:0}.fx h2{font-size:clamp(32px,7vw,68px);letter-spacing:-.05em;line-height:1.08}.fx small{letter-spacing:.12em;color:#bed0c1}.art{position:relative;min-height:180px;border-radius:14px;overflow:hidden;background:radial-gradient(circle at 70% 25%,#d5f875 0 14%,transparent 14.5%),linear-gradient(135deg,#087e75,#183c30)}.art:before{content:'';position:absolute;width:130px;height:130px;border:25px solid #cde4d0;border-radius:30px;left:20%;top:25%;transform:rotate(-25deg)}.art:after{content:'';position:absolute;inset:16px;border:1px solid #cde4d044;border-radius:8px}.art[data-art="2"]{background:radial-gradient(circle at 25% 70%,#a7b7ff 0 20%,transparent 20.5%),linear-gradient(45deg,#102943,#687ab1)}.art[data-art="3"]{background:radial-gradient(circle at 60% 50%,#ffd9a9 0 24%,transparent 24.5%),linear-gradient(135deg,#492e30,#a97652)}.hint{font-size:12px;color:#b9cbbd}.controls{display:flex;gap:8px;flex-wrap:wrap}.scroller{height:270px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#b5cc91 #18382d;border-radius:10px;position:relative;outline-offset:2px}.spacer{height:100px;display:grid;place-items:center;color:#b9cbbd;font-size:12px}.fx [hidden]{display:none!important}.fx[data-playing="false"] *{animation-play-state:paused!important}.fx button[aria-pressed="true"]{background:#d5f875;color:#11251f}.fx a{color:inherit;text-decoration:underline;text-underline-offset:4px}
@media(max-width:500px){.fx{padding:20px;min-height:330px}.fx h2{font-size:36px}.art{min-height:160px}}
@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
.fx[data-reduced-motion=true] *,.fx[data-reduced-motion=true] *:before,.fx[data-reduced-motion=true] *:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}
.fx[data-reduced-motion=true] .stack-card{position:relative;top:0!important}.fx[data-reduced-motion=true] .horizontal-story{height:270px}.fx[data-reduced-motion=true] .horizontal-pin{overflow-x:auto}.fx[data-reduced-motion=true] .horizontal-track{transform:none!important}.fx[data-reduced-motion=true] .story-grid{grid-template-columns:1fr}.fx[data-reduced-motion=true] .story-image{position:static}.fx[data-reduced-motion=true] .loop-window{overflow-x:auto}.fx[data-reduced-motion=true] .loop-set[aria-hidden=true]{display:none}
.stack{padding:0 15px 65px}.stack-card{position:sticky;top:8px;background:#254336;padding:10px;border-radius:12px;margin-bottom:90px;box-shadow:0 8px 24px #0003}.stack-card:nth-child(2){top:19px}.stack-card:nth-child(3){top:30px}.stack-card .art{min-height:160px}.stack-card strong{display:block;padding:10px}.fx[data-playing=false] .stack-card{position:relative;top:0}@media(prefers-reduced-motion:reduce){.stack-card{position:relative;top:0!important}}
Example source · pinned-stack.js
// WESWOO original educational example; no third-party runtime required.
export function mountEffect(root, {reducedMotion=false}={}) {
const reduced=reducedMotion||matchMedia('(prefers-reduced-motion: reduce)').matches;
root.dataset.reducedMotion=String(reduced);
const life=new AbortController(), animations=[];
let active=false, started=false;
const on=(node,type,fn,options={})=>node.addEventListener(type,fn,{...options,signal:life.signal});
const animate=(node,frames,options)=>{
if(reduced)return null;
const animation=node.animate(frames,options);animations.push(animation);return animation;
};
let start=()=>{}, stop=()=>{}, cleanup=()=>{};
// Sticky positioning supplies the effect. No scroll listener or pinning library.
// The shared lifecycle toggles data-playing; the reduced-motion CSS returns normal flow.
root.dataset.playing='false';
return {
resume(){active=true;root.dataset.playing='true';if(!started){started=true;start();}animations.forEach(a=>a.play());root.dispatchEvent(new Event('ww:resume'));},
pause(){active=false;root.dataset.playing='false';animations.forEach(a=>a.pause());stop();},
destroy(){active=false;life.abort();animations.forEach(a=>a.cancel());cleanup();}
};
}
Reference effect & official sources
View the reference on the WESWOO case siteA newly written, lightweight implementation of the same interaction family. It does not reproduce the reference theme or its bundled source code.
- Shopify: Section architecture
- Shopify: theme editor events and lifecycle
- Shopify: theme performance
- MDN: Web Animations API
- MDN: reduced-motion preference
- MDN: iframe sandboxing
WESWOO articles:Further reading will be added after a matching article is reviewed.
Scroll-driven horizontal gallery
Browse a small sequence of visual chapters across a bounded horizontal track.
Paused — press Play to interact.
Abstract illustrations and sample text. Scroll effects use a local preview area; page integration uses the Section’s own scroll interval.
Where does this belong in Shopify?
A brand-feature Section, not a replacement for collection filters or purchase flow.
How does the interaction work?
On desktop, normalize a local sticky interval to 0–1 and multiply by track overflow. Recalculate on resize. In a page, use the Section interval rather than the preview scroller.
How do touch, keyboard and reduced motion work?
Phones and reduced motion use direct native horizontal scrolling, without wheel remapping. The preview scroller makes the mapping inspectable.
What should be checked before launch?
Check complete first and final frames, resizing without a blank tail, native page scrolling and canceled frames on pause.
Example source · horizontal-gallery.html
<section class="fx" data-effect="horizontal-gallery"><small>11 / A HORIZONTAL CHAPTER</small><div class="scroller" tabindex="0" aria-label="Scroll vertically to move the gallery"><div class="horizontal-story"><div class="horizontal-pin"><div class="horizontal-track"><div class="art" data-art="1"><strong>01 / Explore</strong></div><div class="art" data-art="2"><strong>02 / Compare</strong></div><div class="art" data-art="3"><strong>03 / Decide</strong></div></div></div></div></div><p class="hint">On small screens or reduced motion, swipe the gallery directly.</p></section>
Example source · horizontal-gallery.css
*{box-sizing:border-box}body{margin:0;background:#11251f;color:#eef5e6;font:15px/1.5 system-ui,sans-serif}button,input{font:inherit}button{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,[tabindex]:focus-visible{outline:3px solid #d5f875;outline-offset:4px}button{border:1px solid #819a8a;border-radius:30px;padding:10px 18px;color:inherit;background:transparent}button:hover{background:#294638}.fx{position:relative;min-height:350px;padding:30px;overflow:hidden;display:grid;align-content:center;gap:20px}.fx h2,.fx p{margin:0}.fx h2{font-size:clamp(32px,7vw,68px);letter-spacing:-.05em;line-height:1.08}.fx small{letter-spacing:.12em;color:#bed0c1}.art{position:relative;min-height:180px;border-radius:14px;overflow:hidden;background:radial-gradient(circle at 70% 25%,#d5f875 0 14%,transparent 14.5%),linear-gradient(135deg,#087e75,#183c30)}.art:before{content:'';position:absolute;width:130px;height:130px;border:25px solid #cde4d0;border-radius:30px;left:20%;top:25%;transform:rotate(-25deg)}.art:after{content:'';position:absolute;inset:16px;border:1px solid #cde4d044;border-radius:8px}.art[data-art="2"]{background:radial-gradient(circle at 25% 70%,#a7b7ff 0 20%,transparent 20.5%),linear-gradient(45deg,#102943,#687ab1)}.art[data-art="3"]{background:radial-gradient(circle at 60% 50%,#ffd9a9 0 24%,transparent 24.5%),linear-gradient(135deg,#492e30,#a97652)}.hint{font-size:12px;color:#b9cbbd}.controls{display:flex;gap:8px;flex-wrap:wrap}.scroller{height:270px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#b5cc91 #18382d;border-radius:10px;position:relative;outline-offset:2px}.spacer{height:100px;display:grid;place-items:center;color:#b9cbbd;font-size:12px}.fx [hidden]{display:none!important}.fx[data-playing="false"] *{animation-play-state:paused!important}.fx button[aria-pressed="true"]{background:#d5f875;color:#11251f}.fx a{color:inherit;text-decoration:underline;text-underline-offset:4px}
@media(max-width:500px){.fx{padding:20px;min-height:330px}.fx h2{font-size:36px}.art{min-height:160px}}
@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
.fx[data-reduced-motion=true] *,.fx[data-reduced-motion=true] *:before,.fx[data-reduced-motion=true] *:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}
.fx[data-reduced-motion=true] .stack-card{position:relative;top:0!important}.fx[data-reduced-motion=true] .horizontal-story{height:270px}.fx[data-reduced-motion=true] .horizontal-pin{overflow-x:auto}.fx[data-reduced-motion=true] .horizontal-track{transform:none!important}.fx[data-reduced-motion=true] .story-grid{grid-template-columns:1fr}.fx[data-reduced-motion=true] .story-image{position:static}.fx[data-reduced-motion=true] .loop-window{overflow-x:auto}.fx[data-reduced-motion=true] .loop-set[aria-hidden=true]{display:none}
.horizontal-story{height:850px}.horizontal-pin{position:sticky;top:0;height:270px;overflow:hidden}.horizontal-track{display:flex;gap:15px;padding:10px;width:max-content;will-change:transform}.horizontal-track .art{width:min(440px,70vw);height:240px;flex:none}.horizontal-track strong{position:absolute;bottom:20px;left:20px;z-index:1}.fx[data-playing=false] .horizontal-story{height:270px}.fx[data-playing=false] .horizontal-pin{overflow-x:auto}.fx[data-playing=false] .horizontal-track{transform:none!important}@media(max-width:600px),(prefers-reduced-motion:reduce){.horizontal-story{height:270px}.horizontal-pin{overflow-x:auto;scroll-snap-type:x mandatory}.horizontal-track{transform:none!important}.horizontal-track .art{scroll-snap-align:start}}
Example source · horizontal-gallery.js
// WESWOO original educational example; no third-party runtime required.
export function mountEffect(root, {reducedMotion=false}={}) {
const reduced=reducedMotion||matchMedia('(prefers-reduced-motion: reduce)').matches;
root.dataset.reducedMotion=String(reduced);
const life=new AbortController(), animations=[];
let active=false, started=false;
const on=(node,type,fn,options={})=>node.addEventListener(type,fn,{...options,signal:life.signal});
const animate=(node,frames,options)=>{
if(reduced)return null;
const animation=node.animate(frames,options);animations.push(animation);return animation;
};
let start=()=>{}, stop=()=>{}, cleanup=()=>{};
const scroller=root.querySelector('.scroller'),story=root.querySelector('.horizontal-story'),track=root.querySelector('.horizontal-track'),pin=root.querySelector('.horizontal-pin'),small=matchMedia('(max-width:600px)');let frame=0;
function render(){frame=0;if(!active||reduced||small.matches){track.style.transform='';return;}const max=Math.max(1,story.offsetHeight-pin.clientHeight),p=Math.max(0,Math.min(1,scroller.scrollTop/max));track.style.transform=`translateX(${-p*Math.max(0,track.scrollWidth-pin.clientWidth)}px)`;}
on(scroller,'scroll',()=>{if(active&&!frame)frame=requestAnimationFrame(render);},{passive:true});on(window,'resize',render);start=render;stop=()=>{cancelAnimationFrame(frame);frame=0;track.style.transform='';};cleanup=stop;
root.dataset.playing='false';
return {
resume(){active=true;root.dataset.playing='true';if(!started){started=true;start();}animations.forEach(a=>a.play());root.dispatchEvent(new Event('ww:resume'));},
pause(){active=false;root.dataset.playing='false';animations.forEach(a=>a.pause());stop();},
destroy(){active=false;life.abort();animations.forEach(a=>a.cancel());cleanup();}
};
}
Reference effect & official sources
View the reference on the WESWOO case siteA newly written, lightweight implementation of the same interaction family. It does not reproduce the reference theme or its bundled source code.
- Shopify: Section architecture
- Shopify: theme editor events and lifecycle
- Shopify: theme performance
- MDN: Web Animations API
- MDN: reduced-motion preference
- MDN: iframe sandboxing
WESWOO articles:Further reading will be added after a matching article is reviewed.
Sticky visual with a reading sequence
Keep a supporting image visible while explaining structure, material and everyday value in order.
Paused — press Play to interact.
Abstract illustrations and sample text. Scroll effects use a local preview area; page integration uses the Section’s own scroll interval.
Where does this belong in Shopify?
A product-information Section. Put specifications in text instead of baking essential facts into images.
How does the interaction work?
Use a shared two-column container with a sticky visual aligned to its start. Copy remains in normal source order and the parent bounds the sticky interval.
How do touch, keyboard and reduced motion work?
Small screens and reduced motion use one column. Content remains readable without JavaScript.
What should be checked before launch?
Check captions on short screens, source order, the Section boundary and no phone overlap or clipping.
Example source · sticky-story.html
<section class="fx" data-effect="sticky-story"><small>12 / EXPLAIN THEN SHOW</small><div class="scroller" tabindex="0" aria-label="Read the two-column product story"><div class="story-grid"><div class="story-image"><div class="art" role="img" aria-label="Abstract product construction illustration"></div><p>One visual. Three meaningful details.</p></div><div class="story-copy"><article><h3>01 / Structure</h3><p>Start with the job this part has to do.</p></article><article><h3>02 / Material</h3><p>Explain the choice, its source and its limits.</p></article><article><h3>03 / Everyday use</h3><p>Connect the detail to a real usage decision.</p></article></div></div></div></section>
Example source · sticky-story.css
*{box-sizing:border-box}body{margin:0;background:#11251f;color:#eef5e6;font:15px/1.5 system-ui,sans-serif}button,input{font:inherit}button{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,[tabindex]:focus-visible{outline:3px solid #d5f875;outline-offset:4px}button{border:1px solid #819a8a;border-radius:30px;padding:10px 18px;color:inherit;background:transparent}button:hover{background:#294638}.fx{position:relative;min-height:350px;padding:30px;overflow:hidden;display:grid;align-content:center;gap:20px}.fx h2,.fx p{margin:0}.fx h2{font-size:clamp(32px,7vw,68px);letter-spacing:-.05em;line-height:1.08}.fx small{letter-spacing:.12em;color:#bed0c1}.art{position:relative;min-height:180px;border-radius:14px;overflow:hidden;background:radial-gradient(circle at 70% 25%,#d5f875 0 14%,transparent 14.5%),linear-gradient(135deg,#087e75,#183c30)}.art:before{content:'';position:absolute;width:130px;height:130px;border:25px solid #cde4d0;border-radius:30px;left:20%;top:25%;transform:rotate(-25deg)}.art:after{content:'';position:absolute;inset:16px;border:1px solid #cde4d044;border-radius:8px}.art[data-art="2"]{background:radial-gradient(circle at 25% 70%,#a7b7ff 0 20%,transparent 20.5%),linear-gradient(45deg,#102943,#687ab1)}.art[data-art="3"]{background:radial-gradient(circle at 60% 50%,#ffd9a9 0 24%,transparent 24.5%),linear-gradient(135deg,#492e30,#a97652)}.hint{font-size:12px;color:#b9cbbd}.controls{display:flex;gap:8px;flex-wrap:wrap}.scroller{height:270px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#b5cc91 #18382d;border-radius:10px;position:relative;outline-offset:2px}.spacer{height:100px;display:grid;place-items:center;color:#b9cbbd;font-size:12px}.fx [hidden]{display:none!important}.fx[data-playing="false"] *{animation-play-state:paused!important}.fx button[aria-pressed="true"]{background:#d5f875;color:#11251f}.fx a{color:inherit;text-decoration:underline;text-underline-offset:4px}
@media(max-width:500px){.fx{padding:20px;min-height:330px}.fx h2{font-size:36px}.art{min-height:160px}}
@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
.fx[data-reduced-motion=true] *,.fx[data-reduced-motion=true] *:before,.fx[data-reduced-motion=true] *:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}
.fx[data-reduced-motion=true] .stack-card{position:relative;top:0!important}.fx[data-reduced-motion=true] .horizontal-story{height:270px}.fx[data-reduced-motion=true] .horizontal-pin{overflow-x:auto}.fx[data-reduced-motion=true] .horizontal-track{transform:none!important}.fx[data-reduced-motion=true] .story-grid{grid-template-columns:1fr}.fx[data-reduced-motion=true] .story-image{position:static}.fx[data-reduced-motion=true] .loop-window{overflow-x:auto}.fx[data-reduced-motion=true] .loop-set[aria-hidden=true]{display:none}
.story-grid{display:grid;grid-template-columns:1fr 1fr;gap:25px;padding:10px}.story-image{position:sticky;top:10px;align-self:start}.story-image p{font-size:12px;margin-top:12px}.story-copy article{min-height:200px;padding:15px 0}.story-copy h3{color:#d5f875}.fx[data-playing=false] .story-image{position:static}@media(max-width:600px),(prefers-reduced-motion:reduce){.story-grid{grid-template-columns:1fr}.story-image{position:static}.story-copy article{min-height:120px}}
Example source · sticky-story.js
// WESWOO original educational example; no third-party runtime required.
export function mountEffect(root, {reducedMotion=false}={}) {
const reduced=reducedMotion||matchMedia('(prefers-reduced-motion: reduce)').matches;
root.dataset.reducedMotion=String(reduced);
const life=new AbortController(), animations=[];
let active=false, started=false;
const on=(node,type,fn,options={})=>node.addEventListener(type,fn,{...options,signal:life.signal});
const animate=(node,frames,options)=>{
if(reduced)return null;
const animation=node.animate(frames,options);animations.push(animation);return animation;
};
let start=()=>{}, stop=()=>{}, cleanup=()=>{};
// CSS sticky is bounded by .story-grid, never by a transformed global scroller.
// Small screens and reduced-motion preferences use an ordinary reading order.
root.dataset.playing='false';
return {
resume(){active=true;root.dataset.playing='true';if(!started){started=true;start();}animations.forEach(a=>a.play());root.dispatchEvent(new Event('ww:resume'));},
pause(){active=false;root.dataset.playing='false';animations.forEach(a=>a.pause());stop();},
destroy(){active=false;life.abort();animations.forEach(a=>a.cancel());cleanup();}
};
}
Reference effect & official sources
View the reference on the WESWOO case siteA newly written, lightweight implementation of the same interaction family. It does not reproduce the reference theme or its bundled source code.
- Shopify: Section architecture
- Shopify: theme editor events and lifecycle
- Shopify: theme performance
- MDN: Web Animations API
- MDN: reduced-motion preference
- MDN: iframe sandboxing
WESWOO articles:Further reading will be added after a matching article is reviewed.
Choose the effect after the content
Use the knowledge center to plan the page and the developer guides to connect real data. Add motion only where it clarifies the message, and check performance after integration.
Shopify Knowledge Center · Shopify Developer Guides
Shopify Section integration
The preview is plain HTML, CSS and JavaScript. Adapt one effect at a time to a duplicate Shopify theme, with text and images supplied by Section settings.
- Put the selected CSS in assets/ww-effects.css and the selected JavaScript in assets/ww-selected-effect.js. Scope CSS to your Section before merging with theme styles.
- Replace the shell placeholder with the selected .fx element. Replace demo text and abstract artwork with real Section settings and rights-cleared image_tag output. Translate labels through locale files.
- Use the lifecycle module to mount once, pause when hidden and destroy on shopify:section:unload. Adapt preview scroll containers to the real Section bounds. Verify in the theme editor and on real devices.
Integration shell — insert the selected effect markup before use. This is not an installable theme or an App Store app.
sections/ww-effect.liquid
{% comment %} WESWOO example shell. Add ONE effect's HTML inside .fx-slot. {% endcomment %}
{{ 'ww-effects.css' | asset_url | stylesheet_tag }}
<section id="ww-effect-{{ section.id }}" data-ww-effect-section>
<h2>{{ section.settings.heading | escape }}</h2>
<button type="button" data-effect-toggle aria-pressed="false">Play / pause effect</button>
<div class="fx-slot">
{% comment %} Insert the chosen .fx element here; read copy/images from section settings. {% endcomment %}
</div>
</section>
<script type="module" src="{{ 'ww-effect-section.js' | asset_url }}"></script>
{% schema %}
{"name":"WESWOO effect","settings":[{"type":"text","id":"heading","label":"Heading","default":"Our story"}],"presets":[{"name":"WESWOO effect"}]}
{% endschema %}
Shared initialization — one selected effect per module. Multiple effect types need an explicit registry and per-Section mapping.
assets/ww-effect-section.js
// Put the chosen example JS in assets/ww-selected-effect.js (rename .mjs to .js).
import {mountEffect} from './ww-selected-effect.js';
const instances=new Map();
function mount(container=document){
const roots=[...(container.matches?.('[data-ww-effect-section]')?[container]:[]),...container.querySelectorAll('[data-ww-effect-section]')];
for(const section of roots){
if(instances.has(section))continue;
const root=section.querySelector('.fx'),button=section.querySelector('[data-effect-toggle]');
if(!root||!button)continue;
const effect=mountEffect(root),events=new AbortController();let playing=false;
const pause=()=>{playing=false;effect.pause();button.setAttribute('aria-pressed','false');};
button.addEventListener('click',()=>{playing=!playing;playing?effect.resume():effect.pause();button.setAttribute('aria-pressed',String(playing));},{signal:events.signal});
const observer=new IntersectionObserver(entries=>{if(!entries[0].isIntersecting)pause();});observer.observe(section);
document.addEventListener('visibilitychange',()=>{if(document.hidden)pause();},{signal:events.signal});
instances.set(section,()=>{observer.disconnect();events.abort();effect.destroy();});
}
}
document.addEventListener('shopify:section:load',event=>mount(event.target));
document.addEventListener('shopify:section:unload',event=>{for(const [section,destroy] of instances)if(section===event.target||event.target.contains(section)){destroy();instances.delete(section);}});
mount();
The examples are WESWOO-authored educational implementations. You may inspect and copy the displayed examples with attribution to WESWOO and this page. Reference themes, third-party libraries, trademarks and case imagery retain their own rights; they are not included in these downloads.
Theme integration references in Shopify Dawn (pinned version). Dawn code is subject to its Shopify-platform license; the examples above do not bundle it.
- Shopify/dawn · modal / slider base classes
- Shopify/dawn · product media gallery
- Shopify/dawn · license
References checked 2026-11-12