Skip to content

Instantly share code, notes, and snippets.

@whoisthisstud
Created January 18, 2025 03:46
Show Gist options
  • Select an option

  • Save whoisthisstud/7790728a712d5b6c86170c14d573409e to your computer and use it in GitHub Desktop.

Select an option

Save whoisthisstud/7790728a712d5b6c86170c14d573409e to your computer and use it in GitHub Desktop.
AlpineJS Before/After Image Slider Component
{{-- Before/After Blade Component --}}
@props([
'before_image' => null,
'after_image' => null
])
<div
x-data="beforeAfterSlider('{{ $before_image }}', '{{ $after_image }}')"
x-ref="sliderContainer"
class="relative w-full h-full aspect-video overflow-hidden select-none"
@mousemove="onMove"
@touchmove="onMove"
@mouseup.window="stopDrag"
@touchend.window="stopDrag"
>
<!-- BEFORE image behind, full width -->
<img
:src="beforeImageUrl"
alt="Before"
class="absolute inset-0 w-full h-full object-cover select-none"
draggable="false"
/>
<!-- AFTER image on top, revealed from the right -->
<div
class="absolute inset-0 overflow-hidden"
:style="`
left: ${dividerPosition}%;
width: ${100 - dividerPosition}%;
`"
>
<img
:src="afterImageUrl"
alt="After"
class="absolute inset-0 w-full h-full object-cover object-right select-none"
draggable="false"
/>
</div>
<!-- BEFORE label container (pinned to the left portion).
Clipped at dividerPosition% width. -->
<div
class="absolute top-0 bottom-0 left-0 overflow-hidden pointer-events-none"
:style="`width: ${dividerPosition}%;`"
>
<div
class="absolute top-2 left-2 p-[8px] min-w-[60px] bg-white/50 rounded font-bold text-center text-black
pointer-events-none select-none"
>
BEFORE
</div>
</div>
<!-- AFTER label container (pinned to the right portion).
Starts at dividerPosition% and spans to 100%. -->
<div
class="absolute top-0 bottom-0 pointer-events-none overflow-hidden"
:style="`
left: ${dividerPosition}%;
width: ${100 - dividerPosition}%;
`"
>
<div
class="absolute top-2 right-2 p-[8px] min-w-[60px] bg-white/50 rounded font-bold text-center text-black
pointer-events-none select-none"
>
AFTER
</div>
</div>
<!-- Vertical divider line -->
<div
class="absolute top-0 bottom-0 border-l-2 border-white"
:style="`left: ${dividerPosition}%;`"
:class="{
'opacity-100': !isDragging,
'opacity-25': isDragging,
}"
></div>
<!-- Draggable handle -->
<div
class="absolute top-1/2 w-[36px] h-[36px] bg-white rounded-full pointer-events-auto
border border-gray-300 shadow-md"
:style="`
left: calc(${dividerPosition}% - 18px);
transform: translateY(-50%);
`"
:class="{
'opacity-100': !isDragging,
'opacity-25': isDragging,
}"
@mousedown="startDrag"
@touchstart="startDrag"
>
<div class="relative w-full h-full flex justify-center items-center">
<i class="text-2xl fas fa-grip-lines-vertical text-solera-brown"></i>
</div>
</div>
</div>
export default function beforeAfterSlider(beforeImageUrl, afterImageUrl) {
return {
beforeImageUrl,
afterImageUrl,
containerWidth: 0,
dividerPosition: 50,
isDragging: false,
// Called on mousedown/touchstart on the "handle"
startDrag() {
this.isDragging = true;
},
// Called on mouseup/touchend on the window (so you can release even outside container)
stopDrag() {
this.isDragging = false;
},
onMove(e) {
if (! this.isDragging) return; // <— only drag if actively dragging
const rect = this.$refs.sliderContainer.getBoundingClientRect();
const clientX = e.type.includes('touch')
? e.touches[0].clientX
: e.clientX;
let offsetX = clientX - rect.left;
let newPosition = (offsetX / rect.width) * 100;
// Bound between 0% and 100%
if (newPosition < 0) newPosition = 0;
if (newPosition > 100) newPosition = 100;
this.dividerPosition = newPosition;
}
};
}
{{-- in your view --}}
<x-sliders.before-after-slider
:before_image="$before_image"
:after_image="$after_image"
/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment