I'm trying to add a transition to heroTitle and heroBtn but it doesn't seem to trigger, if I console.log ''element.current'' it shows that the styles have been updated but the current element being rendered by react doesn't seem to change.
I'm trying to add an animation similar to the doomsday hero-slider__hero-title https://doomsdayco.com/. Where the title slides from bottom, and the button fades in. But it seems impossible as the styles are never updated, so no animation happens.
Ref.current updates
DOM doesn't update
React jsx
import { useState, useEffect, useRef } from "react";
import '../../styles/home.css'
import useSwipe from "../../hooks/useSwipe";
const HeroSlider = () => {
const [imgIndex, setImgIndex] = useState(0);
const intervalTim = useRef(null);
const progressRef = useRef(null);
const heroTitle = useRef(null);
const heroBtn = useRef(null);
const autoplay = false;
const heroImgs = [
'https://placehold.co/1920x1080?text=1',
'https://placehold.co/1920x1080?text=2',
'https://placehold.co/1920x1080?text=3'
]
function showPrevImage() {
setImgIndex((index) => index === 0 ? heroImgs.length - 1 : index - 1);
}
function showNextImage() {
setImgIndex((index) => index === heroImgs.length - 1 ? 0 : index + 1);
}
const { handleTouchStart, handleTouchMove, handleTouchEnd } = useSwipe(showNextImage, showPrevImage);
console.log(imgIndex)
const resetProgressBar = () => {
if (progressRef.current) {
progressRef.current.style.transition = 'none';
progressRef.current.style.width = '0%';
setTimeout(() => {
progressRef.current.style.transition = 'width 5s linear';
progressRef.current.style.width = '105%';
}, 25); // Delay to allow the browser to reset the width
}
};
// This should work
// It's supposed to run on render but the styles don't update
useEffect(() => {
heroTitle.current.style.bottom = 0;
heroBtn.current.style.opacity = 1;
}, [])
useEffect(() => {
//resetProgressBar();
console.log('interval render')
intervalTim.current = autoplay && setInterval(() => {
showNextImage();
resetProgressBar();
}, 5000);
return () => {
console.log('clear')
clearInterval(intervalTim.current)
};
}, [imgIndex]);
return (
<section key={test}>
<div className="hero-slider">
<div
className="hero-slider__main-img-slider-container"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{heroImgs.map((imgUrl, i) => (
<div
aria-hidden={imgIndex !== i}
style={{transform: `translateX(${-100 * imgIndex}%)`}}
key={imgUrl}
className="hero-slider__main-img-container"
>
<img
className="hero-slider__main-img"
src={imgUrl}
alt={`img ${i + 1}`}
/>
<div className="hero-slider__hero-text-container">
<div className="hero-slider__hero-text">
<div className="hero-slider__hero-title-container">
// HeroTitle ref
<h1 ref={heroTitle} className="hero-slider__hero-title">Title</h1>
</div>
// HeroButton ref
<button ref={heroBtn} className="hero-slider__hero-btn">shop</button>
</div>
</div>
</div>
))}
</div>
<div className="hero-slider__navigation-btns-container">
<ul className="hero-slider__navigation-btns-list">
{heroImgs.map((_, i) => (
<li key={i}>
<button
aria-label={`Show image ${i + 1}`}
style={i === imgIndex ? {backgroundColor: 'black'} : {backgroundColor: 'white'}}
className="hero-slider__navigation-btn"
onClick={() => {
setImgIndex(i)
}}>
</button>
</li>
))}
</ul>
</div>
<div className="hero-slider__arrow-btns-container">
<button aria-label="Show previous image." className="hero-slider__prev-btn" onClick={showPrevImage}>left</button>
<button aria-label="Show next image." className="hero-slider__next-btn" onClick={showNextImage}>right</button>
</div>
<div className="hero-slider__progress-bar-container">
<div className="hero-slider__progress-bar" ref={progressRef}></div>
</div>
</div>
</section>
);
}
export default HeroSlider;
CSS
.hero-slider {
width: 100%;
height: 70vh;
position: relative;
margin: 0 0 50px;
font-size: 1rem;
}
.hero-slider__main-img-slider-container {
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
}
.hero-slider__main-img-container {
flex-shrink: 0;
flex-grow: 0;
width: 100%;
height: 100%;
position: relative;
transition: transform 0.3s ease-in-out;
}
.hero-slider__main-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* ELEMENTS TO ANIMATE */
.hero-slider__hero-text-container {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 0 0 60px;
}
.hero-slider__hero-text {
max-width: 1300px;
margin: 0 auto;
padding: 0 40px;
border: 2px solid blue;
}
.hero-slider__hero-title-container {
border: 2px solid lightseagreen;
position: relative;
height: 100px;
}
/* HERO BUTTON AND HERO TITLE */
.hero-slider__hero-title {
font-size: 4em;
position: absolute;
bottom: -100px;
transition: bottom 1s;
}
.hero-slider__hero-btn {
background-color: black;
color: white;
padding: 10px;
font-size: 1.25em;
opacity: 0;
transition: opacity 1s;
}
/* END OF ELEMENTS TO ANIMATE */
.hero-slider__arrow-btns-container {
position: absolute;
bottom: -20px;
right: 40px;
z-index: 3;
}
.hero-slider__navigation-btns-container {
position: absolute;
bottom: 10px;
left: 0;
right: 0;
z-index: 2;
}
.hero-slider__navigation-btns-list {
max-width: 1300px;
margin: 0 auto;
padding: 0 40px;
display: flex;
gap: 10px;
list-style: none;
}
.hero-slider__navigation-btn {
border: 2px solid #000;
width: 15px;
height: 15px;
border-radius: 50%;
}
.hero-slider__navigation-btn:focus-visible {
outline: 1px solid #000;
}
.hero-slider__navigation-btn:is(:hover, :focus-visible) {
transform: scale(1.1);
}
.hero-slider__prev-btn,
.hero-slider__next-btn {
background-color: black;
color: white;
padding: 10px;
}
.hero-slider__prev-btn {
margin-right: 10px;
}
.hero-slider__arrow-btns-container button:hover {
background-color: red;
}
.hero-slider__progress-bar-container {
overflow: hidden;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
z-index: 1;
height: 50px;
}
.hero-slider__progress-bar {
position: absolute;
bottom: 0;
left: 0;
height: 3px;
background-color: rgba(255, 255, 255, 0.8);
width: 0;
transition: width 5s linear;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
}
.hero-slider__progress-bar-inital {
transition: none;
width: 0%;
}
.hero-slider__progress-bar-animation {
transition: width 5s linear;
width: 105%;
}
@media screen and (max-width: 750px) {
.hero-slider__navigation-btns-list {
padding: 0 20px;
}
.hero-slider__hero-text {
padding: 0 20px;
}
}
I've tried adding a timeout to account for the DOM rendering, setting an initial style for each element and setting a useState variable that changed onClick and forced the useEffect to render by adding it to the conditions array.
But none of them added any styles to the element, neither were able to trigger the transition.
1. Adding a timeout
useEffect(() => {
setTimeout(() => {
heroTitle.current.style.bottom = '0';
heroBtn.current.style.opacity = '1';
}, 100);
}, []);
2. Adding a State
const [test, setTest] = useState(0);
useEffect(() => {
setTimeout(() => {
heroTitle.current.style.bottom = '0';
heroBtn.current.style.opacity = '1';
}, 100);
}, [test]);
// Added click event to image container to test the state theory
<div
onClick={() => {setTest((prev) => prev + 1)}
className="hero-slider__main-img-slider-container"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
3. Also setting an initial style
useEffect(() => {
// Set initial state
heroTitle.current.style.bottom = '-100px';
heroBtn.current.style.opacity = '0';
// Set final state after a short delay
setTimeout(() => {
heroTitle.current.style.bottom = '0';
heroBtn.current.style.opacity = '1';
}, 100);
}, []);