Shopify Typography & brand expression
Choose the message first, then its entrance, rotation or repetition. Motion supports reading.
Clipped title entrance
Introduce a brand statement one line at a time while keeping copy and actions stable.
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 or brand-story hero Section. Read the heading from text settings and scope it with section.id.
How does the interaction work?
Render the readable heading first. Hide decorative line copies from assistive technology, then animate transform and opacity in line order. Finish at the visible state.
How do touch, keyboard and reduced motion work?
Show the final state for reduced motion and when scripts fail. Break lines by meaning on smaller screens.
What should be checked before launch?
Check complete headings on narrow screens, one accessible name, frozen pause and no duplicate timelines after replay.
Example source · split-title.html
<section class="fx" data-effect="split-title"><small>01 / BRAND INTRO</small><h2 aria-label="Design with intent"><span aria-hidden="true" class="clip"><span>DESIGN</span></span><span aria-hidden="true" class="clip"><span>WITH INTENT.</span></span></h2><p>One message. A clear first impression.</p></section>
Example source · split-title.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}
.clip{display:block;overflow:hidden;padding-bottom:.07em}.clip>span{display:block}
Example source · split-title.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('.clip>span').forEach((line,i)=>animate(line,[{transform:'translateY(110%)',opacity:0},{transform:'translateY(0)',opacity:1}],{duration:850,delay:i*140,fill:'both',easing:'cubic-bezier(.2,.8,.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.
Rotating keywords
Rotate a few values within a stable sentence that makes sense at every step.
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 hero or value-proposition Section with a limited set of text blocks. Keep prices, inventory and delivery commitments stable.
How does the interaction work?
Clip the word stack to one line. A repeated first word closes the loop at 75% translation. Supply one accessible name containing the three values rather than repeated live announcements.
How do touch, keyboard and reduced motion work?
Reduced motion retains the first visible word and the complete accessible name. Do not hide essential meaning in a fleeting word.
What should be checked before launch?
Check the longest translation, readable pauses, timeline pause and exclusion of the repeated visual line from reading order.
Example source · word-rotator.html
<section class="fx" data-effect="word-rotator"><small>02 / ONE PROMISE</small><h2>Built for<br><span class="words" aria-label="clarity, growth and people"><span aria-hidden="true">clarity.<br>growth.<br>people.<br>clarity.</span></span></h2><p>Keep the sentence understandable without motion.</p></section>
Example source · word-rotator.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}
.words{display:block;height:1.12em;overflow:hidden;color:#d5f875}.words>span{display:block;line-height:1.12}.fx h2{font-size:clamp(40px,8vw,68px)}
Example source · word-rotator.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('.words>span'),[{transform:'translateY(0)',offset:0},{transform:'translateY(0)',offset:.23},{transform:'translateY(-25%)',offset:.32},{transform:'translateY(-25%)',offset:.55},{transform:'translateY(-50%)',offset:.64},{transform:'translateY(-50%)',offset:.87},{transform:'translateY(-75%)',offset:1}],{duration:7200,iterations:Infinity,easing:'ease-in-out'});
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.
Brand text marquee
Create rhythm with a repeating slogan while keeping essential explanations 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 decorative brand or collection Section. Render text from theme settings and keep important links in a stable area.
How does the interaction work?
Two equal-width copies form one track that moves by half its total width. Hide visual duplicates from assistive technology and use an explicit playback control.
How do touch, keyboard and reduced motion work?
Stop under reduced motion. Avoid long terms or essential moving links.
What should be checked before launch?
Check the loop seam, one accessible phrase, a real pause and no horizontal overflow on the host page.
Example source · text-marquee.html
<section class="fx" data-effect="text-marquee"><small>03 / BRAND RHYTHM</small><p>Design for the way people shop.</p><div class="marquee" aria-label="Design. Build. Improve."><div class="belt" aria-hidden="true"><span>DESIGN. BUILD. IMPROVE. </span><span>DESIGN. BUILD. IMPROVE. </span></div></div><p class="hint">Decorative repetition stays outside the reading order.</p></section>
Example source · text-marquee.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}
.marquee{overflow:hidden;margin:20px -30px}.belt{display:flex;width:max-content;color:#d5f875;font-weight:750;font-size:clamp(40px,7vw,62px);line-height:1.2}.belt span{flex:none}
Example source · text-marquee.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('.belt'),[{transform:'translateX(0)'},{transform:'translateX(-50%)'}],{duration:16000,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.
Letter decoding reveal
Resolve a short phrase from temporary letters for a one-time brand emphasis.
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 short brand heading or footer invitation. Limit its length and retain a stable accessible version.
How does the interaction work?
requestAnimationFrame advances elapsed time only while playing. Lock one character every 70ms, restore the exact source text at the end and cancel the pending frame when paused.
How do touch, keyboard and reduced motion work?
Show the source text for reduced motion or script failure. This Latin-letter example is not a Chinese text-shuffling algorithm.
What should be checked before launch?
Verify exact final text, no background frame after pause and a single loop after resume. Use a separate word reveal for Chinese.
Example source · text-shuffle.html
<section class="fx" data-effect="text-shuffle"><small>04 / QUIET REVEAL</small><h2 aria-label="MAKE IT CLEAR"><span aria-hidden="true" class="shuffle">MAKE IT CLEAR</span></h2><p>Keep one stable, accessible version of the heading.</p></section>
Example source · text-shuffle.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}
.shuffle{font-family:ui-monospace,monospace;letter-spacing:-.04em;color:#d5f875;overflow-wrap:anywhere}
Example source · text-shuffle.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 label=root.querySelector('.shuffle'),text=label.textContent,letters='ABCDEFGHJKLMNPRSTUVWXYZ';
let elapsed=0,previous=0,frame=0;
function tick(time){if(!active)return;elapsed+=previous?Math.min(time-previous,60):0;previous=time;const done=Math.floor(elapsed/70);label.textContent=[...text].map((c,i)=>c===' '||i<done?c:letters[Math.floor(Math.random()*letters.length)]).join('');if(done<text.length)frame=requestAnimationFrame(tick);else label.textContent=text;}
start=()=>{if(!reduced)frame=requestAnimationFrame(tick);};
stop=()=>{cancelAnimationFrame(frame);previous=0;};
// Resume the same timeline after a pause, rather than creating another loop.
on(root,'ww:resume',()=>{if(active&&!reduced&&elapsed<text.length*70){previous=0;cancelAnimationFrame(frame);frame=requestAnimationFrame(tick);}});
cleanup=()=>{cancelAnimationFrame(frame);label.textContent=text;};
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