beta

Lens — <mv-lens>

Magnifier that follows the cursor over an image or any content: an enlarged copy clipped to a circle (or square) anchored at the pointer, glass rim, animated opening, optional fixed position and keyboard movement.

CategoryMedia
TypeWeb Component (<mv-lens>)
Statusbeta
Keywordslens, magnifier, zoom, loupe, image, product, hover, magnifying-glass

When to use

Avoid when

Install

node scripts/add.mjs lens --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["lens"], 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/pointer.js, components/lens/lens.js, components/lens/lens.css.

Usage

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

<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(18rem,1fr));gap:2rem;align-items:center;width:100%;max-width:58rem">
  <mv-lens zoom="3" size="190" label="Lumen watch dial" style="max-width:26rem;width:100%;justify-self:center;border-radius:var(--mv-radius-2xl)">
    <svg viewBox="0 0 600 600" role="img" aria-label="Lumen Automatic watch, midnight-blue sunburst dial, date at 3 o’clock">
      <defs>
        <radialGradient id="lens-demo-bg" cx="50%" cy="40%" r="75%"><stop offset="0" stop-color="#e9e7f5"/><stop offset="1" stop-color="#b9b5cf"/></radialGradient>
        <radialGradient id="lens-demo-dial" cx="45%" cy="38%" r="70%"><stop offset="0" stop-color="#3b3f9c"/><stop offset=".55" stop-color="#1c1f5e"/><stop offset="1" stop-color="#0b0c2a"/></radialGradient>
        <linearGradient id="lens-demo-case" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f8f8fb"/><stop offset=".45" stop-color="#9a98a8"/><stop offset=".55" stop-color="#d9d8e0"/><stop offset="1" stop-color="#6f6d7d"/></linearGradient>
        <linearGradient id="lens-demo-hand" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#c9c8d6"/></linearGradient>
        <path id="lens-demo-arc" d="M150 300a150 150 0 0 1 300 0"/>
      </defs>
      <rect width="600" height="600" fill="url(#lens-demo-bg)"/>
      <rect x="262" y="0" width="76" height="70" rx="6" fill="#2a2b48"/><rect x="262" y="530" width="76" height="70" rx="6" fill="#2a2b48"/>
      <rect x="548" y="282" width="22" height="36" rx="5" fill="url(#lens-demo-case)"/>
      <circle cx="300" cy="300" r="262" fill="url(#lens-demo-case)"/>
      <circle cx="300" cy="300" r="244" fill="#15163f"/>
      <circle cx="300" cy="300" r="236" fill="url(#lens-demo-dial)"/>
      <g opacity=".08" stroke="#fff" stroke-width="1"><path d="M300 64V536M64 300H536M133 133 467 467M467 133 133 467M209 81 391 519M391 81 209 519M81 209 519 391M81 391 519 209"/></g>
      <circle cx="300" cy="300" r="222" fill="none" stroke="#c7c9ee" stroke-width="10" stroke-dasharray="1.2 22.03" stroke-dashoffset=".6" transform="rotate(-90 300 300)"/>
      <circle cx="300" cy="300" r="214" fill="none" stroke="#fff" stroke-width="26" stroke-dasharray="6 106.05" stroke-dashoffset="3" transform="rotate(-90 300 300)"/>
      <g fill="#dfe0ff" font-family="Georgia, serif" font-size="10" text-anchor="middle">
        <text x="300" y="104">60</text><text x="498" y="304">15</text><text x="300" y="506">30</text><text x="102" y="304">45</text>
      </g>
      <text x="300" y="196" fill="#fff" font-family="Georgia, serif" font-size="30" letter-spacing="8" text-anchor="middle">LUMEN</text>
      <text x="300" y="216" fill="#aeb1ea" font-family="Arial, sans-serif" font-size="8.5" letter-spacing="3" text-anchor="middle">AUTOMATIC · 28,800 VPH</text>
      <text fill="#8f93d8" font-family="Arial, sans-serif" font-size="6" letter-spacing="2.2" text-anchor="middle"><textPath href="#lens-demo-arc" startOffset="50%">HAND-ASSEMBLED · 72-HOUR POWER RESERVE · NO. 0417/1200</textPath></text>
      <text x="300" y="418" fill="#c7c9ee" font-family="Arial, sans-serif" font-size="9" letter-spacing="2" text-anchor="middle">CERTIFIED CHRONOMETER</text>
      <text x="300" y="432" fill="#8f93d8" font-family="Arial, sans-serif" font-size="6" letter-spacing="1.6" text-anchor="middle">SAPPHIRE · 100 M · 25 JEWELS</text>
      <text x="300" y="512" fill="#7f83c8" font-family="Arial, sans-serif" font-size="5.5" letter-spacing="1.8" text-anchor="middle">FRANCE</text>
      <rect x="420" y="287" width="38" height="26" rx="3" fill="#f7f7fb" stroke="#9a98a8" stroke-width="2"/>
      <text x="439" y="306" fill="#1c1f5e" font-family="Arial, sans-serif" font-size="17" font-weight="700" text-anchor="middle">23</text>
      <g transform="rotate(-62 300 300)"><path d="M293 300 297 170 300 158 303 170 307 300Z" fill="url(#lens-demo-hand)"/><path d="M298 250 300 175 302 250Z" fill="#8ee6c5"/></g>
      <g transform="rotate(58 300 300)"><path d="M295 300 298 108 300 96 302 108 305 300Z" fill="url(#lens-demo-hand)"/><path d="M299 250 300 115 301 250Z" fill="#8ee6c5"/></g>
      <g transform="rotate(196 300 300)"><path d="M299 340 299.3 90 300.7 90 301 340Z" fill="#ff6b4a"/><circle cx="300" cy="120" r="5" fill="none" stroke="#ff6b4a" stroke-width="2"/></g>
      <circle cx="300" cy="300" r="9" fill="#e8e8f2"/><circle cx="300" cy="300" r="3.5" fill="#ff6b4a"/>
    </svg>
  </mv-lens>
  <div style="display:grid;gap:1rem;max-width:24rem">
    <p style="margin:0;font:600 .75rem/1 var(--mv-font-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--mv-accent-fg)">Limited edition · 1,200 pieces</p>
    <h3 style="margin:0;font:700 1.6rem/1.15 var(--mv-font-sans);letter-spacing:-.02em">Lumen Automatic 40 mm</h3>
    <p style="margin:0;color:var(--mv-fg-muted);line-height:1.6">Hover over the dial to read the fine print: power reserve, serial number, certification. With a keyboard, move the magnifier with the arrow keys.</p>
    <mv-lens data-shape="square" zoom="2.6" size="120" label="Alpine landscape" style="border-radius:var(--mv-radius-lg)">
      <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 700'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%231e1b4b'/%3E%3Cstop offset='.45' stop-color='%236d28d9'/%3E%3Cstop offset='.78' stop-color='%23f472b6'/%3E%3Cstop offset='1' stop-color='%23fdba74'/%3E%3C/linearGradient%3E%3ClinearGradient id='a' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%237c3aed'/%3E%3Cstop offset='1' stop-color='%23312e81'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%233b0764'/%3E%3Cstop offset='1' stop-color='%231e1b4b'/%3E%3C/linearGradient%3E%3ClinearGradient id='w' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23fb923c' stop-opacity='.85'/%3E%3Cstop offset='.5' stop-color='%239d174d' stop-opacity='.7'/%3E%3Cstop offset='1' stop-color='%231e1b4b'/%3E%3C/linearGradient%3E%3CradialGradient id='g'%3E%3Cstop offset='0' stop-color='%23fff7ed'/%3E%3Cstop offset='.3' stop-color='%23fde68a'/%3E%3Cstop offset='1' stop-color='%23fb923c' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect width='1200' height='470' fill='url(%23s)'/%3E%3Ccircle cx='760' cy='420' r='210' fill='url(%23g)'/%3E%3Ccircle cx='760' cy='430' r='54' fill='%23fff7ed'/%3E%3Cpath d='M0 390 140 250l60 45 40-30 120-100 70 90 40-30 90 70 60-50 90 110 110-130 70 60 40-20 70 60 50-60 130 110 V480 H0Z' fill='url(%23a)'/%3E%3Cpath d='M360 165 395 195 380 205 415 222 440 200 420 180Z M820 230 850 258 840 266 870 280 890 262Z' fill='%23f5f3ff' opacity='.75'/%3E%3Cpath d='M0 480 120 390l140 60 140-100 160 110 140-70 150 80 150-90 200 80 V480 H0Z' fill='url(%23b)'/%3E%3Crect y='470' width='1200' height='230' fill='url(%23w)'/%3E%3Cg fill='%23fde68a'%3E%3Crect x='640' y='500' width='240' height='5' rx='2.5' opacity='.7'/%3E%3Crect x='680' y='530' width='160' height='4' rx='2' opacity='.5'/%3E%3Crect x='710' y='560' width='100' height='3' rx='1.5' opacity='.4'/%3E%3Crect x='735' y='590' width='50' height='3' rx='1.5' opacity='.3'/%3E%3C/g%3E%3Cpath d='M0 700 V610 Q180 570 360 625 T760 645 T1200 600 V700Z' fill='%230b0720'/%3E%3Cg fill='%230b0720'%3E%3Cpath d='M90 610 110 520 130 610Z M130 615 155 500 180 615Z M1040 600 1062 505 1084 600Z M1080 605 1100 530 1120 605Z'/%3E%3C/g%3E%3C/svg%3E" alt="Mountain lake at sunset" loading="lazy">
    </mv-lens>
    <p style="margin:0;font-size:.8rem;color:var(--mv-fg-subtle)">data-shape="square" · zoom 2.6×</p>
  </div>
</div>

API

Attributes

NameTypeDefaultDescription
zoomnumber2.5Magnification factor.
sizenumber170Diameter (or side) of the magnifier in px.
data-shapecircle | squarecircleMagnifier shape.
staticbooleanMagnifier always visible, fixed at position.
positionstring50,50“x,y” in % for static mode.
labelstringMagnifierAccessible name of the group.

Methods

NameDescription
refresh()Copies the content into the magnifier again after the children change.

CSS classes

NameDescription
mv-lens-view / -zoom / -ringClipped layer, enlarged copy, rim (generated).

CSS variables

NameDefaultDescription
--mv-lens-radiusvar(--mv-radius-xl)Frame radius.
--mv-lens-rimwhite 55%Rim color.
--mv-lens-backdropvar(--mv-bg)Background under the enlarged copy.

Accessibility

Focusable group (aria-roledescription “magnifier”): on keyboard focus the magnifier appears, ←/→/↑/↓ move it (Shift: bigger steps), Escape closes it. The enlarged copy is aria-hidden and inert (no duplicated ids); the original content keeps its alt text. Touch: press and hold. Reduced motion: opening is instant.