Before/After Slider

Draggable divider comparing two images.

html

<div class="ba-slider" style="--pos:50%;--accent:#BFD9F2;">
  <div class="ba-slider__frame">
    <div class="ba-slider__img ba-slider__img--after" aria-hidden="true">
      <svg viewBox="0 0 400 240" width="100%" height="100%" preserveAspectRatio="xMidYMid slice"><rect width="400" height="240" fill="#A8E6C1"/><circle cx="300" cy="80" r="50" fill="#FAF7F2"/></svg>
      <span class="ba-slider__tag">After</span>
    </div>
    <div class="ba-slider__img ba-slider__img--before" aria-hidden="true">
      <svg viewBox="0 0 400 240" width="100%" height="100%" preserveAspectRatio="xMidYMid slice"><rect width="400" height="240" fill="#241430"/><circle cx="300" cy="80" r="50" fill="#D9C8F0"/></svg>
      <span class="ba-slider__tag">Before</span>
    </div>
    <div class="ba-slider__handle"></div>
  </div>
  <!-- In production, sync an input[type=range] with this element's --pos custom property on the "input" event to make the divider live-draggable. -->
  <input class="ba-slider__range" type="range" min="0" max="100" value="50" aria-label="Comparison position between Before and After" />
</div>
<style>
.ba-slider { max-width: 360px; font: 12px/1.4 system-ui, sans-serif; color: #241430; }
.ba-slider__frame { position: relative; aspect-ratio: 5 / 3; border-radius: 12px; overflow: hidden; border: 1px solid #24143022; }
.ba-slider__img { position: absolute; inset: 0; }
.ba-slider__img svg { display: block; width: 100%; height: 100%; }
.ba-slider__img--before { clip-path: inset(0 calc(100% - var(--pos)) 0 0); }
.ba-slider__tag { position: absolute; top: 8px; left: 8px; padding: 2px 8px; border-radius: 6px; background: #241430cc; color: #FAF7F2; font-weight: 600; }
.ba-slider__handle { position: absolute; top: 0; bottom: 0; left: var(--pos); width: 3px; background: var(--accent); transform: translateX(-50%); pointer-events: none; }
.ba-slider__range { width: 100%; margin-top: 10px; accent-color: var(--accent); }
</style>

tailwind

<div class="ba-slider-tw max-w-[360px] font-sans text-xs text-[#241430]" style="--pos:50%;--accent:#BFD9F2;">
  <div class="relative overflow-hidden rounded-xl border border-[#24143022]" style="aspect-ratio:5/3;">
    <div class="absolute inset-0" aria-hidden="true">
      <svg viewBox="0 0 400 240" class="block w-full h-full" preserveAspectRatio="xMidYMid slice"><rect width="400" height="240" fill="#A8E6C1"/><circle cx="300" cy="80" r="50" fill="#FAF7F2"/></svg>
      <span class="absolute top-2 left-2 px-2 py-0.5 rounded-md font-semibold text-[#FAF7F2]" style="background:#241430cc;">After</span>
    </div>
    <div class="absolute inset-0" style="clip-path:inset(0 calc(100% - var(--pos)) 0 0);" aria-hidden="true">
      <svg viewBox="0 0 400 240" class="block w-full h-full" preserveAspectRatio="xMidYMid slice"><rect width="400" height="240" fill="#241430"/><circle cx="300" cy="80" r="50" fill="#D9C8F0"/></svg>
      <span class="absolute top-2 left-2 px-2 py-0.5 rounded-md font-semibold text-[#FAF7F2]" style="background:#241430cc;">Before</span>
    </div>
    <div class="absolute top-0 bottom-0 w-[3px] pointer-events-none" style="left:var(--pos);background:var(--accent);transform:translateX(-50%);"></div>
  </div>
  <!-- In production, sync an input[type=range] with this element's --pos custom property on the "input" event to make the divider live-draggable. -->
  <input class="w-full mt-2.5" style="accent-color:var(--accent);" type="range" min="0" max="100" value="50" aria-label="Comparison position between Before and After" />
</div>

react

export function BeforeAfterSlider({
  beforeLabel = "Before",
  afterLabel = "After",
  position = 50,
  accent = "#BFD9F2",
}) {
  return (
    <div style={{ maxWidth: 360, font: "12px/1.4 system-ui, sans-serif", color: "#241430",
      "--pos": `${position}%`, "--accent": accent }}>
      <div style={{ position: "relative", overflow: "hidden", borderRadius: 12, border: "1px solid #24143022", aspectRatio: "5 / 3" }}>
        <div style={{ position: "absolute", inset: 0 }} aria-hidden="true">
          <svg viewBox="0 0 400 240" width="100%" height="100%" preserveAspectRatio="xMidYMid slice"><rect width="400" height="240" fill="#A8E6C1" /><circle cx="300" cy="80" r="50" fill="#FAF7F2" /></svg>
          <span style={{ position: "absolute", top: 8, left: 8, padding: "2px 8px", borderRadius: 6, background: "#241430cc", color: "#FAF7F2", fontWeight: 600 }}>{afterLabel}</span>
        </div>
        <div style={{ position: "absolute", inset: 0, clipPath: `inset(0 calc(100% - ${position}%) 0 0)` }} aria-hidden="true">
          <svg viewBox="0 0 400 240" width="100%" height="100%" preserveAspectRatio="xMidYMid slice"><rect width="400" height="240" fill="#241430" /><circle cx="300" cy="80" r="50" fill="#D9C8F0" /></svg>
          <span style={{ position: "absolute", top: 8, left: 8, padding: "2px 8px", borderRadius: 6, background: "#241430cc", color: "#FAF7F2", fontWeight: 600 }}>{beforeLabel}</span>
        </div>
        <div style={{ position: "absolute", top: 0, bottom: 0, left: `${position}%`, width: 3, background: accent, transform: "translateX(-50%)", pointerEvents: "none" }} />
      </div>
      {/* In production, wire this range's onChange to update `position` state so the divider is live-draggable. */}
      <input
        type="range"
        min={0}
        max={100}
        defaultValue={position}
        aria-label={`Comparison position between ${beforeLabel} and ${afterLabel}`}
        style={{ width: "100%", marginTop: 10, accentColor: accent }}
      />
    </div>
  );
}