Shopify 文字与品牌表达
先明确一句核心信息,再决定入场、轮换或重复,让动效服务阅读。
遮罩分行入场
让品牌主张分两行依次出现,正文和按钮保持稳定,避免整屏等待动画。
已暂停,点击播放后体验交互。
演示使用抽象图形和示例文案。滚动效果在局部预览区内体验,接入页面时应使用 Section 自身的滚动区间。
适合放在 Shopify 的哪个位置?
首页或品牌页的首屏 Section;标题来自 text / inline_richtext 设置,使用唯一的 section.id 包裹。
交互和代码如何运作?
HTML 先输出完整可读标题;装饰行使用 aria-hidden,父标题保留稳定名称。逐行执行 transform 与 opacity,延迟由行序决定,播放后停在终态。
触摸、键盘和减少动态效果如何处理?
减少动态效果时直接显示终态;脚本失败时仍可读。移动端按语义分行,避免长标题截断。
上线前要检查什么?
窄屏标题完整;读屏只读一遍;暂停停住时间轴;重新播放不会累积动画。
示例源码 · 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>
示例源码 · 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}
示例源码 · 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();}
};
}
案例效果与官方依据
查看 WESWOO 案例站中的参考效果这是根据同类交互重新编写的轻量实现,不复刻参考站主题及其整包源代码。
WESWOO 文章:对应文章经核对匹配后再补充。
关键词轮换
在固定句子中轮换少量关键词,表达不同价值点,句子本身始终成立。
已暂停,点击播放后体验交互。
演示使用抽象图形和示例文案。滚动效果在局部预览区内体验,接入页面时应使用 Section 自身的滚动区间。
适合放在 Shopify 的哪个位置?
品牌首屏或价值主张 Section;词组由有限 blocks 配置,不把价格、库存或履约承诺放进轮换。
交互和代码如何运作?
固定一个行高的视窗,四行内容中最后一行重复首词,以 0、25%、50%、75% 位移完成一轮。无障碍名称完整列出三个词,避免逐秒播报。
触摸、键盘和减少动态效果如何处理?
减少动态效果时保留首词和完整可访问名称;不要依赖某一闪过的词才能理解信息。
上线前要检查什么?
最长译文不裁切;各词停留足够阅读;暂停不跳回首词;重复行不进入读屏顺序。
示例源码 · 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>
示例源码 · 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)}
示例源码 · 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();}
};
}
品牌文字跑马灯
用连续文字建立节奏,适合重复口号或装饰信息,核心说明另外保留。
已暂停,点击播放后体验交互。
演示使用抽象图形和示例文案。滚动效果在局部预览区内体验,接入页面时应使用 Section 自身的滚动区间。
适合放在 Shopify 的哪个位置?
品牌页面或系列页的装饰 Section;文案用主题设置输出,关键链接放在稳定区域。
交互和代码如何运作?
两份等宽文字组成一个轨道,线性移动半个总宽度完成无缝循环;复制内容对读屏隐藏。预览由外部播放按钮统一控制。
触摸、键盘和减少动态效果如何处理?
减少动态效果时静止;不要放需要逐字阅读的长条款,也不让重要链接持续移动。
上线前要检查什么?
两段拼接没有明显空档;读屏没有重复;暂停确实停止;手机页面本身不产生横向溢出。
示例源码 · 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>
示例源码 · 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}
示例源码 · 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();}
};
}
案例效果与官方依据
查看 WESWOO 案例站中的参考效果这是根据同类交互重新编写的轻量实现,不复刻参考站主题及其整包源代码。
WESWOO 文章:对应文章经核对匹配后再补充。
字母解码显现
短句由随机字母逐步还原,适合一次性的品牌强调,不用于需要立即确认的数据。
已暂停,点击播放后体验交互。
演示使用抽象图形和示例文案。滚动效果在局部预览区内体验,接入页面时应使用 Section 自身的滚动区间。
适合放在 Shopify 的哪个位置?
品牌短标题或页尾邀请 Section;限制句子长度,保留静态原文供读屏读取。
交互和代码如何运作?
requestAnimationFrame 只在播放期间推进已用时间;每 70ms 锁定一个字符,剩余字符临时替换。结束后恢复精确原文,暂停取消帧并保留进度。
触摸、键盘和减少动态效果如何处理?
减少动态效果或没有脚本时显示原文。示例以拉丁字母为主;中文不应强行替换成随机拉丁字母。
上线前要检查什么?
结束文案逐字一致;暂停后无后台帧;恢复不叠加循环;中文方案应另做逐词显现。
示例源码 · 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>
示例源码 · 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}
示例源码 · 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();}
};
}
案例效果与官方依据
查看 WESWOO 案例站中的参考效果这是根据同类交互重新编写的轻量实现,不复刻参考站主题及其整包源代码。
WESWOO 文章:对应文章经核对匹配后再补充。
内容确定后,再选择特效
先在知识库确定页面逻辑,再在开发知识库连接真实数据。只在有助理解信息的位置增加动效,并在接入后检查性能。
接入 Shopify Section
预览由普通 HTML、CSS 和 JavaScript 组成。先在复制主题中逐个接入,再由 Section 设置提供真实文案和图片。
- 将所选 CSS 保存为 assets/ww-effects.css,JavaScript 保存为 assets/ww-selected-effect.js;与主题整合前,把 CSS 限定在该 Section 内。
- 用所选 .fx 元素替换下方壳层占位,将示例文字、抽象图形改为 Section 设置及有授权的 image_tag 输出,控件标签接入语言文件。
- 用生命周期脚本防止重复挂载,离屏暂停、shopify:section:unload 时销毁;把局部滚动演示改成实际 Section 的区间计算,在主题编辑器和真实设备验收。
接入壳层:先插入所选效果 HTML 才能使用,不是可直接安装的主题或应用。
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 %}
共用初始化:每个模块接入一种所选效果;多种效果需显式建立注册表和 Section 对应关系。
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();
这些示例为 WESWOO 编写的教学实现,可查看、复制并保留 WESWOO 及本页来源说明。参考主题、第三方库、商标和案例图片仍适用各自权利,未包含在演示下载中。
Shopify Dawn 主题整合参考(固定版本)。Dawn 源码适用其 Shopify 平台使用许可,上述示例未打包该仓库代码。
- Shopify/dawn · modal / slider base classes
- Shopify/dawn · product media gallery
- Shopify/dawn · license
依据核对 2026-11-12