webdevpuneet.com

360° Product Spin Viewer — Drag, Inertia & Views in JS

360° Product Spin Viewer · Media · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="card">
  <div class="head">
    <div>
      <span class="eyebrow">New · 250 ml</span>
      <h2>Nova Cold Brew</h2>
      <p class="sub">Oat · Vanilla · interactive 360° view</p>
    </div>
    <div class="buy">
      <span class="price">$3.49</span>
      <div class="swatches" id="swatches" role="radiogroup" aria-label="Colourway">
        <button type="button" class="sw" role="radio" aria-checked="true" data-pal="midnight" aria-label="Midnight" style="--a:#1e1b4b;--b:#4338ca"></button>
        <button type="button" class="sw" role="radio" aria-checked="false" data-pal="sunset" aria-label="Sunset" style="--a:#9f1239;--b:#f97316"></button>
        <button type="button" class="sw" role="radio" aria-checked="false" data-pal="mint" aria-label="Mint" style="--a:#064e3b;--b:#10b981"></button>
      </div>
    </div>
  </div>

  <div class="stage" id="stage" tabindex="0" aria-label="360 degree product viewer. Drag, or use the left and right arrow keys, to rotate.">
    <canvas id="view"></canvas>
    <div class="loader" id="loader"><div class="bar"><i id="barFill"></i></div><span id="loadTxt">Rendering frames… 0%</span></div>
    <span class="badge" id="deg" aria-hidden="true">0°</span>
    <div class="hint" id="hint" aria-hidden="true"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 8l-4 4 4 4"/><path d="M16 8l4 4-4 4"/><path d="M4 12h16"/></svg>Drag to spin</div>
    <div class="callout" id="callout" aria-live="polite"><b id="coTitle"></b><span id="coText"></span></div>
  </div>

  <div class="row">
    <button type="button" class="pill" id="autoBtn" aria-pressed="true">⟳ Auto-spin</button>
    <label class="scrub"><span class="sr">Rotation in degrees</span><input id="scrub" type="range" min="0" max="359" step="1" value="0"></label>
    <button type="button" class="pill" id="resetBtn">Reset</button>
  </div>

  <div class="chips" id="chips" role="group" aria-label="Jump to a view">
    <button type="button" data-a="0">Front</button>
    <button type="button" data-a="90">Side</button>
    <button type="button" data-a="180">Barcode</button>
    <button type="button" data-a="270">Ingredients</button>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Frame-sequence engine
A frames array, a draw() that fits any image size, and a single FRAME_URL setting to swap the demo for real turntable photos.
Code-rendered demo product
A can drawn column by column with asin foreshortening, so the label really wraps, with lighting, a turning pull-tab, reflection and shadow.
Drag with inertia
Pointer Events with pointer capture, smoothed release velocity and friction-based fling, with touch-action pan-y for page scrolling.
Auto-spin and scrubber
Idle rotation until the first interaction, a toggle button, and a range slider bound to the angle.
Views and callouts
Chips animate to named angles along the shortest path, and a callout appears while that face is toward the viewer.
Colourways with caching
Swatches regenerate the frame set with a progress bar, cache each set, and cancel stale renders with a token.
Keyboard and screen-reader support
Focusable slider semantics, arrow keys, Home, Space, aria-valuenow and a live callout.
Responsive and light/dark
Scales with its container, follows prefers-color-scheme, caps canvas resolution for memory, and respects reduced motion.

About this UI Snippet

360° Product Spin Viewer — Frame Sequences, Drag Inertia, Colourways and a Code-Rendered Demo Product

Screenshot of the 360° Product Spin Viewer snippet rendered live

Interactive 360° viewers let shoppers drag a product around and inspect it from every angle, which is why they appear on footwear, watch, cosmetics and furniture pages where shape, texture and material matter more than any single photo can show. This snippet is a complete, reusable viewer: pointer drag with momentum, auto-spin, a scrubber, keyboard control, colourway switching, "jump to view" shortcuts and a loading bar. The demo product is a can that is rendered in code, so it needs no image files, yet the viewer itself works on a frame sequence and takes real photos with a single line.

How real 360° viewers work

A production viewer is a frame sequence: the product sits on a motorised turntable and is photographed at fixed steps (24, 36 or 72 shots for a full turn). The page preloads the images, turns the horizontal drag distance into a frame index and swaps the visible frame. Done quickly, the eye reads it as continuous rotation. This viewer keeps exactly that structure: a frames array, a draw() that shows frames[index], and a FRAME_URL setting. Set FRAME_URL = i => 'shots/spin-' + (i + 1) + '.webp' and the demo is replaced by your photos, with the same progress bar, drag, inertia and views.

A demo that really rotates

Instead of sliding a gradient sideways, the demo draws the can the way a 3D engine would, column by column. For each 0.5 px column across the can the code computes the angle around the cylinder with Math.asin, samples the matching strip of a 1024×512 wrap-around label texture, and stretches it to the column's height. The arcsine gives the correct foreshortening, so text compresses toward the edges and genuinely wraps. A fixed gradient pass adds lighting and a highlight, the lid has a pull-tab that turns with the can, and a faded reflection plus a contact shadow ground it. Each of the 48 frames is rendered into its own canvas in 12 ms slices with setTimeout, so the page stays responsive and the progress bar fills while it works.

