I do apologize for such a question, I have tried on my own before asking but failing to achieve the desired effect, first of all here is a working example with vanillajs
https://stackblitz.com/edit/vitejs-vite-tr68pgsl?file=index.html,src%2Fmain.js,src%2Fstyle.css
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app">
<div class="slider">
<div class="slider-images">
<ul>
<li class="slider-item">
<img src="/imgs/1.jpg" alt="Image 1">
</li>
<li class="slider-item">
<img src="/imgs/2.jpg" alt="Image 2">
</li>
<li class="slider-item">
<img src="/imgs/3.jpg" alt="Image 3">
</li>
</ul>
</div>
<!-- slider title -->
<div class="slider-title">
<ul>
<li>the revival ensemble</li>
<li>Above the canvas</li>
<li>Harmony in every note</li>
</ul>
</div>
<!-- slider counter -->
<div class="slider-counter">
<div class="counter">
<p>1</p>
<p>2</p>
<p>3</p>
</div>
<div>
<p>—</p>
</div>
<div>
<p>3</p>
</div>
</div>
<!-- slider preview -->
<div class="slider-preview">
<div class="preview">
<img src="/imgs/1.jpg" alt="">
</div>
<div class="preview">
<img src="/imgs/2.jpg" alt="">
</div>
<div class="preview">
<img src="/imgs/3.jpg" alt="">
</div>
</div>
<!-- slider indicators -->
<div class="slider-indicators">
<p>+</p>
<p>+</p>
</div>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
html,
body {
width: 100%;
height: 100%;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
a,
p {
text-decoration: none;
color: #fff;
font-size: 14px;
}
.slider {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.slider-images {
position: absolute;
width: 100%;
height: 100%;
}
.slider-item {
position: absolute;
width: 100%;
height: 100%;
list-style: none;
}
.slider-counter {
position: absolute;
bottom: 2em;
left: 50%;
transform: translateX(-50%);
height: 24px;
display: flex;
gap: 0.5em;
overflow: hidden;
}
.slider-counter > div {
flex: 1;
}
.slider-counter p {
line-height: 20px;
}
.counter {
position: relative;
top: 0;
will-change: transform;
}
.slider-title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 64px;
overflow: hidden;
color: #fff;
}
.slider-title ul {
text-align: center;
}
.slider-title li {
font-size: 50px;
line-height: 60px;
}
.slider-indicators {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 75%;
display: flex;
justify-content: space-between;
}
.slider-indicators p {
font-size: 40px;
font-weight: 200;
}
.slider-preview {
position: absolute;
bottom: 2em;
right: 2em;
width: 35%;
height: 50px;
display: flex;
gap: 1em;
}
.preview {
position: relative;
flex: 1;
cursor: pointer;
}
.preview::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
transition: 0.3s ease-in-out;
}
.preview.active::after {
background: rgba(0, 0, 0, 0);
}
@media (max-width: 900px) {
.slider-indicators {
width: 90%;
}
.slider-preview {
width: 90%;
bottom: 5em;
}
.slider-title li {
font-size: 30px;
}
}
js
import './style.css';
import gsap from 'gsap';
import CustomEase from 'gsap/CustomEase';
document.addEventListener('DOMContentLoaded', () => {
gsap.registerPlugin(CustomEase);
CustomEase.create(
'hop',
'M0,0 C0.071,0.505 0.192,0.726 0.318,0.852 0.45,0.984 0.504,1 1,1'
);
const sliderImages = document.querySelector('.slider-images ul');
const counter = document.querySelector('.counter');
const title = document.querySelector('.slider-title ul');
const indicators = document.querySelectorAll('.slider-indicators p');
const previews = document.querySelectorAll('.slider-preview .preview');
const totalSlides = 3;
let currentImg = 1;
let indicatorRotation = 0;
const updateCounterAndTitlePosition = () => {
const counterY = -20 * (currentImg - 1);
const titleY = -60 * (currentImg - 1);
gsap.to(counter, {
y: counterY,
duration: 1,
ease: 'hop',
});
gsap.to(title, {
y: titleY,
duration: 1.5,
ease: 'hop',
});
};
const updateActiveSlidePreview = () => {
previews.forEach((prev) => prev.classList.remove('active'));
previews[currentImg - 1].classList.add('active');
};
const animateSlide = (direction) => {
const currentSlide = sliderImages.querySelector('.slider-item:last-child');
const slideImg = document.createElement('li');
slideImg.classList.add('slider-item');
const slideImgElem = document.createElement('img');
slideImgElem.src = `/imgs/${currentImg}.jpg`;
gsap.set(slideImgElem, { x: direction === 'left' ? -300 : 300 });
slideImg.appendChild(slideImgElem);
sliderImages.appendChild(slideImg);
gsap.to(currentSlide.querySelector('img'), {
x: direction === 'left' ? 300 : -300,
duration: 1.5,
ease: 'power4.out',
});
gsap.fromTo(
slideImg,
{
clipPath: direction === 'left'
? 'polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)'
: 'polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)',
},
{
clipPath: 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)',
duration: 1.5,
ease: 'power4.out',
}
);
gsap.to(slideImgElem, {
x: 0,
duration: 1.5,
ease: 'power4.out',
});
cleanupSlides();
indicatorRotation += direction === 'left' ? -90 : 90;
gsap.to(indicators, {
rotate: indicatorRotation,
duration: 1,
ease: 'hop',
});
};
document.addEventListener('click', (event) => {
const sliderWidth = document.querySelector('.slider').clientWidth;
const clickPosition = event.clientX;
const clickedPreview = event.target.closest('.preview');
if (clickedPreview) {
const clickedIndex = Array.from(previews).indexOf(clickedPreview) + 1;
if (clickedIndex !== currentImg) {
currentImg = clickedIndex;
animateSlide(clickedIndex < currentImg ? 'left' : 'right');
updateActiveSlidePreview();
updateCounterAndTitlePosition();
}
return;
}
if (clickPosition < sliderWidth / 2 && currentImg !== 1) {
currentImg--;
animateSlide('left');
} else if (clickPosition > sliderWidth / 2 && currentImg !== totalSlides) {
currentImg++;
animateSlide('right');
}
updateActiveSlidePreview();
updateCounterAndTitlePosition();
});
const cleanupSlides = () => {
const imgElements = document.querySelectorAll('.slider-images .slider-item');
if (imgElements.length > totalSlides) {
imgElements[0].remove();
}
};
});
And here is what I’ve tried so far with next js
I have tried but the images are out of sync
and the title animation is broken
i understand that it’s lack of skills
would be grateful if someone can guide me how to fix those issues
'use client'
import { useState } from 'react';
import gsap from 'gsap';
import CustomEase from 'gsap/CustomEase';
import { useGSAP } from '@gsap/react';
const Slider = () => {
// State to manage current image and indicator rotation
const [currentImg, setCurrentImg] = useState(1);
const [indicatorRotation, setIndicatorRotation] = useState(0);
const totalSlides = 3;
// Initialize GSAP with .slider as the container
useGSAP(() => {
// Register GSAP plugin and create custom easing
gsap.registerPlugin(CustomEase);
CustomEase.create(
'hop',
'M0,0 C0.071,0.505 0.192,0.726 0.318,0.852 0.45,0.984 0.504,1 1,1'
);
}, []);
useGSAP(() => {
// Update counter and title position
updateCounterAndTitlePosition();
updateActiveSlidePreview();
}, [currentImg]);
const updateCounterAndTitlePosition = () => {
const counterY = -20 * (currentImg - 1);
const titleY = -60 * (currentImg - 1);
gsap.to('.slider .counter', {
y: counterY,
duration: 1,
ease: 'hop',
});
gsap.to('.slider .slider-title', {
y: titleY,
duration: 1.5,
ease: 'hop',
});
};
const updateActiveSlidePreview = () => {
const previews = document.querySelectorAll('.slider .preview');
previews.forEach((prev) => prev.classList.remove('active'));
previews[currentImg - 1].classList.add('active');
};
const animateSlide = (direction) => {
const sliderImages = document.querySelector('.slider .slider-images ul');
const currentSlide = sliderImages.querySelector('.slider-item:last-child');
const slideImg = document.createElement('li');
slideImg.classList.add('slider-item');
const slideImgElem = document.createElement('img');
slideImgElem.src = `/imgs/${currentImg}.jpg`;
gsap.set(slideImgElem, { x: direction === 'left' ? -300 : 300 });
slideImg.appendChild(slideImgElem);
sliderImages.appendChild(slideImg);
gsap.to(currentSlide.querySelector('img'), {
x: direction === 'left' ? 300 : -300,
duration: 1.5,
ease: 'power4.out',
});
gsap.fromTo(
slideImg,
{
clipPath: direction === 'left'
? 'polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)'
: 'polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)',
},
{
clipPath: 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)',
duration: 1.5,
ease: 'power4.out',
}
);
gsap.to(slideImgElem, {
x: 0,
duration: 1.5,
ease: 'power4.out',
});
cleanupSlides();
setIndicatorRotation((prevRotation) => prevRotation + (direction === 'left' ? -90 : 90));
gsap.to('.slider .slider-indicators', {
rotate: indicatorRotation,
duration: 1,
ease: 'hop',
});
};
const cleanupSlides = () => {
const imgElements = document.querySelectorAll('.slider .slider-item');
if (imgElements.length > totalSlides) {
imgElements[0].remove();
}
};
const handleClick = (event) => {
const sliderWidth = document.querySelector('.slider').clientWidth;
const clickPosition = event.clientX;
const clickedPreview = event.target.closest('.preview');
if (clickedPreview) {
const clickedIndex = Array.from(document.querySelectorAll('.slider .preview')).indexOf(clickedPreview) + 1;
if (clickedIndex !== currentImg) {
setCurrentImg(clickedIndex);
animateSlide(clickedIndex < currentImg ? 'left' : 'right');
updateActiveSlidePreview();
updateCounterAndTitlePosition();
}
return;
}
if (clickPosition < sliderWidth / 2 && currentImg !== 1) {
setCurrentImg(currentImg - 1);
animateSlide('left');
} else if (clickPosition > sliderWidth / 2 && currentImg !== totalSlides) {
setCurrentImg(currentImg + 1);
animateSlide('right');
}
updateActiveSlidePreview();
updateCounterAndTitlePosition();
};
return (
<div className="slider" onClick={handleClick}>
<div className="slider-images">
<ul>
{[...Array(totalSlides)].map((_, index) => (
<li key={index} className="slider-item">
<img src={`/imgs/${index + 1}.jpg`} alt={`Image ${index + 1}`} />
</li>
))}
</ul>
</div>
<div className="slider-title">
<ul>
<li>the revival ensemble</li>
<li>Above the canvas</li>
<li>Harmony in every note</li>
</ul>
</div>
<div className="slider-counter">
<div className="counter">
{[...Array(totalSlides)].map((_, index) => (
<p key={index}>{index + 1}</p>
))}
</div>
<div>
<p>—</p>
</div>
<div>
<p>{totalSlides}</p>
</div>
</div>
<div className="slider-preview">
{[...Array(totalSlides)].map((_, index) => (
<div key={index} className={`preview ${currentImg === index + 1 ? 'active' : ''}`}>
<img src={`/imgs/${index + 1}.jpg`} alt={`Preview ${index + 1}`} />
</div>
))}
</div>
<div className="slider-indicators">
{[...Array(2)].map((_, index) => (
<p key={index}>+</p>
))}
</div>
</div>
);
};
export default Slider;