Project portfolio Browse selected work

Shopify Plus Upgrade Monthly Fee Reduction + Up to $4800 Development Fee Credit - Exclusive WesWoo Offer

Shopify Effects & Custom Modules / Galleries & detail viewing

Shopify Galleries & detail viewing

Show the whole first, then let people inspect details or change slides. Reserve loops for decoration.

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
HTML · WESWOO
<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
CSS · WESWOO
*{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
JavaScript · WESWOO
// 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 site
.zoom-gallery / .image-link

A newly written, lightweight implementation of the same interaction family. It does not reproduce the reference theme or its bundled source code.

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.

  1. 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.
  2. 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.
  3. 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
WESWOO
{% 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
WESWOO
// 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.

References checked 2026-11-12