0

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

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);
  }, []);

1 Answer 1

0

The problem was that I had more than one button, and react got the reference of the last one and animated it. What I did to solve it; getting the reference of them all, buttons and titles. And then, adding styles on render for each one using ref and an array.

JSX

const heroTitle = useRef([]);
const heroBtn = useRef([]);

useEffect(() => {

  heroTitle.current.forEach((lm) => {
    lm.style.bottom = 0;
  })

  heroBtn.current.forEach((lm) => {
    lm.style.opacity = 1;
  })

}, []);


return (
  <div className="hero-slider__hero-text-container">
    <div className="hero-slider__hero-text">
      <div className="hero-slider__hero-title-container">
        <h1 ref={(el) => {
          heroTitle.current[i] = el;
        }} className="hero-slider__hero-title">Title</h1>
      </div>
      <button ref={(el) => {
        heroBtn.current[i] = el;
      }} className="hero-slider__hero-btn">shop</button>
    </div>
  </div>
);

So, after a day I've just discovered that I was just overcomplicating myself. I've ended up using the ImgIndex from the slider to generate the styles for each hero title component, only the current active component positioned on the current slide has an animation and the others can't even be tabbed, which is how I discovered this, tabbing. With the last implementation if you tabbed it went to the hidden buttons also. But now works fine, finally!

I've also refactored it into its own component, separated from HeroSlider. Furthermore, added timeouts to make it look exactly like doomsday's one.

Improved JSX code

import { Link } from "react-router-dom";
import { useEffect, useState } from "react";

const HeroText = ({ i, right, title, btnText, imgIndex }) => {
  const [isTitleStyled, setIsTitleStyled] = useState(false);
  const [isButtonStyled, setIsButtonStyled] = useState(false);

  useEffect(() => {
    const titleTim = setTimeout(() => {
      setIsTitleStyled(true);
    }, 500);

    const buttonTim = setTimeout(() => {
      setIsButtonStyled(true);
    }, 1500);

    return () => {
      clearTimeout(titleTim);
      clearTimeout(buttonTim);
      setIsTitleStyled(false);
      setIsButtonStyled(false);
    }
  }, [imgIndex])
  
  return ( 
    <div className="hero-slider__hero-text-container">
      <div className={right ? 'hero-slider__hero-text hero-slider__hero-text-right' : 'hero-slider__hero-text'}>
        <div className="hero-slider__hero-title-container">
          <h1 
            style={imgIndex === i && isTitleStyled ? {bottom: 0} : {visibility: 'hidden'}} 
            className={right ? 'hero-slider__hero-title hero-slider__hero-title-right' : 'hero-slider__hero-title'}
          >
            {title.toUpperCase()}
          </h1>
        </div>
        <Link 
          style={imgIndex === i && isButtonStyled ? {opacity: 1} : {visibility: 'hidden'}} 
          className="hero-slider__hero-btn" to="/jackets" 
        >
          {btnText.toUpperCase()}
        </Link>
      </div>
    </div>
  );
}
 
export default HeroText;
2
  • Why did you use JSX style instead of simply adding classes that apply the animation styles?
    – Beauceron
    Commented Jun 5 at 17:42
  • I've been learning react for the past month, if there is a better way I'll be glad to know. I have had finnally used the image index as key to change each element styles, it works pretty much as doomsday slider. And it changes every time the imgIndex changes so I only have to call showNextImage.
    – Angel
    Commented Jun 6 at 8:47

Not the answer you're looking for? Browse other questions tagged or ask your own question.