Beta
Image Compare <mv-image-compare>
Vorher/Nachher-Vergleich, bei dem der ganze Rahmen die Schiene des Sliders ist: ein kapselförmiger Griff mit role=slider und voller Tastaturunterstützung, ein Live-Wert-Chip, Badges, die sauber an der Linie abgeschnitten werden, Touch-Bedienung, die Tippen, Wischen und Seitenscrollen unterscheidet, horizontal oder vertikal, Ziehen oder Mausverfolgung, ein optionaler Schwenk und eine Glow-Variante.
| Kategorie | Medien |
|---|---|
| Typ | Web Component (<mv-image-compare>) |
| Status | Beta |
| Keywords | compare, before-after, comparison, slider, image, diff, reveal |
When to use
- Before and after results must be compared on one image, like retouching, color grading or renovations
- Two aligned versions of a render, design or any HTML content should be compared with a draggable, keyboard-operable divider
- A hero demo should sweep automatically to show a transformation until the visitor takes over
Avoid when
- A second layer should be uncovered by a circle that follows the cursor → use Reveal Mask instead
- The two images are not aligned views of the same scene; show them side by side instead
Installation
node scripts/add.mjs image-compare --out ./src/marvelousKI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["image-compare"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Kopierte Dateien (inklusive Abhängigkeiten): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/image-compare/image-compare.js, components/image-compare/image-compare.css.
Verwendung
Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),1fr));gap:1.5rem;width:100%;max-width:62rem;align-items:start">
<figure style="margin:0;display:grid;gap:.75rem">
<mv-image-compare id="ic-demo-photo" label="Compare the raw and color-graded photo" before-label="Raw" after-label="Graded" value="42" autoplay style="aspect-ratio:12/7">
<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="Sunset over a mountain lake, raw file with dull colors" loading="lazy" style="filter:grayscale(.72) contrast(.78) brightness(1.1) sepia(.12)">
<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="The same sunset after color grading, vivid colors" loading="lazy">
</mv-image-compare>
<figcaption style="display:flex;justify-content:space-between;gap:1rem;font-size:.85rem;color:var(--mv-fg-muted)">
<span>Lake Annecy · “Dusk” preset</span>
<output id="ic-demo-out" style="font-variant-numeric:tabular-nums">42%</output>
</figcaption>
</figure>
<figure style="margin:0;display:grid;gap:.75rem">
<mv-image-compare orientation="vertical" mode="hover" data-variant="glow" label="Compare the wireframe and the final UI" before-label="Wireframe" after-label="Final UI" style="aspect-ratio:4/3">
<div aria-hidden="true" style="display:grid;grid-template-rows:auto 1fr auto;gap:12px;padding:18px;background:var(--mv-bg-subtle);font:500 11px/1 var(--mv-font-mono);color:var(--mv-fg-subtle)">
<div style="display:flex;gap:8px;align-items:center"><span style="width:26px;height:26px;border:1.5px dashed currentColor;border-radius:6px"></span><span style="flex:1;height:10px;border:1.5px dashed currentColor;border-radius:4px;max-width:120px"></span><span style="width:70px;height:26px;border:1.5px dashed currentColor;border-radius:6px"></span></div>
<div style="display:grid;grid-template-columns:1.2fr 1fr;gap:12px"><div style="border:1.5px dashed currentColor;border-radius:10px;display:grid;place-items:center">IMAGE</div><div style="display:grid;gap:8px;align-content:center"><span style="height:14px;border:1.5px dashed currentColor;border-radius:4px"></span><span style="height:14px;width:70%;border:1.5px dashed currentColor;border-radius:4px"></span><span style="height:8px;width:90%;background:currentColor;opacity:.3;border-radius:4px"></span><span style="height:8px;width:80%;background:currentColor;opacity:.3;border-radius:4px"></span><span style="height:28px;width:60%;border:1.5px dashed currentColor;border-radius:8px;margin-top:6px"></span></div></div>
<div style="display:flex;gap:8px"><span style="flex:1;height:34px;border:1.5px dashed currentColor;border-radius:8px"></span><span style="flex:1;height:34px;border:1.5px dashed currentColor;border-radius:8px"></span><span style="flex:1;height:34px;border:1.5px dashed currentColor;border-radius:8px"></span></div>
</div>
<div aria-hidden="true" style="display:grid;grid-template-rows:auto 1fr auto;gap:12px;padding:18px;background:var(--mv-surface);font:13px/1.3 var(--mv-font-sans);color:var(--mv-fg)">
<div style="display:flex;gap:8px;align-items:center"><span style="width:26px;height:26px;border-radius:7px;background:linear-gradient(135deg,var(--mv-accent),color-mix(in oklch,var(--mv-accent),white 40%))"></span><strong style="flex:1">Lumen Studio</strong><span style="padding:6px 12px;border-radius:999px;background:var(--mv-accent);color:var(--mv-fg-on-accent);font-size:12px;font-weight:600">Book now</span></div>
<div style="display:grid;grid-template-columns:1.2fr 1fr;gap:12px"><div style="border-radius:10px;background:radial-gradient(circle at 70% 35%,#fde68a 0 12%,transparent 13%),linear-gradient(180deg,#6d28d9,#f472b6 70%,#fdba74)"></div><div style="display:grid;gap:6px;align-content:center"><strong style="font-size:17px;letter-spacing:-.01em">Mountain photo workshop</strong><span style="color:var(--mv-fg-muted);font-size:12px">3 days · Annecy · 8 spots</span><span style="color:var(--mv-fg-muted);font-size:12px">Golden light, lakes and peaks.</span><span style="justify-self:start;margin-top:6px;padding:7px 12px;border-radius:8px;border:1px solid var(--mv-border-strong);font-size:12px;font-weight:600">View itinerary</span></div></div>
<div style="display:flex;gap:8px"><span style="flex:1;padding:8px 10px;border-radius:8px;background:var(--mv-bg-muted);font-size:11px"><b style="display:block;font-size:14px">4.9★</b>reviews</span><span style="flex:1;padding:8px 10px;border-radius:8px;background:var(--mv-bg-muted);font-size:11px"><b style="display:block;font-size:14px">$640</b>all-inclusive</span><span style="flex:1;padding:8px 10px;border-radius:8px;background:var(--mv-accent-subtle);color:var(--mv-accent-fg);font-size:11px"><b style="display:block;font-size:14px">May 12</b>departure</span></div>
</div>
</mv-image-compare>
<figcaption style="font-size:.85rem;color:var(--mv-fg-muted)">Vertical, mode="hover": the line follows a mouse; touch and keyboard still drag.</figcaption>
</figure>
</div>
<script type="module">
const ic = document.getElementById("ic-demo-photo");
const out = document.getElementById("ic-demo-out");
ic.addEventListener("mv-input", (e) => { out.textContent = `${Math.round(e.detail.value)}%`; });
</script>API
Attributes
| Name | Typ | Default | Description |
|---|---|---|---|
value | number | 50 | Divider position (0-100, share of “before” kept visible). |
mode | drag | hover | drag | drag: press or drag anywhere; hover: the divider follows a mouse on fine-pointer devices (touch and keyboard always drag). |
orientation | horizontal | vertical | horizontal | horizontal: before on the left, vertical line; vertical: before on top, horizontal line. |
before-label / after-label | string | Corner badges, clipped by the line with their layer; also used in aria-valuetext. | |
label | string | Compare before and after | Accessible name of the slider. |
autoplay | boolean | Gently sweeps until the first interaction; holds still under the mouse, pauses off-screen, off with reduced motion. | |
data-variant | glow | Accent-tinted line with a soft halo. |
Properties
| Name | Typ | Description |
|---|---|---|
position | number | Current position (read/write, no event). |
MvImageCompare.strings | { label, before, after } | Default English wording, to translate once for every instance. |
Events
| Name | Description |
|---|---|
mv-input | While the position changes from the user (detail: { value }). |
mv-change | When a drag, tap, key press or mouse-follow ends on a new position (detail: { value }). |
Content structure
| Name | Description |
|---|---|
1st child | “Before” layer (img, picture, video or any content). |
2nd child | “After” layer, revealed on the right (or bottom). |
CSS classes
| Name | Description |
|---|---|
mv-image-compare-before / -after / -divider / -handle / -knob / -value / -label | Parts (added to the children or generated). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-image-compare-line | white | Line and knob color. |
--mv-image-compare-line-width | 2px | Line thickness. |
--mv-image-compare-knob-size | 2.75rem | Knob length (the capsule is 64% as wide); the hit area never drops below 44px. |
--mv-image-compare-radius | var(--mv-radius-xl) | Frame corner radius. |
Accessibility
The handle is a focusable role=slider with aria-valuenow, aria-valuetext (“Raw 42%, Graded 58%”) and aria-orientation. Keys: ←/→ (↑/↓ also) ±1, Shift ±10, PageUp/PageDown ±10, Home/End; in vertical, ↑ and PageUp move the line up. Both layers keep their own alt text; the badges and value chip are aria-hidden. Touch: a swipe along the axis drags, a tap jumps, a swipe across it scrolls the page; the thumb hit area is at least 44px. Reduced motion: no sweep and no glide. Forced colors: system-colored line, knob and focus ring. Hover effects and mouse-follow only apply to fine pointers; the system cursor is never hidden.