Created
December 8, 2020 17:13
-
-
Save pixleight/9b2e0ae1fd6971f95f2612dfc215ba6d to your computer and use it in GitHub Desktop.
Tailwind CSS / Alpine.js Image Carousel with Lightbox
This file contains 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
.snap { | |
-ms-scroll-snap-type: x mandatory; | |
scroll-snap-type: x mandatory; | |
-ms-overflow-style: none; | |
scroll-behavior: smooth | |
} | |
.snap::-webkit-scrollbar { | |
display: none; | |
} | |
.snap > div { | |
scroll-snap-align: center; | |
} |
This file contains 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
window.xCarousel = function () { | |
return { | |
activeItem: 0, | |
numItems: 0, | |
lightboxOpen: false, | |
init() { | |
// Initializes how many items this slider has | |
this.numItems = this.$refs.slider.dataset.items; | |
}, | |
next() { | |
// Moves the slider items wrapper to the previous item | |
var $slider = this.$refs.slider; | |
$slider.scrollLeft = $slider.scrollLeft + ($slider.scrollWidth / this.numItems); | |
}, | |
previous() { | |
// Moves the slider items wrapper to the next item | |
var $slider = this.$refs.slider; | |
$slider.scrollLeft = $slider.scrollLeft - ($slider.scrollWidth / this.numItems); | |
}, | |
goTo(num) { | |
// Goes to a specific item | |
var $slider = this.$refs.slider; | |
$slider.scrollLeft = ($slider.scrollWidth / this.numItems * num ); | |
}, | |
setActiveItem(event) { | |
// Figures out which thumbnail should be active based on the scroll position of the slider items wrapper | |
this.activeItem = Math.round(event.target.scrollLeft / (event.target.scrollWidth / this.numItems)); | |
}, | |
toggleLightbox() { | |
// Opens the lightbox if it's not open, otherwise closes it | |
if (this.lightboxOpen) { | |
this.closeLightbox(); | |
console.log(this.lightboxOpen) | |
return; | |
} | |
// Add some styles to <body> to prevent scrolling while lightbox is open. | |
document.body.style.overflowY = 'hidden'; | |
document.body.style.height = '100vh'; | |
this.lightboxOpen = true; | |
}, | |
closeLightbox() { | |
// Closes the lightbox, removes styles from <body> | |
this.lightboxOpen = false; | |
document.body.style.overflowY = null; | |
document.body.style.height = null; | |
} | |
} | |
} |
This file contains 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
{% if images is defined and images|length %} | |
{% set componentId = 'carousel_' ~ random() %} | |
<div id="{{ componentId }}" x-data="xCarousel()" x-init="init()" class="relative"> | |
{# Start wrapper, transforms to lightbox when opened #} | |
<div x-bind:class="{ 'fixed top-0 bottom-0 right-0 z-30 left-0 w-screen h-screen overflow-hidden bg-white': lightboxOpen === true }" | |
x-on:keydown.arrow-left.debounce="previous()" | |
x-on:keydown.arrow-right.debounce="next()" | |
> | |
{# Start slider wrapper #} | |
<div class="relative overflow-x-hidden group"> | |
{# Start slider items wrapper. Opens lightbox on click; calculates active item on scroll #} | |
<div class="relative flex flex-no-wrap overflow-auto transition-all cursor-pointer snap" | |
x-ref="slider" | |
data-items="{{ images|length }}" | |
x-on:click="toggleLightbox()" | |
x-on:scroll.debounce="setActiveItem($event)" | |
> | |
{% for image in images %} | |
<div class="relative flex-shrink-0" | |
x-bind:class="{ 'w-screen h-screen': lightboxOpen, 'w-full aspect-ratio': !lightboxOpen }" | |
> | |
<div class="flex items-center justify-center" x-bind:class="{ 'w-screen h-screen': lightboxOpen, 'aspect-ratio-item': !lightboxOpen }"> | |
<img src="{{ image.url({ width: 1000 }) }}" alt="{{ image.title }}" class="block object-contain w-full h-full"> | |
</div> | |
</div> | |
{% endfor %} | |
</div> | |
{# End slider items wrapper #} | |
{# Set up prev/next button styles #} | |
{% set buttonClass = 'absolute top-0 bottom-0 flex items-center p-6 text-3xl transition-all duration-150 ease-out transform bg-white bg-opacity-50 outline-none focus:outline-none hover:bg-opacity-75 group-hover:translate-x-0' %} | |
{% set buttonDisabledClass = 'cursor-default opacity-25' %} | |
{% set buttonActiveClass = 'cursor-pointer hover:bg-opacity-75 ' %} | |
{# Previous item button. Becomes disabled if we're on the first item #} | |
<a class="{{ buttonClass }} left-0 -translate-x-full" | |
href="#{{ componentId }}" | |
role="button" | |
aria-label="Previous" | |
x-on:click.prevent="previous()" | |
x-bind:class="{ '{{ buttonDisabledClass }}': activeItem === 0, '{{ buttonActiveClass }}': activeItem > 0 }" | |
x-bind:disabled="activeItem === 0" | |
> | |
{{ svg('@webroot/images/icons/chevron-left.svg') }} | |
</a> | |
{# Next item button. Becomes disabled if we're on the last item #} | |
<a class="{{ buttonClass }} right-0 translate-x-full" | |
href="#{{ componentId }}" | |
role="button" | |
x-on:click.prevent="next()" | |
aria-label="Next" | |
x-bind:class="{ '{{ buttonDisabledClass }}': activeItem === {{ images|length - 1 }}, '{{ buttonActiveClass }}': activeItem < {{ images|length - 1 }} }" | |
x-bind:disabled="activeItem === {{ images|length - 1 }}" | |
> | |
{{ svg('@webroot/images/icons/chevron-right.svg') }} | |
</a> | |
{# Lightbox close button. Only in DOM if lightbox is open #} | |
<template x-if="lightboxOpen"> | |
<button class="absolute top-0 right-0 z-20 p-6 text-3xl text-gray-700 bg-white rounded-bl-lg shadow-lg" x-on:click="closeLightbox()" aria-label="Close"> | |
{{ svg('@webroot/images/icons/x.svg') }} | |
</button> | |
</template> | |
</div> | |
{# End slider wrapper #} | |
{# Start thumbnails wrapper #} | |
<ol class="grid grid-cols-4 gap-4 mt-4" x-show="!lightboxOpen"> | |
{% for image in images %} | |
{# Thumbnail click moves to that image #} | |
<li class="block border-4 border-transparent outline-none cursor-pointer focus:outline-none" | |
role="button" | |
aria-label="Jump to {{ image.title }}" | |
x-bind:class="{ 'border-orange-500': activeItem === {{ loop.index0 }} }" | |
x-on:click="goTo({{ loop.index0 }})" | |
> | |
<img class="block object-cover w-full h-full" src="{{ image.url({ width: 128, height: 128 }) }}" alt="{{ image.title }} Thumbnail"> | |
</li> | |
{% endfor %} | |
</ol> | |
{# End thumbnails wrapper #} | |
</div> | |
</div> | |
{% endif %} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment