Product Gallery — <mv-product-gallery>

Product page gallery: main image on native scroll-snap (swipe on touch), synced thumbnails in a column or a row (optional swap on hover), hover zoom that follows the pointer, arrows and counter, a segmented bar in small containers, and fullscreen in a <dialog> with click-to-zoom and panning.

CategoryMedia
TypeWeb Component (<mv-product-gallery>)
Statusstable
Keywordsgallery, galerie, product, pdp, thumbnails, zoom, lightbox, fullscreen, swipe, e-commerce

When to use

Avoid when

Install

node scripts/add.mjs product-gallery --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["product-gallery"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, core/motion.js, components/product-gallery/product-gallery.js, components/product-gallery/product-gallery.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="pg-demo" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,22rem),1fr));gap:2.5rem;width:min(100%,62rem);margin-inline:auto;align-items:start">
  <div style="display:grid;gap:.75rem">
    <mv-product-gallery id="pg-demo-main" label="Merino crewneck sweater" thumbs="start" zoom="2.2">
      <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.97 0.012 80)'/%3E%3Cstop offset='1' stop-color='oklch(0.91 0.025 75)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cdefs%3E%3Cpattern id='k' width='12' height='10' patternUnits='userSpaceOnUse'%3E%3Cpath d='M0 0l6 8 6-8' fill='none' stroke='black' stroke-opacity='.09' stroke-width='1.6'/%3E%3C/pattern%3E%3C/defs%3E%3Cellipse cx='200' cy='420' rx='150' ry='18' fill='url(%23fl)' opacity='1'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='oklch(0.8 0.05 75)'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='url(%23k)'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='url(%23sh)'/%3E%3Cpath d='M160 118 Q200 152 240 118 Q200 136 160 118Z' fill='black' fill-opacity='.22'/%3E%3Cpath d='M154 116 Q200 160 246 116' fill='none' stroke='black' stroke-opacity='.12' stroke-width='5'/%3E%3Cpath d='M133 370 Q200 379 267 370 L267 388 Q200 397 133 388 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M64 318 L100 331 L103 316 L68 303 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M336 318 L300 331 L297 316 L332 303 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M132 222 L133 380 M268 222 L267 380' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3C/svg%3E" alt="Merino crewneck sweater in sand, front view">
      <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.97 0.012 80)'/%3E%3Cstop offset='1' stop-color='oklch(0.91 0.025 75)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cdefs%3E%3Cpattern id='k' width='12' height='10' patternUnits='userSpaceOnUse'%3E%3Cpath d='M0 0l6 8 6-8' fill='none' stroke='black' stroke-opacity='.09' stroke-width='1.6'/%3E%3C/pattern%3E%3C/defs%3E%3Crect x='0' y='0' width='400' height='500' fill='white' fill-opacity='.18'/%3E%3Cpath d='M200 58 q0-16 12-16 q12 0 12 12 q0 10-12 14 L200 78' fill='none' stroke='oklch(0.55 0.02 60)' stroke-width='5' stroke-linecap='round'/%3E%3Cpath d='M200 78 L96 124 L304 124 Z' fill='none' stroke='oklch(0.6 0.08 60)' stroke-width='8' stroke-linejoin='round'/%3E%3Cg transform='translate(40 30) scale(.8)'%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='oklch(0.8 0.05 75)'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='url(%23k)'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='url(%23sh)'/%3E%3Cpath d='M160 118 Q200 152 240 118 Q200 136 160 118Z' fill='black' fill-opacity='.2'/%3E%3Cpath d='M133 372 Q200 380 267 372 L267 388 Q200 397 133 388 Z' fill='black' fill-opacity='.1'/%3E%3C/g%3E%3Cpath d='M300 470 c-4-60 6-110 30-150 c-2 50-10 100-12 150z' fill='oklch(0.55 0.08 150)' opacity='.55'/%3E%3Cpath d='M318 470 c8-52 30-86 58-104 c-14 38-30 72-40 104z' fill='oklch(0.5 0.08 150)' opacity='.5'/%3E%3Crect x='292' y='452' width='64' height='48' rx='6' fill='oklch(0.72 0.06 55)' opacity='.9'/%3E%3C/svg%3E" alt="The sweater on a wooden hanger">
      <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.97 0.012 80)'/%3E%3Cstop offset='1' stop-color='oklch(0.91 0.025 75)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cdefs%3E%3Cpattern id='k' width='44' height='36' patternUnits='userSpaceOnUse'%3E%3Cpath d='M2 2l20 30 20-30' fill='none' stroke='black' stroke-opacity='.14' stroke-width='7' stroke-linecap='round'/%3E%3Cpath d='M2 2l20 30 20-30' fill='none' stroke='white' stroke-opacity='.12' stroke-width='2' transform='translate(0 -3)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='400' height='500' fill='oklch(0.8 0.05 75)'/%3E%3Crect width='400' height='500' fill='url(%23k)'/%3E%3Crect width='400' height='500' fill='url(%23sh)'/%3E%3Cg transform='rotate(-6 250 330)'%3E%3Crect x='190' y='300' width='150' height='74' rx='6' fill='oklch(0.97 0.01 90)'/%3E%3Crect x='190' y='300' width='150' height='74' rx='6' fill='none' stroke='black' stroke-opacity='.12'/%3E%3Ctext x='265' y='333' text-anchor='middle' font-family='Georgia,serif' font-size='20' letter-spacing='3' fill='oklch(0.3 0.02 60)'%3EMERINO%3C/text%3E%3Ctext x='265' y='356' text-anchor='middle' font-family='Arial,sans-serif' font-size='9' letter-spacing='1.2' fill='oklch(0.45 0.02 60)'%3E100 % LAINE %C2%B7 18,5 %C2%B5m%3C/text%3E%3C/g%3E%3C/svg%3E" alt="Close-up of the knit and the merino label">
      <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.96 0.015 145)'/%3E%3Cstop offset='1' stop-color='oklch(0.89 0.03 150)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cdefs%3E%3Cpattern id='k' width='12' height='10' patternUnits='userSpaceOnUse'%3E%3Cpath d='M0 0l6 8 6-8' fill='none' stroke='black' stroke-opacity='.09' stroke-width='1.6'/%3E%3C/pattern%3E%3C/defs%3E%3Cellipse cx='200' cy='392' rx='150' ry='16' fill='url(%23fl)' opacity='1'/%3E%3Crect x='96' y='292' width='208' height='96' rx='16' fill='oklch(0.7 0.06 150)' style='filter:brightness(.9)'/%3E%3Crect x='96' y='292' width='208' height='96' rx='16' fill='url(%23k)'/%3E%3Crect x='96' y='292' width='208' height='96' rx='16' fill='url(%23sh)'/%3E%3Cpath d='M104 356h192' stroke='black' stroke-opacity='.12' stroke-width='3'/%3E%3Crect x='110' y='190' width='180' height='112' rx='16' fill='oklch(0.7 0.06 150)'/%3E%3Crect x='110' y='190' width='180' height='112' rx='16' fill='url(%23k)'/%3E%3Crect x='110' y='190' width='180' height='112' rx='16' fill='url(%23sh)'/%3E%3Cpath d='M160 190 Q200 232 240 190' fill='black' fill-opacity='.14'/%3E%3Cpath d='M166 190 Q200 222 234 190' fill='none' stroke='black' stroke-opacity='.18' stroke-width='5'/%3E%3Cpath d='M118 268h164' stroke='black' stroke-opacity='.1' stroke-width='3'/%3E%3C/svg%3E" alt="Two folded sweaters in sage">
      <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.95 0.012 250)'/%3E%3Cstop offset='1' stop-color='oklch(0.87 0.025 255)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cdefs%3E%3Cpattern id='k' width='12' height='10' patternUnits='userSpaceOnUse'%3E%3Cpath d='M0 0l6 8 6-8' fill='none' stroke='black' stroke-opacity='.09' stroke-width='1.6'/%3E%3C/pattern%3E%3C/defs%3E%3Cellipse cx='200' cy='420' rx='150' ry='18' fill='url(%23fl)' opacity='1'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='oklch(0.34 0.05 262)'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='url(%23k)'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='url(%23sh)'/%3E%3Cpath d='M160 118 Q200 152 240 118 Q200 136 160 118Z' fill='black' fill-opacity='.22'/%3E%3Cpath d='M154 116 Q200 160 246 116' fill='none' stroke='black' stroke-opacity='.12' stroke-width='5'/%3E%3Cpath d='M133 370 Q200 379 267 370 L267 388 Q200 397 133 388 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M64 318 L100 331 L103 316 L68 303 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M336 318 L300 331 L297 316 L332 303 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M132 222 L133 380 M268 222 L267 380' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3C/svg%3E" alt="Midnight blue, front view">
    </mv-product-gallery>
    <p style="margin:0;font-size:.75rem;color:var(--mv-fg-muted)">Hover to zoom · click for fullscreen · arrow keys · swipe on touch.</p>
  </div>
  <div style="display:grid;gap:.75rem">
    <mv-product-gallery label="Aurora sneaker" thumbs="bottom" hover-thumbs style="--mv-product-gallery-ratio:1;--mv-product-gallery-thumb:4rem">
      <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.96 0.015 230)'/%3E%3Cstop offset='1' stop-color='oklch(0.88 0.035 235)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cellipse cx='200' cy='322' rx='170' ry='14' fill='url(%23fl)' opacity='1'/%3E%3Cg transform='translate(-10 10)'%3E%3Cpath d='M62 300 L358 300 C362 320 350 336 328 336 L86 336 C68 336 58 320 62 300 Z' fill='oklch(0.97 0.005 90)'/%3E%3Cpath d='M62 318 L360 318' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='oklch(0.78 0.07 235)'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='url(%23sh)'/%3E%3Cpath d='M292 240 C324 250 348 262 352 286 L354 300 L318 300 C318 278 306 256 292 240Z' fill='black' fill-opacity='.08'/%3E%3Cpath d='M140 292 C184 258 236 250 306 264' fill='none' stroke='white' stroke-opacity='.85' stroke-width='9' stroke-linecap='round'/%3E%3Cpath d='M214 206 l26 36 M226 200 l26 36 M200 214 l28 34' stroke='oklch(0.97 0.005 90)' stroke-width='5' stroke-linecap='round'/%3E%3Cpath d='M206 198 C226 186 252 188 264 204' fill='none' stroke='black' stroke-opacity='.18' stroke-width='5'/%3E%3Ccircle cx='96' cy='262' r='5' fill='white' fill-opacity='.8'/%3E%3C/g%3E%3C/svg%3E" alt="Aurora sneaker in sky, side view">
      <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.96 0.015 230)'/%3E%3Cstop offset='1' stop-color='oklch(0.88 0.035 235)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cellipse cx='210' cy='372' rx='170' ry='16' fill='url(%23fl)' opacity='1'/%3E%3Cg transform='translate(40 70) scale(.85)'%3E%3Cpath d='M62 300 L358 300 C362 320 350 336 328 336 L86 336 C68 336 58 320 62 300 Z' fill='oklch(0.97 0.005 90)'/%3E%3Cpath d='M62 318 L360 318' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='oklch(0.78 0.07 235)'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='url(%23sh)'/%3E%3Cpath d='M292 240 C324 250 348 262 352 286 L354 300 L318 300 C318 278 306 256 292 240Z' fill='black' fill-opacity='.08'/%3E%3Cpath d='M140 292 C184 258 236 250 306 264' fill='none' stroke='white' stroke-opacity='.85' stroke-width='9' stroke-linecap='round'/%3E%3Cpath d='M214 206 l26 36 M226 200 l26 36 M200 214 l28 34' stroke='oklch(0.97 0.005 90)' stroke-width='5' stroke-linecap='round'/%3E%3Cpath d='M206 198 C226 186 252 188 264 204' fill='none' stroke='black' stroke-opacity='.18' stroke-width='5'/%3E%3Ccircle cx='96' cy='262' r='5' fill='white' fill-opacity='.8'/%3E%3C/g%3E%3Cg transform='translate(-10 30) scale(.85)' opacity='1'%3E%3Cpath d='M62 300 L358 300 C362 320 350 336 328 336 L86 336 C68 336 58 320 62 300 Z' fill='oklch(0.97 0.005 90)'/%3E%3Cpath d='M62 318 L360 318' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='oklch(0.78 0.07 235)'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='url(%23sh)'/%3E%3Cpath d='M292 240 C324 250 348 262 352 286 L354 300 L318 300 C318 278 306 256 292 240Z' fill='black' fill-opacity='.08'/%3E%3Cpath d='M140 292 C184 258 236 250 306 264' fill='none' stroke='white' stroke-opacity='.85' stroke-width='9' stroke-linecap='round'/%3E%3Cpath d='M214 206 l26 36 M226 200 l26 36 M200 214 l28 34' stroke='oklch(0.97 0.005 90)' stroke-width='5' stroke-linecap='round'/%3E%3Cpath d='M206 198 C226 186 252 188 264 204' fill='none' stroke='black' stroke-opacity='.18' stroke-width='5'/%3E%3Ccircle cx='96' cy='262' r='5' fill='white' fill-opacity='.8'/%3E%3C/g%3E%3C/svg%3E" alt="The pair, three-quarter view">
      <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.96 0.015 45)'/%3E%3Cstop offset='1' stop-color='oklch(0.89 0.035 40)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cellipse cx='200' cy='322' rx='170' ry='14' fill='url(%23fl)' opacity='1'/%3E%3Cg transform='translate(-10 10)'%3E%3Cpath d='M62 300 L358 300 C362 320 350 336 328 336 L86 336 C68 336 58 320 62 300 Z' fill='oklch(0.97 0.005 90)'/%3E%3Cpath d='M62 318 L360 318' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='oklch(0.58 0.13 38)'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='url(%23sh)'/%3E%3Cpath d='M292 240 C324 250 348 262 352 286 L354 300 L318 300 C318 278 306 256 292 240Z' fill='black' fill-opacity='.08'/%3E%3Cpath d='M140 292 C184 258 236 250 306 264' fill='none' stroke='white' stroke-opacity='.85' stroke-width='9' stroke-linecap='round'/%3E%3Cpath d='M214 206 l26 36 M226 200 l26 36 M200 214 l28 34' stroke='oklch(0.97 0.005 90)' stroke-width='5' stroke-linecap='round'/%3E%3Cpath d='M206 198 C226 186 252 188 264 204' fill='none' stroke='black' stroke-opacity='.18' stroke-width='5'/%3E%3Ccircle cx='96' cy='262' r='5' fill='white' fill-opacity='.8'/%3E%3C/g%3E%3C/svg%3E" alt="Brick, side view">
      <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.96 0.015 45)'/%3E%3Cstop offset='1' stop-color='oklch(0.89 0.035 40)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cellipse cx='210' cy='372' rx='170' ry='16' fill='url(%23fl)' opacity='1'/%3E%3Cg transform='translate(40 70) scale(.85)'%3E%3Cpath d='M62 300 L358 300 C362 320 350 336 328 336 L86 336 C68 336 58 320 62 300 Z' fill='oklch(0.97 0.005 90)'/%3E%3Cpath d='M62 318 L360 318' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='oklch(0.58 0.13 38)'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='url(%23sh)'/%3E%3Cpath d='M292 240 C324 250 348 262 352 286 L354 300 L318 300 C318 278 306 256 292 240Z' fill='black' fill-opacity='.08'/%3E%3Cpath d='M140 292 C184 258 236 250 306 264' fill='none' stroke='white' stroke-opacity='.85' stroke-width='9' stroke-linecap='round'/%3E%3Cpath d='M214 206 l26 36 M226 200 l26 36 M200 214 l28 34' stroke='oklch(0.97 0.005 90)' stroke-width='5' stroke-linecap='round'/%3E%3Cpath d='M206 198 C226 186 252 188 264 204' fill='none' stroke='black' stroke-opacity='.18' stroke-width='5'/%3E%3Ccircle cx='96' cy='262' r='5' fill='white' fill-opacity='.8'/%3E%3C/g%3E%3Cg transform='translate(-10 30) scale(.85)' opacity='1'%3E%3Cpath d='M62 300 L358 300 C362 320 350 336 328 336 L86 336 C68 336 58 320 62 300 Z' fill='oklch(0.97 0.005 90)'/%3E%3Cpath d='M62 318 L360 318' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='oklch(0.58 0.13 38)'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='url(%23sh)'/%3E%3Cpath d='M292 240 C324 250 348 262 352 286 L354 300 L318 300 C318 278 306 256 292 240Z' fill='black' fill-opacity='.08'/%3E%3Cpath d='M140 292 C184 258 236 250 306 264' fill='none' stroke='white' stroke-opacity='.85' stroke-width='9' stroke-linecap='round'/%3E%3Cpath d='M214 206 l26 36 M226 200 l26 36 M200 214 l28 34' stroke='oklch(0.97 0.005 90)' stroke-width='5' stroke-linecap='round'/%3E%3Cpath d='M206 198 C226 186 252 188 264 204' fill='none' stroke='black' stroke-opacity='.18' stroke-width='5'/%3E%3Ccircle cx='96' cy='262' r='5' fill='white' fill-opacity='.8'/%3E%3C/g%3E%3C/svg%3E" alt="Brick, the pair">
    </mv-product-gallery>
    <output id="pg-demo-out" aria-live="polite" style="font-size:.75rem;color:var(--mv-fg-muted)">Thumbnails below, swap on hover.</output>
  </div>