Drag, momentum and the wrap-around maths

The stage uses Pointer Events, so mouse, pen and touch share one code path, with setPointerCapture keeping the drag alive outside the element and touch-action: pan-y leaving vertical page scrolling intact. The drag distance is divided by PX_PER_FRAME to get degrees, and angles are normalised with ((a % 360) + 360) % 360 so spinning past either end loops. Release velocity is smoothed with an exponential average while dragging, then decays each frame by FRICTION raised to the elapsed time, giving a natural fling; if the finger paused for 80 ms before lifting there is no fling.

Auto-spin, views and the shortest route

The product turns slowly until the first interaction, and a button or Space toggles it. The four chips (Front, Side, Barcode, Ingredients) animate to a chosen angle with an ease-out curve along the shortest direction, using ((target − angle + 540) % 360) − 180. When the angle is within 34° of a view, a callout appears with a short note, a simple form of "hotspot" that is tied to a rotation range instead of fixed pixels.

Colourways without reloading

The three swatches rebuild the label texture and re-render all frames, and every finished set is cached, so switching back is instant. If the user changes colour mid-load, a token check abandons the stale render. This is the same pattern a real configurator uses to load a different image set per finish.

Accessibility, performance and reduced motion

The stage is focusable and behaves as a slider: arrow keys turn it a frame (Shift for four), Home resets and Space toggles auto-spin, and a real range input offers the same control. aria-valuenow and the callout update live. Canvases are scaled by devicePixelRatio capped at 1.5 to limit memory (48 frames of this size use roughly 45 MB), and prefers-reduced-motion disables auto-spin and inertia. For 3D models rather than photos, see the Babylon.js spinning product, and for a lighter hover effect the 3D card tilt.

Step by step

How to Use

  1. 1
    Drag to spinPress anywhere on the stage and drag left or right. The product follows your finger, and if you let go while moving it keeps turning and slows down with inertia.
  2. 2
    Let it auto-spin or scrubThe can turns slowly until your first touch. Use the Auto-spin button (or Space) to toggle it, and the slider under the stage to set an exact angle from 0° to 359°.
  3. 3
    Jump to a viewClick Front, Side, Barcode or Ingredients. The viewer animates the short way round and a callout explains what you are looking at. The chip lights up when you are within 34° of that view.
  4. 4
    Try a colourwayPick Midnight, Sunset or Mint. The 48 frames re-render with a progress bar, and each set is cached, so going back is instant.
  5. 5
    Use your own photosShoot 24 to 72 turntable images, set FRAMES to the count and FRAME_URL to a function like i => "shots/spin-" + String(i + 1).padStart(2, "0") + ".webp". The loader preloads them and the rest keeps working.
  6. 6
    Tune the feelPX_PER_FRAME sets drag sensitivity, FRICTION how long the fling lasts, AUTO_SPEED the idle rotation, and VIEWS the labelled angles and their notes.

Real-world uses

Common Use Cases

E-commerce product pages
Let shoppers inspect stitching, finish and shape before buying. Swap in 24 to 72 turntable photos and keep the same interaction. Pair it with the product card grid for the listing page.
Configurators
The colourway swatches show the pattern for finish pickers: one frame set per option, cached after the first load, with the angle preserved when you switch.
Packaging and label reviews
Jump-to-view chips are ideal for showing the barcode, ingredient panel or legal text of a package, which is why the demo wraps a label around a can.
Teaching drag, inertia and angle maths
Delta to angle, wrap-around with modulo, shortest-path easing and velocity decay are reusable in sliders, dials and carousels, as in the drag spin dial.
Prototype before the photo shoot
Validate drag feel, hint placement and view callouts with the rendered demo, then drop in real frames at launch by changing one setting.

Got questions?

Frequently Asked Questions

Most commercial viewers use 24 or 36 frames, which feels smooth at normal drag speed while keeping the download small (24 compressed WebP frames at about 40 KB each is under 1 MB). Jewellery and watches sometimes use 72. This viewer defaults to 48 and you can change FRAMES.

Set FRAME_URL to a function that returns the URL of frame i, for example i => "shots/spin-" + String(i + 1).padStart(2, "0") + ".webp", and set FRAMES to the number of images. The loader preloads every frame and shows progress before the first drag, and draw() fits images of any size into the canvas.

It gives true cylinder foreshortening with only 2D canvas calls. Each column uses asin(x / radius) as its angle around the can and samples the label at that position, so text bends and compresses toward the edges just like a photographed can. No WebGL or image files are needed.

While dragging, velocity in degrees per millisecond is smoothed with an exponential average. On release it is applied every frame and multiplied by FRICTION raised to the elapsed time over 16 ms, so it decays the same at 60 Hz and 144 Hz. If you stop moving before letting go, velocity is zeroed so it does not fling.

The stage has touch-action: pan-y, which lets the browser handle vertical swipes while giving horizontal movement to your pointer handlers.

The demo caches 48 canvases at 450 by 570 pixels, roughly 45 MB. Real images are usually lighter because the browser decodes compressed formats lazily. Reduce FRAMES or the canvas scale for low-end devices.

Yes. Keep the frames array and angle in refs, run the animation loop in a mount effect, and cancel requestAnimationFrame and the pointer listeners in the cleanup. Use the export buttons on this page for the markup and styles.