Skip to content

Instantly share code, notes, and snippets.

@CodeMyUI
Created May 30, 2021 06:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save CodeMyUI/b0d019c24ab94a1b1a256d529d805288 to your computer and use it in GitHub Desktop.
Save CodeMyUI/b0d019c24ab94a1b1a256d529d805288 to your computer and use it in GitHub Desktop.
Voyage Slider | GSAP
<div class="app">
<div class="cardList">
<button class="cardList__btn btn btn--left">
<div class="icon">
<svg>
<use xlink:href="#arrow-left"></use>
</svg>
</div>
</button>
<div class="cards__wrapper">
<div class="card current--card">
<div class="card__image">
<img src="https://source.unsplash.com/Z8dtTatMVMw" alt="" />
</div>
</div>
<div class="card next--card">
<div class="card__image">
<img src="https://source.unsplash.com/9dmycbFE7mQ" alt="" />
</div>
</div>
<div class="card previous--card">
<div class="card__image">
<img src="https://source.unsplash.com/m7K4KzL5aQ8" alt="" />
</div>
</div>
</div>
<button class="cardList__btn btn btn--right">
<div class="icon">
<svg>
<use xlink:href="#arrow-right"></use>
</svg>
</div>
</button>
</div>
<div class="infoList">
<div class="info__wrapper">
<div class="info current--info">
<h1 class="text name">Highlands</h1>
<h4 class="text location">Scotland</h4>
<p class="text description">The mountains are calling</p>
</div>
<div class="info next--info">
<h1 class="text name">Machu Pichu</h1>
<h4 class="text location">Peru</h4>
<p class="text description">Adventure is never far away</p>
</div>
<div class="info previous--info">
<h1 class="text name">Chamonix</h1>
<h4 class="text location">France</h4>
<p class="text description">Let your dreams come true</p>
</div>
</div>
</div>
<div class="app__bg">
<div class="app__bg__image current--image">
<img src="https://source.unsplash.com/Z8dtTatMVMw" alt="" />
</div>
<div class="app__bg__image next--image">
<img src="https://source.unsplash.com/9dmycbFE7mQ" alt="" />
</div>
<div class="app__bg__image previous--image">
<img src="https://source.unsplash.com/m7K4KzL5aQ8" alt="" />
</div>
</div>
</div>
<div class="loading__wrapper">
<div class="loader--text">Loading...</div>
<div class="loader">
<span></span>
</div>
</div>
<svg class="icons" style="display: none;">
<symbol id="arrow-left" xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'>
<polyline points='328 112 184 256 328 400'
style='fill:none;stroke:#fff;stroke-linecap:round;stroke-linejoin:round;stroke-width:48px' />
</symbol>
<symbol id="arrow-right" xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'>
<polyline points='184 112 328 256 184 400'
style='fill:none;stroke:#fff;stroke-linecap:round;stroke-linejoin:round;stroke-width:48px' />
</symbol>
</svg>
<div class="support">
<a href="https://twitter.com/DevLoop01" target="_blank"><i class="fab fa-twitter-square"></i></a>
<a href="https://dribbble.com/devloop01" target="_blank"><i class="fab fa-dribbble"></i></a>
</div>
console.clear();
const { gsap, imagesLoaded } = window;
const buttons = {
prev: document.querySelector(".btn--left"),
next: document.querySelector(".btn--right"),
};
const cardsContainerEl = document.querySelector(".cards__wrapper");
const appBgContainerEl = document.querySelector(".app__bg");
const cardInfosContainerEl = document.querySelector(".info__wrapper");
buttons.next.addEventListener("click", () => swapCards("right"));
buttons.prev.addEventListener("click", () => swapCards("left"));
function swapCards(direction) {
const currentCardEl = cardsContainerEl.querySelector(".current--card");
const previousCardEl = cardsContainerEl.querySelector(".previous--card");
const nextCardEl = cardsContainerEl.querySelector(".next--card");
const currentBgImageEl = appBgContainerEl.querySelector(".current--image");
const previousBgImageEl = appBgContainerEl.querySelector(".previous--image");
const nextBgImageEl = appBgContainerEl.querySelector(".next--image");
changeInfo(direction);
swapCardsClass();
removeCardEvents(currentCardEl);
function swapCardsClass() {
currentCardEl.classList.remove("current--card");
previousCardEl.classList.remove("previous--card");
nextCardEl.classList.remove("next--card");
currentBgImageEl.classList.remove("current--image");
previousBgImageEl.classList.remove("previous--image");
nextBgImageEl.classList.remove("next--image");
currentCardEl.style.zIndex = "50";
currentBgImageEl.style.zIndex = "-2";
if (direction === "right") {
previousCardEl.style.zIndex = "20";
nextCardEl.style.zIndex = "30";
nextBgImageEl.style.zIndex = "-1";
currentCardEl.classList.add("previous--card");
previousCardEl.classList.add("next--card");
nextCardEl.classList.add("current--card");
currentBgImageEl.classList.add("previous--image");
previousBgImageEl.classList.add("next--image");
nextBgImageEl.classList.add("current--image");
} else if (direction === "left") {
previousCardEl.style.zIndex = "30";
nextCardEl.style.zIndex = "20";
previousBgImageEl.style.zIndex = "-1";
currentCardEl.classList.add("next--card");
previousCardEl.classList.add("current--card");
nextCardEl.classList.add("previous--card");
currentBgImageEl.classList.add("next--image");
previousBgImageEl.classList.add("current--image");
nextBgImageEl.classList.add("previous--image");
}
}
}
function changeInfo(direction) {
let currentInfoEl = cardInfosContainerEl.querySelector(".current--info");
let previousInfoEl = cardInfosContainerEl.querySelector(".previous--info");
let nextInfoEl = cardInfosContainerEl.querySelector(".next--info");
gsap.timeline()
.to([buttons.prev, buttons.next], {
duration: 0.2,
opacity: 0.5,
pointerEvents: "none",
})
.to(
currentInfoEl.querySelectorAll(".text"),
{
duration: 0.4,
stagger: 0.1,
translateY: "-120px",
opacity: 0,
},
"-="
)
.call(() => {
swapInfosClass(direction);
})
.call(() => initCardEvents())
.fromTo(
direction === "right"
? nextInfoEl.querySelectorAll(".text")
: previousInfoEl.querySelectorAll(".text"),
{
opacity: 0,
translateY: "40px",
},
{
duration: 0.4,
stagger: 0.1,
translateY: "0px",
opacity: 1,
}
)
.to([buttons.prev, buttons.next], {
duration: 0.2,
opacity: 1,
pointerEvents: "all",
});
function swapInfosClass() {
currentInfoEl.classList.remove("current--info");
previousInfoEl.classList.remove("previous--info");
nextInfoEl.classList.remove("next--info");
if (direction === "right") {
currentInfoEl.classList.add("previous--info");
nextInfoEl.classList.add("current--info");
previousInfoEl.classList.add("next--info");
} else if (direction === "left") {
currentInfoEl.classList.add("next--info");
nextInfoEl.classList.add("previous--info");
previousInfoEl.classList.add("current--info");
}
}
}
function updateCard(e) {
const card = e.currentTarget;
const box = card.getBoundingClientRect();
const centerPosition = {
x: box.left + box.width / 2,
y: box.top + box.height / 2,
};
let angle = Math.atan2(e.pageX - centerPosition.x, 0) * (35 / Math.PI);
gsap.set(card, {
"--current-card-rotation-offset": `${angle}deg`,
});
const currentInfoEl = cardInfosContainerEl.querySelector(".current--info");
gsap.set(currentInfoEl, {
rotateY: `${angle}deg`,
});
}
function resetCardTransforms(e) {
const card = e.currentTarget;
const currentInfoEl = cardInfosContainerEl.querySelector(".current--info");
gsap.set(card, {
"--current-card-rotation-offset": 0,
});
gsap.set(currentInfoEl, {
rotateY: 0,
});
}
function initCardEvents() {
const currentCardEl = cardsContainerEl.querySelector(".current--card");
currentCardEl.addEventListener("pointermove", updateCard);
currentCardEl.addEventListener("pointerout", (e) => {
resetCardTransforms(e);
});
}
initCardEvents();
function removeCardEvents(card) {
card.removeEventListener("pointermove", updateCard);
}
function init() {
let tl = gsap.timeline();
tl.to(cardsContainerEl.children, {
delay: 0.15,
duration: 0.5,
stagger: {
ease: "power4.inOut",
from: "right",
amount: 0.1,
},
"--card-translateY-offset": "0%",
})
.to(cardInfosContainerEl.querySelector(".current--info").querySelectorAll(".text"), {
delay: 0.5,
duration: 0.4,
stagger: 0.1,
opacity: 1,
translateY: 0,
})
.to(
[buttons.prev, buttons.next],
{
duration: 0.4,
opacity: 1,
pointerEvents: "all",
},
"-=0.4"
);
}
const waitForImages = () => {
const images = [...document.querySelectorAll("img")];
const totalImages = images.length;
let loadedImages = 0;
const loaderEl = document.querySelector(".loader span");
gsap.set(cardsContainerEl.children, {
"--card-translateY-offset": "100vh",
});
gsap.set(cardInfosContainerEl.querySelector(".current--info").querySelectorAll(".text"), {
translateY: "40px",
opacity: 0,
});
gsap.set([buttons.prev, buttons.next], {
pointerEvents: "none",
opacity: "0",
});
images.forEach((image) => {
imagesLoaded(image, (instance) => {
if (instance.isComplete) {
loadedImages++;
let loadProgress = loadedImages / totalImages;
gsap.to(loaderEl, {
duration: 1,
scaleX: loadProgress,
backgroundColor: `hsl(${loadProgress * 120}, 100%, 50%`,
});
if (totalImages == loadedImages) {
gsap.timeline()
.to(".loading__wrapper", {
duration: 0.8,
opacity: 0,
pointerEvents: "none",
})
.call(() => init());
}
}
});
});
};
waitForImages();
<script src="https://unpkg.com/imagesloaded@4/imagesloaded.pkgd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.3/gsap.min.js"></script>
@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@500;600;700;800&display=swap");
:root {
--card-width: 200px;
--card-height: 300px;
--card-transition-duration: 800ms;
--card-transition-easing: ease;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.787);
overflow: hidden;
}
button {
border: none;
background: none;
cursor: pointer;
&:focus {
outline: none;
border: none;
}
}
.app {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
&__bg {
position: absolute;
width: 100%;
height: 100%;
z-index: -5;
filter: blur(8px);
pointer-events: none;
user-select: none;
overflow: hidden;
&::before {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #000;
z-index: 1;
opacity: 0.8;
}
&__image {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) translateX(var(--image-translate-offset, 0));
width: 180%;
height: 180%;
transition: transform 1000ms ease, opacity 1000ms ease;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
&.current--image {
opacity: 1;
--image-translate-offset: 0;
}
&.previous--image,
&.next--image {
opacity: 0;
}
&.previous--image {
--image-translate-offset: -25%;
}
&.next--image {
--image-translate-offset: 25%;
}
}
}
}
.cardList {
position: absolute;
width: calc(3 * var(--card-width));
height: auto;
&__btn {
--btn-size: 35px;
width: var(--btn-size);
height: var(--btn-size);
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 100;
&.btn--left {
left: -5%;
}
&.btn--right {
right: -5%;
}
.icon {
width: 100%;
height: 100%;
svg {
width: 100%;
height: 100%;
}
}
}
.cards__wrapper {
position: relative;
width: 100%;
height: 100%;
perspective: 1000px;
}
}
.card {
--card-translateY-offset: 100vh;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) translateX(var(--card-translateX-offset))
translateY(var(--card-translateY-offset)) rotateY(var(--card-rotation-offset))
scale(var(--card-scale-offset));
display: inline-block;
width: var(--card-width);
height: var(--card-height);
transition: transform var(--card-transition-duration)
var(--card-transition-easing);
user-select: none;
&::before {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #000;
z-index: 1;
transition: opacity var(--card-transition-duration)
var(--card-transition-easing);
opacity: calc(1 - var(--opacity));
}
&__image {
position: relative;
width: 100%;
height: 100%;
img {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
}
&.current--card {
--current-card-rotation-offset: 0;
--card-translateX-offset: 0;
--card-rotation-offset: var(--current-card-rotation-offset);
--card-scale-offset: 1.2;
--opacity: 0.8;
}
&.previous--card {
--card-translateX-offset: calc(-1 * var(--card-width) * 1.1);
--card-rotation-offset: 25deg;
}
&.next--card {
--card-translateX-offset: calc(var(--card-width) * 1.1);
--card-rotation-offset: -25deg;
}
&.previous--card,
&.next--card {
--card-scale-offset: 0.9;
--opacity: 0.4;
}
}
.infoList {
position: absolute;
width: calc(3 * var(--card-width));
height: var(--card-height);
pointer-events: none;
.info__wrapper {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: flex-start;
align-items: flex-end;
perspective: 1000px;
transform-style: preserve-3d;
}
}
.info {
margin-bottom: calc(var(--card-height) / 8);
margin-left: calc(var(--card-width) / 1.5);
transform: translateZ(2rem);
transition: transform var(--card-transition-duration)
var(--card-transition-easing);
.text {
position: relative;
font-family: "Montserrat";
font-size: calc(var(--card-width) * var(--text-size-offset, 0.2));
white-space: nowrap;
color: #fff;
width: fit-content;
}
.name,
.location {
text-transform: uppercase;
}
.location {
font-weight: 800;
}
.location {
--mg-left: 40px;
--text-size-offset: 0.12;
font-weight: 600;
margin-left: var(--mg-left);
margin-bottom: calc(var(--mg-left) / 2);
padding-bottom: 0.8rem;
&::before,
&::after {
content: "";
position: absolute;
background: #fff;
left: 0%;
transform: translate(calc(-1 * var(--mg-left)), -50%);
}
&::before {
top: 50%;
width: 20px;
height: 5px;
}
&::after {
bottom: 0;
width: 60px;
height: 2px;
}
}
.description {
--text-size-offset: 0.065;
font-weight: 500;
}
&.current--info {
opacity: 1;
display: block;
}
&.previous--info,
&.next--info {
opacity: 0;
display: none;
}
}
.loading__wrapper {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #000;
z-index: 200;
.loader--text {
color: #fff;
font-family: "Montserrat";
font-weight: 500;
margin-bottom: 1.4rem;
}
.loader {
position: relative;
width: 200px;
height: 2px;
background: rgba(255, 255, 255, 0.25);
span {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgb(255, 0, 0);
transform: scaleX(0);
transform-origin: left;
}
}
}
@media only screen and (min-width: 800px) {
:root {
--card-width: 250px;
--card-height: 400px;
}
}
.support {
position: absolute;
right: 10px;
bottom: 10px;
padding: 10px;
display: flex;
a {
margin: 0 10px;
color: #fff;
font-size: 1.8rem;
backface-visibility: hidden;
transition: all 150ms ease;
&:hover {
transform: scale(1.1);
}
}
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.1/css/all.min.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Montserrat&amp;display=swap"rel="stylesheet" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment