Shopify Pointers, buttons & menus
Start with touch and keyboard operation, then enhance pointer feedback and previews.
Magnetic call to action
Add a small pointer response around a normal button without replacing the cursor.
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 CTA Section. Replace the demo action with a real destination during integration.
How does the interaction work?
For mouse pointers only, map distance from the container center to a bounded translation. Reset on pointer leave, blur and pause; release listeners with AbortController.
How do touch, keyboard and reduced motion work?
Touch and keyboard activate the original target directly. Reduced motion disables displacement.
What should be checked before launch?
Check bounded movement, pointer and keyboard activation, reset and no global cursor or scrolling changes.
Example source · magnetic-cta.html
<section class="fx" data-effect="magnetic-cta"><small>05 / FOCUSED ACTION</small><h2>START SOMETHING.</h2><div class="magnet"><button type="button" class="magnet-button">Explore the idea ↗</button></div><p class="hint" role="status">Move over the button, or activate it by keyboard / touch.</p></section>
Example source · magnetic-cta.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}
.magnet{display:grid;place-items:center;min-height:130px}.magnet-button{background:#d5f875;color:#11251f;padding:20px 30px;transition:transform .18s ease-out}.magnet-button:hover{background:#e2ff9e}
Example source · magnetic-cta.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 button=root.querySelector('.magnet-button'),zone=root.querySelector('.magnet');
on(zone,'pointermove',event=>{if(!active||reduced||event.pointerType!=='mouse')return;const r=zone.getBoundingClientRect();button.style.transform=`translate(${Math.max(-18,Math.min(18,(event.clientX-r.left-r.width/2)*.12))}px,${Math.max(-12,Math.min(12,(event.clientY-r.top-r.height/2)*.12))}px)`;});
const reset=()=>button.style.transform='';on(zone,'pointerleave',reset);on(button,'blur',reset);on(button,'click',()=>{root.querySelector('[role=status]').textContent='Action selected — connect this button to a real destination.';});stop=reset;cleanup=reset;
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.
List hover preview
Identify an item in a list, then inspect its supporting image without covering the next action.
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 case list, collection guide or editorial Section. Read title, destination and image from the same block.
How does the interaction work?
Pointer enter, keyboard focus and click share one selection function. Update pressed state and alternative text. Use image_url / image_tag with reserved dimensions for real imagery.
How do touch, keyboard and reduced motion work?
Tap to choose on phones. Reduced motion swaps content without transition. Provide a separate explicit destination link.
What should be checked before launch?
Check consistent selection across inputs, matching alternative text, unobscured actions and a usable text-only fallback.
Example source · hover-preview.html
<section class="fx" data-effect="hover-preview"><small>06 / PREVIEW BEFORE OPENING</small><div class="preview-layout"><div class="preview-list" aria-label="Choose a concept"><button type="button" data-preview="1" aria-pressed="true">01 — Product story</button><button type="button" data-preview="2" aria-pressed="false">02 — Collection guide</button><button type="button" data-preview="3" aria-pressed="false">03 — Brand journal</button></div><div class="art preview-art" data-art="1" role="img" aria-label="Abstract product story illustration"></div></div><p class="hint">Hover, focus or tap to select a preview.</p></section>
Example source · hover-preview.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}
.preview-layout{display:grid;grid-template-columns:1fr 1fr;gap:24px;align-items:center}.preview-list{display:grid;gap:8px}.preview-list button{border-radius:0;text-align:left;border-width:0 0 1px;padding:15px 0}.preview-art{transition:opacity .25s}.preview-list button[aria-pressed=true]{background:transparent;color:#d5f875}@media(max-width:500px){.preview-layout{gap:15px;grid-template-columns:1fr}.preview-art{min-height:120px}.preview-list button{padding:8px 0}}
Example source · hover-preview.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 artwork=root.querySelector('.preview-art'),buttons=[...root.querySelectorAll('[data-preview]')];
function select(button){if(!active)return;buttons.forEach(item=>item.setAttribute('aria-pressed',String(item===button)));artwork.dataset.art=button.dataset.preview;artwork.setAttribute('aria-label','Abstract illustration: '+button.textContent.trim());}
buttons.forEach(button=>{on(button,'pointerenter',event=>{if(event.pointerType==='mouse')select(button);});on(button,'focus',()=>select(button));on(button,'click',()=>select(button));});
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.
Rolling button label
Roll the same label on hover or focus to reinforce feedback without changing its meaning.
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?
Enhance an existing CTA label. Retain the current theme link or button semantics and cart behavior.
How does the interaction work?
Keep one accessible name and clip two hidden visual copies to one line. Translate both copies for hover and focus-visible.
How do touch, keyboard and reduced motion work?
Touch activates directly and reduced motion removes the transition. Do not put different actions on the two faces.
What should be checked before launch?
Check visible focus, consistent meaning, longer translations and no duplicate accessible labels.
Example source · rolling-label.html
<section class="fx" data-effect="rolling-label"><small>07 / MICRO INTERACTION</small><h2>A SMALL INVITATION.</h2><button type="button" class="rolling" aria-label="Explore the collection"><span aria-hidden="true"><span>Explore the collection ↗</span><span>Explore the collection ↗</span></span></button><p class="hint" role="status">Hover, focus or tap. The destination label stays the same.</p></section>
Example source · rolling-label.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}
.rolling{justify-self:start;border-color:#cce5ac;overflow:hidden}.rolling>span{display:block;height:1.5em;overflow:hidden}.rolling>span>span{display:block;transition:transform .3s}.fx[data-playing=true] .rolling:hover>span>span,.fx[data-playing=true] .rolling:focus-visible>span>span{transform:translateY(-100%)}
Example source · rolling-label.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=()=>{};
on(root.querySelector('.rolling'),'click',()=>{if(active)root.querySelector('[role=status]').textContent='Collection action selected. Add the actual collection link during integration.';});
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