Shopify Galleries & detail viewing
Show the whole first, then let people inspect details or change slides. Reserve loops for decoration.
Staggered gallery reveal
Give three related illustrations a clear entrance, with one purpose per frame.
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 or construction Section. Real images need explicit dimensions, useful alternatives and offscreen lazy loading.
How does the interaction work?
Reveal each image with clip-path and opacity in source order. The play button triggers this example once; a page may use a one-shot IntersectionObserver and disconnect it afterward.
How do touch, keyboard and reduced motion work?
Reduced motion shows every frame statically. On phones, reduce spacing or reuse the theme carousel; essential meaning must not depend on the entrance.
What should be checked before launch?
Check static visibility, reserved dimensions, clean replay and offscreen images that do not compete with the hero.
Example source · reveal-grid.html
<section class="fx" data-effect="reveal-grid"><small>13 / A CONTROLLED ENTRANCE</small><h2>DETAILS MATTER.</h2><div class="reveal-grid"><div class="art" data-art="1" aria-label="Abstract concept one" role="img"></div><div class="art" data-art="2" aria-label="Abstract concept two" role="img"></div><div class="art" data-art="3" aria-label="Abstract concept three" role="img"></div></div></section>
Example source · reveal-grid.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}
.reveal-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.reveal-grid .art{min-height:190px}.reveal-grid .art:before{left:5%;width:100px;height:100px}@media(max-width:500px){.reveal-grid{gap:8px}.reveal-grid .art{min-height:150px}.reveal-grid .art:before{width:60px;height:80px;border-width:12px}}
Example source · reveal-grid.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=()=>{};
start=()=>root.querySelectorAll('.reveal-grid .art').forEach((card,i)=>animate(card,[{clipPath:'inset(100% 0 0 0)',opacity:.3},{clipPath:'inset(0 0 0 0)',opacity:1}],{duration:900,delay:i*160,fill:'both',easing:'cubic-bezier(.2,.7,.2,1)'}));
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.
Detail zoom dialog
Open a closer view on demand to inspect construction, material or finish.
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-media or craftsmanship Section with a real high-resolution image and the current product context.
How does the interaction work?
A user action opens a native dialog with a contained entrance. Close, Escape and backdrop dismissal restore trigger focus. Large assets can load only after the click.
How do touch, keyboard and reduced motion work?
Reduced motion removes zoom. Limit phone dialog dimensions and allow internal scrolling. This is a single-image detail view, not pinch zoom.
What should be checked before launch?
Check focus in and out, preserved page position, alternative text and an always-available close control if an image fails.
Example source · zoom-dialog.html
<section class="fx" data-effect="zoom-dialog"><small>14 / TAKE A CLOSER LOOK</small><button type="button" class="zoom-open" aria-haspopup="dialog" aria-label="Enlarge the abstract detail"><span class="art" aria-hidden="true"></span><span>Explore the detail +</span></button><dialog aria-label="Enlarged abstract detail"><button type="button" data-close>Close ×</button><div class="art" role="img" aria-label="Enlarged abstract construction detail"></div><p>Replace this illustration with a rights-cleared product image and meaningful alternative text.</p></dialog></section>
Example source · zoom-dialog.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}
.zoom-open{padding:0;border:0;border-radius:12px;text-align:left;width:100%}.zoom-open>.art{display:block;min-height:220px}.zoom-open>span:last-child{display:block;padding:12px}dialog{width:calc(100% - 30px);max-width:680px;border:0;border-radius:14px;background:#16392d;color:#eef5e6;padding:20px}dialog::backdrop{background:#000b}dialog .art{margin:15px 0;min-height:230px}dialog p{font-size:12px}dialog[open] .art{animation:detail-in .35s ease-out}@keyframes detail-in{from{transform:scale(.95);opacity:.4}to{transform:none;opacity:1}}
Example source · zoom-dialog.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 dialog=root.querySelector('dialog'),open=root.querySelector('.zoom-open');
on(open,'click',()=>{if(active&&!dialog.open)dialog.showModal();});on(root.querySelector('[data-close]'),'click',()=>dialog.close());
on(dialog,'click',event=>{if(event.target===dialog){const r=dialog.getBoundingClientRect();if(event.clientX<r.left||event.clientX>r.right||event.clientY<r.top||event.clientY>r.bottom)dialog.close();}});
stop=()=>{if(dialog.open)dialog.close();};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.
User-controlled content slider
Let readers choose their pace with a visible next-slide preview, dots and native swiping.
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 homepage, collection introduction or case Section. Prefer the existing theme slider instead of adding another library.
How does the interaction work?
CSS scroll-snap provides native scrolling. Dots align slide starts with scrollTo; scroll updates find the nearest slide and reflect pressed state and status text.
How do touch, keyboard and reduced motion work?
No automatic advancement. Reduced motion positions instantly. Keep a focusable scroll region and usable dots. This local example should be adapted to the existing component.
What should be checked before launch?
Check dragging, first and last dots, keyboard use, the final slide, synchronized state and no host-page overflow.
Example source · content-slider.html
<section class="fx" data-effect="content-slider"><small>15 / CHOOSE YOUR PACE</small><div class="slides" tabindex="0" aria-label="Swipe the three abstract slides"><div class="slide"><div class="art" data-art="1"></div><strong>01 / The concept</strong></div><div class="slide"><div class="art" data-art="2"></div><strong>02 / The system</strong></div><div class="slide"><div class="art" data-art="3"></div><strong>03 / The experience</strong></div></div><div class="slide-dots" role="group" aria-label="Choose a slide"><button type="button" data-slide="0" aria-pressed="true" aria-label="Slide 1">1</button><button type="button" data-slide="1" aria-pressed="false" aria-label="Slide 2">2</button><button type="button" data-slide="2" aria-pressed="false" aria-label="Slide 3">3</button></div><p class="hint" role="status">Slide 1 of 3. No automatic advancement.</p></section>
Example source · content-slider.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}
.slides{display:flex;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none}.slides::-webkit-scrollbar{display:none}.slide{flex:0 0 88%;scroll-snap-align:start}.slide .art{min-height:210px}.slide strong{display:block;margin-top:10px}.slide-dots{display:flex;gap:10px;justify-content:center}.slide-dots button{width:36px;height:36px;padding:0;border-radius:50%;font-size:12px}
Example source · content-slider.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 viewport=root.querySelector('.slides'),slides=[...root.querySelectorAll('.slide')],dots=[...root.querySelectorAll('[data-slide]')];
dots.forEach((button,i)=>on(button,'click',()=>{if(active)viewport.scrollTo({left:slides[i].offsetLeft-slides[0].offsetLeft,behavior:reduced?'instant':'smooth'});}));
let frame=0;function reflect(){frame=0;let index=0,distance=Infinity;slides.forEach((slide,i)=>{const delta=Math.abs(slide.offsetLeft-slides[0].offsetLeft-viewport.scrollLeft);if(delta<distance){distance=delta;index=i;}});dots.forEach((button,i)=>button.setAttribute('aria-pressed',String(i===index)));root.querySelector('[role=status]').textContent=`Slide ${index+1} of ${slides.length}. No automatic advancement.`;}
on(viewport,'scroll',()=>{if(!frame)frame=requestAnimationFrame(reflect);},{passive:true});cleanup=()=>cancelAnimationFrame(frame);
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.
Continuous decorative gallery
Create a visual ribbon for mood or design direction while keeping real entry points stable elsewhere.
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-mood or inspiration Section using authorized, lightweight assets. Put important project links in a still list.
How does the interaction work?
Two equal-width image sets move by half the total track width. Hide the duplicate set from assistive technology and put no focusable controls inside it. Pause when hidden.
How do touch, keyboard and reduced motion work?
Reduced motion hides the duplicate set and permits direct browsing of the original. Avoid essential product decisions or legal information here.
What should be checked before launch?
Check equal set widths including gaps, no duplicate focus targets, a real pause, one resumed timeline and optimized real imagery.
Example source · loop-gallery.html
<section class="fx" data-effect="loop-gallery"><small>16 / A DECORATIVE LOOP</small><h2>KEEP EXPLORING.</h2><div class="loop-window" aria-label="Abstract illustration strip"><div class="loop-track"><div class="loop-set"><div class="art" data-art="1"></div><div class="art" data-art="2"></div><div class="art" data-art="3"></div></div><div class="loop-set" aria-hidden="true"><div class="art" data-art="1"></div><div class="art" data-art="2"></div><div class="art" data-art="3"></div></div></div></div><p class="hint">A decorative loop. Put real links in a separate, stable list.</p></section>
Example source · loop-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}
.loop-window{overflow:hidden;margin:0 -30px}.loop-track{display:flex;width:max-content}.loop-set{display:flex;gap:16px;padding-right:16px;flex:none}.loop-set .art{width:200px;min-height:180px}.loop-set .art:before{left:10%}@media(prefers-reduced-motion:reduce){.loop-window{overflow-x:auto}.loop-set[aria-hidden=true]{display:none}}
Example source · loop-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=()=>{};
start=()=>animate(root.querySelector('.loop-track'),[{transform:'translateX(0)'},{transform:'translateX(-50%)'}],{duration:22000,iterations:Infinity,easing:'linear'});
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