
Image Zoom 360
- 62 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Add a rich product media experience with high-res hover zoom, pinch-to-zoom, 360-degree spin views, and inline video.
About
Implements zoom, mobile pinch-zoom, 360 spin sequences, and inline video on the product detail page. A developer uses it to reduce returns or upgrade a static image to a full media gallery for detail-sensitive categories.
- Per-platform approach table
- Hover/pinch zoom, 360 spin from image sequences, and inline PDP video
Image Zoom 360 by the numbers
- 62 all-time installs (skills.sh)
- Ranked #1,194 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill image-zoom-360Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 62 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Add a rich product media experience with high-res hover zoom, pinch-to-zoom, 360-degree spin views, and inline video.
Files
Image Zoom & 360-Degree Views
Overview
Implement a rich product media experience that includes high-resolution zoom on hover, touch-native pinch-to-zoom on mobile, 360-degree spin views from a sequence of images, and inline video playback. Richer media is associated with lower return rates and higher conversion for fashion, jewelry, electronics, and other detail-sensitive categories.
When to Use This Skill
- When product return rates are high and additional visual detail could reduce them
- When implementing a product detail page for fashion, jewelry, electronics, or other detail-sensitive categories
- When upgrading from a static single image to a full media gallery
- When integrating with a product photography workflow that includes 360-degree spin assets
- When product videos exist and need to be surfaced inline on the PDP
Core Instructions
Step 1: Determine the merchant's platform and choose the right approach
| Platform | Recommended Approach | Why |
|---|---|---|
| Shopify | Use built-in media support in OS2.0 themes (Dawn, Sense) + Magic Zoom Plus app or Swiper Gallery for 360 | Dawn natively supports product images, video, and 3D models in the media gallery; Magic Zoom Plus ($69 one-time) adds hover zoom and 360 spin without theme edits |
| WooCommerce | Install WooCommerce Product Gallery Slider or YITH WooCommerce Zoom Magnifier (free/premium) | WooCommerce includes a basic gallery; YITH Zoom Magnifier adds hover zoom and lightbox; Product Gallery Slider adds swipe, thumbnails, and video support |
| BigCommerce | Use the Cornerstone theme's built-in zoom (hover zoom is enabled by default) + install Magic 360 or Sirv app for spin views | Cornerstone has native zoom; Sirv ($20+/mo) provides hosted 360 spin and zoom CDN with a Stencil widget |
| Custom / Headless | Build a custom gallery component with CSS transform zoom, Pointer Events API for pinch-to-zoom, and frame-based spin viewer | Full control over performance and UX; see implementation below |
Step 2: Set up the product gallery
---
Shopify
Built-in media gallery (no app required):
1. In your product admin (Products → [product] → Media), upload images, add YouTube/Vimeo video URLs, and upload .glb files for 3D models 2. Shopify OS2.0 themes (Dawn, Sense, Craft) display all media types in the gallery automatically — images, video, and 3D 3. In Online Store → Themes → Customize, navigate to your product page template and find the Product media section:
- Enable Image zoom (magnifier on hover)
- Set Media size to Large or Extra Large for better zoom quality
- Enable Video looping for product videos
4. For variant-specific images: assign images to variants in Products → [product] → Variants → [variant] → Image — the gallery automatically switches to the variant image when a shopper selects that variant
For 360-degree spin views — Magic Zoom Plus ($69 one-time): 1. Install from the Shopify App Store 2. Upload your spin sequence images as numbered files (e.g., product-001.jpg through product-036.jpg) 3. The app creates a spin viewer widget that embeds in your product page without theme code changes
---
WooCommerce
Built-in zoom (no plugin required):
WooCommerce includes basic image zoom (powered by Zoom.js) by default on product pages. To configure it: 1. Go to WooCommerce → Settings → Products → Display 2. Under Product Images, set Zoom behavior: Enabled, Disabled, or Inner 3. Upload high-resolution images (at least 1000×1000px) — the zoom uses the full uploaded image
YITH WooCommerce Zoom Magnifier (free + premium): 1. Install from WordPress.org 2. Go to YITH → Zoom Magnifier → Settings 3. Configure zoom type: Inner (magnifies within the image container), Outer (shows magnified panel beside the image), or Lens (circular magnifier follows cursor) 4. Enable lightbox for full-screen zoom on click
Product Gallery Slider (free — WooCommerce.com): 1. Install and activate 2. Adds swipe support, thumbnail strip, and fullscreen lightbox to the built-in WooCommerce gallery 3. Supports video thumbnails (YouTube/Vimeo) in the gallery
For 360 views: Use WooCommerce 360° Image plugin (free, wordpress.org) — upload numbered spin frames as product images prefixed with 360_ and the plugin creates a drag-to-spin viewer.
---
BigCommerce
Built-in zoom (Cornerstone theme): 1. Go to Storefront → My Themes → Customize 2. Under Product Page → Image, enable Image zoom (hover zoom is on by default) 3. Set Image size to Large for better zoom quality — BigCommerce serves images at multiple sizes automatically
Sirv (hosted 360 spin + zoom CDN): 1. Install Sirv from the BigCommerce App Marketplace 2. Upload your spin sequence images to Sirv's CDN — it auto-detects numbered sequences 3. Paste the Sirv embed code into your product description or use the Sirv BigCommerce widget 4. Sirv also serves all your product images via its CDN with automatic WebP conversion
Product videos: 1. In your BigCommerce product admin, click Add Video in the media section and paste a YouTube or Vimeo URL 2. Videos appear as thumbnail items in the product gallery automatically
---
Custom / Headless
CSS hover zoom (no JavaScript image loading):
// ZoomImage.jsx
import { useState, useRef } from 'react';
export function ZoomImage({ src, alt }) {
const [zoom, setZoom] = useState(null);
const containerRef = useRef(null);
function handleMouseMove(e) {
const rect = containerRef.current.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
setZoom({ x, y });
}
return (
<div
ref={containerRef}
className="zoom-container"
onMouseMove={handleMouseMove}
onMouseLeave={() => setZoom(null)}
>
<img
src={src}
alt={alt}
className="zoom-image"
style={zoom ? {
transformOrigin: `${zoom.x}% ${zoom.y}%`,
transform: 'scale(2.5)',
} : {}}
draggable={false}
/>
</div>
);
}.zoom-container { overflow: hidden; cursor: crosshair; aspect-ratio: 1/1; }
.zoom-image { width: 100%; height: 100%; object-fit: cover; transition: transform 0.05s linear; will-change: transform; }360-degree spin viewer:
// SpinViewer.jsx — preloads all frames, switches on drag
export function SpinViewer({ frames, productName = 'Product' }) {
const [frameIndex, setFrameIndex] = useState(0);
const [loaded, setLoaded] = useState(false);
const dragStart = useRef(null);
useEffect(() => {
let count = 0;
frames.forEach(src => {
const img = new Image();
img.src = src;
img.onload = () => { if (++count === frames.length) setLoaded(true); };
});
}, [frames]);
function handleDragStart(e) { dragStart.current = e.clientX ?? e.touches?.[0]?.clientX; }
function handleDragMove(e) {
if (dragStart.current === null) return;
const clientX = e.clientX ?? e.touches?.[0]?.clientX;
const delta = Math.round((clientX - dragStart.current) / 8);
if (delta !== 0) {
setFrameIndex(i => ((i + delta) % frames.length + frames.length) % frames.length);
dragStart.current = clientX;
}
}
function handleDragEnd() { dragStart.current = null; }
if (!loaded) return <div aria-label="Loading 360 view">Loading...</div>;
return (
<div
onMouseDown={handleDragStart} onMouseMove={handleDragMove}
onMouseUp={handleDragEnd} onMouseLeave={handleDragEnd}
onTouchStart={handleDragStart} onTouchMove={handleDragMove} onTouchEnd={handleDragEnd}
aria-label="360 degree product view — drag to rotate"
role="img" style={{ cursor: 'ew-resize' }}
>
<img src={frames[frameIndex]} alt={`${productName}, frame ${frameIndex + 1} of ${frames.length}`} draggable={false} />
<span aria-hidden="true" style={{ fontSize: '0.75rem', color: '#666' }}>Drag to rotate</span>
</div>
);
}Step 3: Optimize media for performance
Regardless of platform, follow these media guidelines:
1. Hero image: Upload at minimum 2000px on the longest side for zoom quality; platforms resize down automatically for thumbnails 2. Format: Upload WebP images where your platform supports it — Shopify, BigCommerce, and Sirv convert automatically; for WooCommerce use the Imagify or ShortPixel plugin 3. 360 spin frames: 24-36 frames is the sweet spot; at 30 KB per frame (optimized JPEG) that is under 1 MB total for the full sequence 4. Video: Upload MP4 directly to Shopify/BigCommerce, or embed YouTube/Vimeo to offload hosting costs; always include a poster image 5. LCP (Largest Contentful Paint): The main product image is almost always the LCP element — ensure it loads with high priority
Best Practices
- Serve images at 2x the rendered size for retina screens but no larger — a 400px container needs an 800px image, not 2000px
- Use WebP or AVIF format — 30-50% smaller than JPEG at equivalent quality
- Pre-load the hero image — add
fetchpriority="high"andloading="eager"to the first product image - Lazy-load non-hero media — thumbnails, 360 frames 2–N, and video should load after first interaction
- Show a loading state for 360 views — preloading 36 frames can take several seconds on mobile; show a progress indicator
- Provide keyboard alternatives for drag interactions — 360 spin should support left/right arrow keys in addition to mouse drag
- Avoid autoplay with sound — muted autoplay is acceptable; audio autoplay is blocked by most browsers
Common Pitfalls
| Problem | Solution |
|---|---|
| Zoom CSS transform causes layout shift | Use will-change: transform and overflow: hidden on the container so the scaled image does not reflow siblings |
| 360 spin is jittery on mobile | Throttle frame updates to one per requestAnimationFrame; do not call setState on every touchmove event |
| Video does not autoplay on iOS | Add playsInline and muted attributes; iOS requires both for autoplay without user gesture |
| LCP score poor due to large hero image | Add fetchpriority="high" and loading="eager" to the main product image; verify with Lighthouse |
| 360 assets too large (36 × 500 KB) | Target 20–40 KB per frame at 800px wide using JPEG quality 75 through your CDN |
Related Skills
- @product-page-design
- @responsive-storefront
- @accessibility-commerce
- @image-optimization-cdn
{
"context": "Tests whether the agent correctly implements the 360-degree spin viewer with requestIdleCallback-based lazy frame loading, requestAnimationFrame throttling for mobile, correct drag sensitivity, keyboard support, and appropriate ARIA attributes.",
"type": "weighted_checklist",
"checklist": [
{
"name": "requestIdleCallback preload",
"max_score": 12,
"description": "Remaining frames (frames after the first) are preloaded using requestIdleCallback rather than being preloaded eagerly all at once or via setTimeout"
},
{
"name": "requestIdleCallback timeout",
"max_score": 8,
"description": "The requestIdleCallback call includes a { timeout: 5000 } option"
},
{
"name": "requestAnimationFrame throttling",
"max_score": 12,
"description": "Frame index updates on touch/pointer move are throttled using requestAnimationFrame (rAF) rather than updating directly on every event"
},
{
"name": "8px per frame sensitivity",
"max_score": 8,
"description": "The drag-to-frame mapping uses approximately 8 pixels of movement per frame (e.g., Math.round(delta / 8) or equivalent)"
},
{
"name": "Loading state",
"max_score": 6,
"description": "A loading state is displayed while frames are being preloaded, before the viewer becomes interactive"
},
{
"name": "Keyboard left/right arrows",
"max_score": 12,
"description": "The spin viewer responds to left and right arrow key presses to navigate between frames"
},
{
"name": "role img aria-label",
"max_score": 8,
"description": "The spin viewer container has role='img' and an aria-label that references rotating/dragging the product (e.g., '360 degree product view' or similar)"
},
{
"name": "ew-resize cursor",
"max_score": 6,
"description": "The spin viewer container uses cursor: ew-resize (or cursor: 'ew-resize' inline style)"
},
{
"name": "Drag hint text",
"max_score": 6,
"description": "A visible 'Drag to rotate' (or equivalent) hint text element is rendered with aria-hidden='true'"
},
{
"name": "Notes: requestIdleCallback reason",
"max_score": 8,
"description": "IMPLEMENTATION_NOTES.md mentions requestIdleCallback (or idle callback) as the mechanism for background frame preloading"
},
{
"name": "Notes: rAF throttling reason",
"max_score": 8,
"description": "IMPLEMENTATION_NOTES.md mentions requestAnimationFrame (or rAF) as the mobile jitter prevention technique"
},
{
"name": "Frame wrap-around",
"max_score": 6,
"description": "Frame index wraps correctly at both ends (modulo arithmetic so frame after last returns to first, and before first goes to last)"
}
]
}
360-Degree Product Spin Viewer
Problem/Feature Description
An electronics retailer sells premium headphones and wants to give shoppers a way to inspect the product from every angle without physically holding it. Their product photography team has produced a set of 36 still images taken at 10-degree intervals around the product. The goal is to let visitors drag across the product image to spin it, giving a feel for the physical object.
The product team has flagged two specific concerns from a previous attempt at a similar feature on a sister site: the spin felt jerky on phones, and the page loaded slowly because all images were downloaded up front blocking the main hero from appearing quickly. They want the new implementation to address both issues. Additionally, the accessibility team requires that the spinner be operable by keyboard for users who cannot use a pointing device.
Output Specification
Implement a SpinViewer React component in SpinViewer.jsx. The component should accept a frames prop (array of image URLs representing the 36 frames) and an optional autoSpin boolean prop.
Write a spin-viewer.test.js (using plain JavaScript, no test framework required) that documents the expected behavior as comments and verifies at minimum:
- That dragging right advances frames
- That the frame wraps around (e.g., from the last frame back to the first)
Also write a brief IMPLEMENTATION_NOTES.md that explains:
- How frame preloading is handled and why
- How mobile jitter is prevented
- What keyboard interaction is supported
{
"context": "Tests whether the agent implements the CSS-transform-based zoom approach correctly, including the specific scale factor, transform-origin tracking, required CSS properties, and layout shift prevention techniques.",
"type": "weighted_checklist",
"checklist": [
{
"name": "CSS transform approach",
"max_score": 12,
"description": "Zoom is achieved via CSS transform: scale on the image element — no additional image URLs are fetched or swapped during hover"
},
{
"name": "Scale factor 2.5",
"max_score": 8,
"description": "The zoom scale value is 2.5 (i.e., transform: scale(2.5) or equivalent scale value of 2.5)"
},
{
"name": "Cursor-relative transform origin",
"max_score": 10,
"description": "transformOrigin is set dynamically to the cursor's position as percentages relative to the container (e.g., '${x}% ${y}%') during mouse move"
},
{
"name": "overflow hidden on container",
"max_score": 8,
"description": "The container element has overflow: hidden in its CSS to prevent the scaled image from overflowing"
},
{
"name": "will-change transform on image",
"max_score": 8,
"description": "The image element has will-change: transform in its CSS"
},
{
"name": "cursor crosshair",
"max_score": 6,
"description": "The container element has cursor: crosshair in its CSS"
},
{
"name": "aspect-ratio 1/1",
"max_score": 6,
"description": "The container element uses aspect-ratio: 1/1 in its CSS"
},
{
"name": "transition timing",
"max_score": 8,
"description": "The image element has a CSS transition on transform of 0.05s linear (or transition: transform 0.05s linear)"
},
{
"name": "object-fit cover",
"max_score": 6,
"description": "The image element has object-fit: cover so it fills the container dimensions"
},
{
"name": "Zoom reset on mouse leave",
"max_score": 8,
"description": "The zoom state is cleared (transform removed) when the mouse leaves the container (onMouseLeave handler)"
},
{
"name": "Accessible aria-label",
"max_score": 8,
"description": "The container element includes an aria-label that references both the image alt text and the zoom affordance (e.g., contains 'hover to zoom' or similar phrasing)"
},
{
"name": "README zoom mechanism",
"max_score": 12,
"description": "README.md documents that zoom uses CSS transform/scale rather than loading a separate image, and mentions at least one of: transformOrigin, will-change, or overflow:hidden"
}
]
}
Product Image Zoom Feature
Problem/Feature Description
A fashion retailer is launching a new product detail page and wants to differentiate the shopping experience from competitors. Their current setup shows a single static product image, and customers frequently cite "couldn't see the details well enough" in post-return surveys. The UX team has decided to add an interactive zoom experience to the main product image that lets shoppers on desktop inspect fabric texture, stitching, and color gradients.
The team wants the zoom to feel fluid and immediate — the full-resolution image should already be loaded and the zoom effect should follow the mouse position without any perceptible delay or network requests triggered during hover. The interaction should also not cause any layout shift that would move surrounding price/add-to-cart elements when the zoom activates.
Output Specification
Implement a self-contained ZoomImage React component in a file called ZoomImage.jsx and its accompanying styles in ZoomImage.css. The component should accept src (the image URL) and alt (the accessible description) as props.
Also produce a short README.md that documents:
- The props accepted by the component
- How the zoom effect is implemented at a high level (what CSS properties are used and why)
- Any known limitations
Input Files
The following files are provided as starting points. Extract them before beginning.
=============== FILE: src/ZoomImage.jsx =============== // TODO: implement ZoomImage component export function ZoomImage({ src, alt }) { return <img src={src} alt={alt} />; }
=============== FILE: src/ZoomImage.css =============== / TODO: add styles /
{
"context": "Tests whether the agent correctly implements inline video with iOS-compatible autoplay attributes, dual video sources in the right order, CDN image URL construction with proper parameters, retina-correct image sizing, hero image prioritization attributes, and modern image format guidance.",
"type": "weighted_checklist",
"checklist": [
{
"name": "playsInline attribute",
"max_score": 8,
"description": "The video element includes the playsInline attribute (or playsInline={true} in JSX)"
},
{
"name": "muted attribute",
"max_score": 8,
"description": "The video element includes the muted attribute (or muted={true} in JSX)"
},
{
"name": "preload metadata",
"max_score": 8,
"description": "The video element has preload='metadata' (not 'auto' or 'none')"
},
{
"name": "WebM source first",
"max_score": 8,
"description": "The video element includes both a WebM source and an MP4 source, with the WebM <source> listed before the MP4 <source>"
},
{
"name": "WebM URL derivation",
"max_score": 6,
"description": "The WebM URL is derived from the MP4 src by replacing '.mp4' with '.webm' (i.e., src.replace('.mp4', '.webm') or equivalent)"
},
{
"name": "Video fallback text",
"max_score": 6,
"description": "The video element contains a text fallback (inside <video> tags) that includes a download link to the MP4 src"
},
{
"name": "CDN URL quality and format auto",
"max_score": 8,
"description": "buildImageUrl constructs a URL containing both q_auto (or quality=auto) and f_auto (or format=auto) parameters"
},
{
"name": "CDN c_fill crop mode",
"max_score": 6,
"description": "buildImageUrl constructs a URL containing c_fill as the crop/fit mode"
},
{
"name": "fetchpriority high on hero",
"max_score": 10,
"description": "HeroImage.jsx includes fetchpriority='high' (or fetchPriority='high' in JSX) on the img element"
},
{
"name": "loading eager on hero",
"max_score": 8,
"description": "HeroImage.jsx includes loading='eager' on the img element"
},
{
"name": "2x retina sizing guidance",
"max_score": 10,
"description": "MEDIA_GUIDE.md states that images should be served at 2x the rendered/CSS size for retina screens (e.g., a 400px container needs an 800px image), and explicitly warns against requesting unnecessarily larger sizes"
},
{
"name": "WebP/AVIF format guidance",
"max_score": 8,
"description": "MEDIA_GUIDE.md recommends WebP or AVIF as the image format and mentions providing a JPEG fallback (via <picture> element or CDN content negotiation)"
},
{
"name": "iOS autoplay guidance",
"max_score": 6,
"description": "MEDIA_GUIDE.md explains that both playsInline and muted are required for autoplay on iOS Safari"
}
]
}
Product Page Media Overhaul
Problem/Feature Description
An outdoor gear brand is revamping their product detail pages after Lighthouse audits revealed a poor LCP (Largest Contentful Paint) score and slow mobile load times. Their pages currently display a static JPEG hero image and a separate embedded video player (an iframe from a third-party host). The new requirements are: serve the hero image in a modern format for better compression, replace the iframe video with an inline HTML5 player, and ensure the hero image is prioritized by the browser for fast paint.
The engineering team has a Cloudinary account and wants a reusable utility for constructing optimized image URLs. They also want the video player component to work reliably on all mobile browsers, including iOS Safari, where autoplay can be tricky. The solution should follow performance best practices so that hero images load at the right resolution for high-DPI screens without being excessively large.
Output Specification
Produce the following files:
1. src/ProductVideo.jsx — A React component that renders an inline HTML5 video player. It should accept src (an MP4 URL) and poster (a thumbnail image URL) as props.
2. src/lib/imageUrl.js — A utility function buildImageUrl(publicId, options) that constructs Cloudinary-compatible image URLs supporting width, height, quality, and format parameters.
3. src/HeroImage.jsx — A React component that renders the primary product hero image with correct loading priority attributes. Accepts src, alt, width, and height as props.
4. MEDIA_GUIDE.md — A short guide covering:
- Why the video attributes chosen are necessary for cross-browser/iOS support
- What image format should be used and how fallback is handled
- How to size images correctly for retina displays
{
"name": "finsi/image-zoom-360",
"version": "0.1.0",
"summary": "Product image zoom, 360-degree views, and video integration",
"skills": {
"image-zoom-360": {
"path": "SKILL.md"
}
}
}