OiO.lk Community platform!

Oio.lk is an excellent forum for developers, providing a wide range of resources, discussions, and support for those in the developer community. Join oio.lk today to connect with like-minded professionals, share insights, and stay updated on the latest trends and technologies in the development field.
  You need to log in or register to access the solved answers to this problem.
  • You have reached the maximum number of guest views allowed
  • Please register below to remove this limitation

React doesn't trigger animation on position absolute element

  • Thread starter Thread starter Angel
  • Start date Start date
A

Angel

Guest
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

Ref.current updates

DOM doesn't update

DOM doesn't update

React jsx

Code:
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

Code:
.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

Code:
  useEffect(() => {
    setTimeout(() => {
      heroTitle.current.style.bottom = '0';
      heroBtn.current.style.opacity = '1';
    }, 100);
  }, []);

2. Adding a State

Code:
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

Code:
  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);
  }, []);
<p>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.</p>
<p>I'm trying to add an animation similar to the doomsday hero-slider__hero-title <a href="https://doomsdayco.com/" rel="nofollow noreferrer">https://doomsdayco.com/</a>. 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.</p>
<p><strong>Ref.current updates</strong></p>
<p><a href="https://i.sstatic.net/bZC1xceU.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/bZC1xceU.jpg" alt="Ref.current updates" /></a></p>
<p><strong>DOM doesn't update</strong></p>
<p><a href="https://i.sstatic.net/lQsA2bq9.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/lQsA2bq9.jpg" alt="DOM doesn't update" /></a></p>
<p><strong>React jsx</strong></p>
<pre><code>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;
</code></pre>
<p><strong>CSS</strong></p>
<pre class="lang-css prettyprint-override"><code>.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;
}
}
</code></pre>
<p>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.</p>
<p>But none of them added any styles to the element, neither were able to trigger the transition.</p>
<p><strong>1. Adding a timeout</strong></p>
<pre><code> useEffect(() => {
setTimeout(() => {
heroTitle.current.style.bottom = '0';
heroBtn.current.style.opacity = '1';
}, 100);
}, []);
</code></pre>
<p><strong>2. Adding a State</strong></p>
<pre><code>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}
>
</code></pre>
<p><strong>3. Also setting an initial style</strong></p>
<pre><code> 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);
}, []);
</code></pre>
Continue reading...
 

Latest posts

H
Replies
0
Views
1
haifisch123
H
A
Replies
0
Views
1
Adrian-Mihai Enache
A
H
Replies
0
Views
1
Hür Doğan ÜNLÜ
H
Top