Skip to content

Image Compare Directive

The v-image-compare directive is used to compare two images by dragging a slider over them.

Usage

horizontal compare

vertical compare

View Code
vue
<script setup>
const compareData = [
  {
    name: "horizontal compare",
    before:
      "https://images.pexels.com/photos/4344259/pexels-photo-4344259.jpeg?auto=compress&cs=tinysrgb&w=800&lazy=load",
    after:
      "https://images.pexels.com/photos/4344267/pexels-photo-4344267.jpeg?auto=compress&cs=tinysrgb&w=800&lazy=load",
    sliderValue: 50,
  },
  {
    name: "vertical compare",
    before:
      "https://images.pexels.com/photos/3754814/pexels-photo-3754814.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
    after:
      "https://images.pexels.com/photos/3754814/pexels-photo-3754814.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
    sliderValue: 30,
    direction: "vertical", // default is horizontal
    customClassSuffix: "gray", // => before-gray and after-gray, just to add some styles
  },
];
</script>

<template>
  <main class="grid grid-cols-1 items-center justify-center">
    <div v-for="(item, index) in compareData" :key="index">
      <h3 class="capitalize pb-2">{{ item.name }}</h3>
      <div
        class="rounded-2xl overflow-hidden"
        v-image-compare="{
          before: item.before,
          after: item.after,
          sliderValue: item.sliderValue,
          direction: item.direction,
          customClassSuffix: item.customClassSuffix,
        }"
      ></div>
    </div>
  </main>
</template>

<style>
.image-compare-container {
  box-sizing: border-box;
  position: relative;
  display: flex;
  /* flex-direction: column; */
  overflow: hidden;
  width: 100%;
}

.after {
  display: flex;
  position: absolute;
  object-fit: cover;
  height: 100%;
  width: 100%;
}

.before,
.after {
  user-select: none;
}

.handle-vertical {
  rotate: 90deg;
  transform: translateX(-50%);
}
.after-gray {
  filter: grayscale(100%);
}
</style>

Arguments

The v-image-compare does not take any arguments.

Modifiers

The v-image-compare does not take any modifiers.

Source Code

js
export const vImageCompare = {
  mounted(el, binding) {
    const container = document.createElement("div");
    container.classList.add("image-compare-container");

    const imgWrapperBefore = document.createElement("img");
    imgWrapperBefore.classList.add("img-wrapper", "before");
    binding.value.customClassSuffix
      ? imgWrapperBefore.classList.add(
          "before-" + binding.value.customClassSuffix
        )
      : null;
    imgWrapperBefore.src = binding.value.before;

    const imgWrapperAfter = document.createElement("img");
    imgWrapperAfter.classList.add("img-wrapper", "after");
    binding.value.customClassSuffix
      ? imgWrapperAfter.classList.add(
          "after-" + binding.value.customClassSuffix
        )
      : null;
    imgWrapperAfter.src = binding.value.after;

    const slider = document.createElement("div");
    binding.value.direction === "vertical"
      ? slider.classList.add("slider", "slider-vertical")
      : slider.classList.add("slider");

    const handle = document.createElement("div");
    handle.classList.add("handle");
    handle.innerHTML =
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -3 16 6"> <path d="M -5 -2 L -7 0 L -5 2 M 5 -2 L 7 0 L 5 2" fill="none" vector-effect="non-scaling-stroke"></path> </svg>';
    if (binding.value.direction === "vertical") {
      handle.classList.add("handle-vertical");
    }
    slider.appendChild(handle);
    container.appendChild(imgWrapperBefore);
    container.appendChild(imgWrapperAfter);
    container.appendChild(slider);

    el.appendChild(container);

    let isSliding = false;

    const debounce = (func, delay) => {
      if (binding.value.direction === "vertical") {
        let timeoutX, timeoutY;

        return function () {
          const context = this;
          const args = arguments;

          // Use different timeouts for vertical and vertical sliders
          if (args[0].type === "mousemove") {
            clearTimeout(timeoutX);
            timeoutX = setTimeout(() => func.apply(context, args), delay);
          } else if (args[0].type === "mousedown") {
            clearTimeout(timeoutY);
            timeoutY = setTimeout(() => func.apply(context, args), delay);
          }
        };
      } else {
        let timeout;
        return function () {
          const context = this;
          const args = arguments;

          clearTimeout(timeout);
          timeout = setTimeout(() => func.apply(context, args), delay);
        };
      }
    };

    const startSlide = () => {
      isSliding = true;
    };

    const endSlide = () => {
      isSliding = false;
    };

    const setSliderPosition = (percentage) => {
      if (binding.value.direction === "vertical") {
        slider.style.top = `${percentage}%`;
        // slider.style.transform = `translate(-50%, -50%) rotate(90deg)`;
        imgWrapperBefore.style.clipPath = `inset(${percentage}% 0 0 0)`;
        imgWrapperAfter.style.clipPath = `inset(0 0  ${100 - percentage}% 0)`;
      } else {
        slider.style.left = `${percentage}%`;
        imgWrapperBefore.style.clipPath = `inset(0 ${100 - percentage}% 0 0)`;
        imgWrapperAfter.style.clipPath = `inset(0 0 0 ${percentage}%)`;
        // inset(0 0 50% 0);
      }
    };

    if (binding.value.sliderValue) {
      setSliderPosition(binding.value.sliderValue);
    } else {
      setSliderPosition(50);
    }

    const slide = debounce((e) => {
      if (isSliding) {
        const rect = container.getBoundingClientRect();
        let position;

        if (binding.value.direction === "vertical") {
          position = e.clientY - rect.top;
          position = Math.max(0, Math.min(position, rect.height));

          const percentage = (position / rect.height) * 100;
          setSliderPosition(percentage);
        } else {
          position = e.pageX - rect.left;
          position = Math.max(0, Math.min(position, rect.width));

          const percentage = (position / rect.width) * 100;
          setSliderPosition(percentage);
        }
      }
    }, 10);

    const touchSlide = (e) => {
      const rect = container.getBoundingClientRect();
      let position;

      if (binding.value.direction === "vertical") {
        // disabler horizontal scroll
        e.preventDefault();
        position = e.touches[0].clientY - rect.top;
        position = Math.max(0, Math.min(position, rect.height));

        const percentage = (position / rect.height) * 100;
        setSliderPosition(percentage);
      } else {
        position = e.touches[0].pageX - rect.left;
        position = Math.max(0, Math.min(position, rect.width));

        const percentage = (position / rect.width) * 100;
        setSliderPosition(percentage);
      }
    };

    slider.addEventListener("mousedown", startSlide);
    el.addEventListener("mouseup", endSlide);
    el.addEventListener("mousemove", slide);
    // touch events
    slider.addEventListener("touchstart", startSlide);
    el.addEventListener("touchend", endSlide);
    el.addEventListener("touchmove", touchSlide);
  },
};