<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[imgi]]></title><description><![CDATA[imgi]]></description><link>https://imgico.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>imgi</title><link>https://imgico.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 11 Sep 2026 17:15:45 GMT</lastBuildDate><atom:link href="https://imgico.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[A before/after image slider in about 120 lines, no library]]></title><description><![CDATA[Every "drag to compare two images" widget you've run into is doing the same small trick underneath, and it's a lot simpler than the pile of npm packages makes it look. Two images in one box, the top o]]></description><link>https://imgico.hashnode.dev/a-before-after-image-slider-in-about-120-lines-no-library</link><guid isPermaLink="true">https://imgico.hashnode.dev/a-before-after-image-slider-in-about-120-lines-no-library</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Henrik]]></dc:creator><pubDate>Tue, 14 Jul 2026 23:51:36 GMT</pubDate><content:encoded><![CDATA[<p>Every "drag to compare two images" widget you've run into is doing the same small trick underneath, and it's a lot simpler than the pile of npm packages makes it look. Two images in one box, the top one clipped, and you drag where the clip ends. No canvas, no framework, nothing to install. Here's the whole thing as a web component you can drop on any page.</p>
<p>I pulled this out of <a href="https://imgi.co">imgi.co</a>, the before/after tool I run, because the slider was never the hard part and there's no reason to keep it locked away. Take it, ship it.</p>
<h2>The idea</h2>
<p>Stack two images in a container. The bottom one, the "before", is just a plain img. The top one, the "after", sits inside a wrapper you clip with clip-path. Move the clip and you reveal more or less of the top image. A handle rides along the clip edge so people know there's something to drag.</p>
<p>The one line that does the actual work:</p>
<pre><code class="language-js">frame.style.clipPath = `inset(0 ${100 - pct}% 0 0)`;
</code></pre>
<p><code>pct</code> is the handle position, 0 to 100. At 50 you clip half off the right of the top image, so the left shows the after and the right shows the before underneath. clip-path runs on the compositor and doesn't trigger layout, so dragging stays smooth even on a chunky 4000px screenshot. Animating width instead would reflow on every frame and you'd feel it.</p>
<p>One thing that trips people up: clip the wrapper, not the image. Resize the img itself and the picture squashes, the two frames drift out of alignment, and the whole comparison lies to you. Keep the after-image full size inside a wrapper that matches the base image, and clip the wrapper.</p>
<h2>Dragging, for real fingers and mice</h2>
<p>Skip the separate mouse and touch handlers. Pointer events give you one code path for mouse, touch, and pen:</p>
<pre><code class="language-js">el.addEventListener("pointerdown", (e) =&gt; {
  el.setPointerCapture(e.pointerId);
  move(e);
  window.addEventListener("pointermove", move);
  window.addEventListener("pointerup", up, { once: true });
});

function move(e) {
  const r = el.getBoundingClientRect();
  set(((e.clientX - r.left) / r.width) * 100);
}
function up() {
  window.removeEventListener("pointermove", move);
}
</code></pre>
<p>Two details that are easy to miss. Capture the pointer (or bind pointermove to window, like above) so a fast drag that shoots off the element doesn't just stop dead. And put <code>touch-action: none</code> on the handle, otherwise dragging sideways on a phone scrolls the page instead of moving the slider. Leave the rest of the element scrollable so the page still behaves.</p>
<h2>The keyboard, which everyone forgets</h2>
<pre><code class="language-js">handle.setAttribute("role", "slider");
handle.setAttribute("tabindex", "0");
handle.setAttribute("aria-valuenow", "50");

handle.addEventListener("keydown", (e) =&gt; {
  const step = e.shiftKey ? 10 : 2;
  if (e.key === "ArrowLeft" || e.key === "ArrowUp") set(pos - step);
  if (e.key === "ArrowRight" || e.key === "ArrowDown") set(pos + step);
  if (e.key === "Home") set(0);
  if (e.key === "End") set(100);
});
</code></pre>
<p>Making the handle a real focusable slider with arrow keys is maybe five lines, and it's the gap between a demo and something you'd actually put in front of users. Update aria-valuenow when the position changes so a screen reader can follow along.</p>
<h2>Wrapping it as a custom element</h2>
<p>Put it in a custom element and the markup on the page reads like plain HTML:</p>
<pre><code class="language-html">&lt;before-after
  before="original.jpg"
  after="edited.jpg"
  before-label="Before"
  after-label="After"&gt;&lt;/before-after&gt;
</code></pre>
<p>Inside, a shadow root holds the two images, the clip wrapper, and the handle, plus a bit of scoped CSS. The element reads its <code>before</code> and <code>after</code> attributes, wires up the pointer and key handlers in connectedCallback, and exposes a <code>value</code> property and a <code>change</code> event so the rest of your app can drive it or react to it. That's the entire surface.</p>
<h2>That's the whole thing</h2>
<p>A working before/after image comparison slider, vanilla JS, no build step, MIT. The clip trick is the core, pointer events handle the input, and a focusable handle keeps it accessible. Everything past that is taste.</p>
<p>If you'd rather not host it, <a href="https://imgi.co">imgi.co</a> gives you the same slider on a permanent shareable link with a copy-paste embed, plus GIF and MP4 export for the spots a live slider won't load, like Reddit or a Discord post. But if you just want the widget on your own page, the code up there is all of it. Go build.</p>
]]></content:encoded></item></channel></rss>