Created
January 18, 2025 03:46
-
-
Save whoisthisstud/7790728a712d5b6c86170c14d573409e to your computer and use it in GitHub Desktop.
AlpineJS Before/After Image Slider Component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {{-- 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| }; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {{-- 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