</div>
<script type="module">
  const out = document.getElementById("pg-demo-out");
  document.getElementById("pg-demo").addEventListener("mv-change", (e) => {
    if (e.target.localName === "mv-product-gallery") out.textContent = `mv-change · image ${e.detail.index + 1}`;
  });
</script>

API

Attributes

NameTypeDefaultDescription
indexnumber0Displayed image (0-based), reflected on every change.
thumbsstart | bottom | nonestartThumbnails in a left column, a row below, or none. Under 26rem wide: a segmented bar instead.
hover-thumbsbooleanHovering a thumbnail with the mouse switches the image.
zoomnumber2Hover zoom factor (mouse); 1 disables it.
labelstringProduct imagesAccessible name of the carousel and the fullscreen view.
no-fullscreenbooleanDisables fullscreen.

Properties

NameTypeDescription
countnumberNumber of images (read-only).

Methods

NameDescription
goTo(index, { smooth }) / next() / prev()Navigation.
openFullscreen(index?)Opens fullscreen.

Events

NameDescription
mv-changedetail: { index, image }.
mv-fullscreendetail: { open, index }.

Content structure

NameDescription
<img> / <picture>Direct children are the images; data-thumb for a different thumbnail.

CSS classes

NameDescription
mv-product-gallery-thumbs / -thumb / -main / -viewport / -track / -slide / -image / -arrow / -fullscreen / -counter / -bar / -lightboxGenerated parts; data-active, data-zoomed.

CSS variables

NameDefaultDescription
--mv-product-gallery-ratio4 / 5Image aspect ratio.
--mv-product-gallery-thumb4.5remThumbnail width.
--mv-product-gallery-radiusvar(--mv-radius-xl)Main image radius.

Accessibility

APG carousel pattern: focusable aria-roledescription=“carousel” region (←/→, Home/End, Enter/Space = fullscreen), role=group slides “2 of 5”, hidden images are inert. Thumbnails are “Image 2 of 5: …” buttons with aria-current, navigable with arrow keys (roving tabindex). Changes are announced (aria-live polite). Named arrows, disabled at the ends. Fullscreen: native modal <dialog> (focus trap, Esc, focus returned to the gallery). Reduced motion: instant scrolling, no hover zoom.