Inspired by this design: https://dribbble.com/shots/6759822-Voyage-travel-website
Created
May 23, 2023 19:11
-
-
Save landsurveyorsunited/2292cbe87bd881fdb5f2d6cea54ab635 to your computer and use it in GitHub Desktop.
Voyage Slider | GSAP
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
<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://storage.ning.com/topology/rest/1.0/file/get/11129814452?profile=original" alt="" /> | |
</div> | |
</div> | |
<div class="card next--card"> | |
<div class="card__image"> | |
<img src="https://storage.ning.com/topology/rest/1.0/file/get/11129814494?profile=original" alt="" /> | |
</div> | |
</div> | |
<div class="card previous--card"> | |
<div class="card__image"> | |
<img src="https://storage.ning.com/topology/rest/1.0/file/get/11129814690?profile=original" 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">INNOVATIVE</h1> | |
<h4 class="text location">ROOFING & Solar</h4> | |
<p class="text description">America's Favorite Roofing Specialists</p> | |
</div> | |
<div class="info next--info"> | |
<h1 class="text name">RESIDENTIAL</h1> | |
<h4 class="text location">ROOFING</h4> | |
<p class="text description">Protect your Home</p> | |
</div> | |
<div class="info previous--info"> | |
<h1 class="text name">COMMERCIAL</h1> | |
<h4 class="text location">ROOFING</h4> | |
<p class="text description">Energize your business</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> |
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
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(); |
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
<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> |
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
@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); | |
} | |
} | |
} |
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
<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&display=swap"rel="stylesheet" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment