The making of before-after slider Vue 3 component

experimenting Vue3 new feature - CSS reactive binding
... so much fun!

BeforeAfter

random image fromloremspace

How to Use

<template>
 <before-after-slider
      img-src-before="url"
      img-src-after="url"
      thumb-height="250px"
      thumb-width="5px"
      thumb-color="#ffffff"
      center-thumb
      labelled
    />
</template>
PropsTypeOptional
img-src-beforeurl: Stringyes
img-src-beforeurl: Stringyes
thumb-heightpx: Stringyes
thumb-widthpx: Stringyes
thumb-colorHexyes
center-thumbBooleanyes
labelledBooleanyes

Code

// BeforeAfterSlider.vue
<template>
<div
    class="relative min-w-[250px] min-h-[250px] group"
    @click="handleMobileTouch()"
  >
    <div
      class="img "
      :style="{ backgroundImage: `url(${imgSrcAfter})` }"
    ></div>
    <!-- fake effect -->
    <div
      class="img foreground-img  filter grayscale-80"
      :style="{ backgroundImage: `url(${imgSrcBefore})` }"
    ></div>
    <input
      ref="sliderThumb"
      v-model="rangeValue"
      type="range"
      min="0"
      max="100"
      class="slider appearance-none bg-transparent group-hover:opacity-75 opacity-0 absolute inset-0 h-full w-full transition ease-in-out"
      @touchstart="handleMobileTouch()"
      @touchend="handleMobileLeave()"
      @blur="handleMobileLeave()"
    >
    <span v-show="labelled" class="absolute inset-0">Before</span>
    <span v-show="labelled" class="absolute right-0">After</span>

    <emojione:left-right-arrow
      v-show="centerThumb"
      ref="sliderCenterThumb"
      class="center-thumb inline-block absolute transform origin-center -translate-x-1/2 -translate-y-1/2 inset-1/2 w-8 h-8
       pointer-events-none transition ease opacity-0 group-hover:opacity-100"
    />
  </div>
</template>
<script setup lang='ts'>
defineProps({
  imgSrcBefore: {
    type: String,
    default: 'https://api.lorem.space/image/movie?w=250&h=250',
  },
  imgSrcAfter: {
    type: String,
    default: 'https://api.lorem.space/image/movie?w=250&h=250',
  },
  thumbHeight: {
    type: String,
    default: '150px',
    validator: (value: string) => /^\d+px$/.test(value),
  },
  thumbWidth: {
    type: String,
    default: '1px',
    validator: (value: string) => /^\d+px$/.test(value),
  },
  thumbColor: {
    type: String,
    default: '#fffff',
    validator: (value: string) => /^#[0-9a-f]{6}$/.test(value),
  },
  labelled: {
    type: Boolean,
    default: false,
    required: false,
  },
  centerThumb: {
    type: Boolean,
    default: false,
    required: false,
  },
})

// more props, endless possibilities

const rangeValue = ref<string>('50')
const sliderThumb = ref<HTMLElement | null>(null)
const sliderCenterThumb = ref<HTMLElement | null>(null)
const foregroundWidth = computed(() => {
  const range = rangeValue.value
  return `${range}%`
})

// mobile touch support instead of hover
const handleMobileTouch = () => {
  if (sliderThumb.value) {
    sliderThumb.value.classList.remove('opacity-0')
    sliderThumb.value.classList.add('opacity-75')
  }
}
const handleMobileLeave = () => {
  if (sliderThumb.value) {
    sliderThumb.value.classList.add('opacity-0')
    sliderThumb.value.classList.remove('opacity-75')
  }
}

</script>

Styling

here comesv-bindso magic

<style scoped>
.img {
@apply absolute inset-0 w-full h-full background-cover;
}

.foreground-img {
    width: v-bind(foregroundWidth);
}

.slider::-moz-range-thumb  {
    @apply appearance-none cursor-pointer;
    height: v-bind(thumbHeight);
    width: v-bind(thumbWidth);

}

.slider::-webkit-slider-thumb {
    @apply appearance-none cursor-pointer ;
    height: v-bind(thumbHeight);
    width: v-bind(thumbWidth);
    background-color: v-bind(thumbColor);
}

span {
    @apply p-1 text-black text-white w-min
    h-min text-xs bg-opacity-40 bg-light-600;
}

.center-thumb {
    left: v-bind(foregroundWidth);
}

Made withby leovoon