Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create an image background fade when mapping an array of images with React

I am working on a React.js/Next.js project using a Sanity headless CMS.

I have a hero element that accepts an array of images but want to be able to cycle through this automatically to create a background slider effect. I was tempted to use a slick slider or similar but would like to try and do this with pure CSS if possible as the webpack config for the project doesn't make importing external css files very easy.

My component...

import React from "react";
import PropTypes from "prop-types";
import styles from "./Hero.module.css";
import client from "../../client";
import SimpleBlockContent from "../SimpleBlockContent";
import Cta from "../Cta";
import imageUrlBuilder from "@sanity/image-url";

const builder = imageUrlBuilder(client);

function Hero(props) {
  const { heading, image, tagline, ctas } = props;


  const images = props.image;
  return (
    <div className={styles.root}>
      <div className={styles.content}>
        <h1 className={styles.title}>{heading}</h1>
        <div className={styles.tagline}>{tagline && <SimpleBlockContent blocks={tagline} />}</div>
        <div>
          {images.map((image) => (
            <img
              className={styles.image}
              src={builder.image(image).url()}
              className={styles.image}
              alt={heading}
            />
          ))}
        </div>
      </div>
    </div>
  );
}

Hero.propTypes = {
  heading: PropTypes.string,
  backgroundImage: PropTypes.object,
  tagline: PropTypes.array,
  ctas: PropTypes.arrayOf(PropTypes.object),
};

export default Hero;

My CSS

@import "../../styles/custom-properties.css";

.root {
  composes: center from "../../styles/shared.module.css";
  position: relative;
  color: var(--color-white, #fff);
  background: none;
  padding-bottom: 2rem;

  @media (--media-min-medium) {
    padding-bottom: 1rem;
  }
}

.content {
  width: 100vw;
  min-height: 100vh;
  padding: 0 1.5em;
  box-sizing: border-box;
  z-index: 1;
  display: flex;
  justify-content: center;
  align-items: center;
}

.image {
  position: absolute;
  z-index: -1;
  left: 0;
  top: 0;
  overflow: hidden;
  display: block;
  width: 100vw;
  height: 100vh;
  object-fit: cover;
  animation: fade 2s infinite;
}

.title {
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 2px;
  position: relative;
  font-weight: 300;
  font-size: var(--font-title2-size);
  line-height: var(--font-title2-line-height);
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);
  margin: 0;
  padding: 0;

  @media (--media-min-medium) {
    font-size: var(--font-title1-size);
    line-height: var(--font-title1-line-height);
    padding-top: 0;
  }
}

@keyframes fade {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

This code does fade the first image in the array in and out but doesn't effect the other images, which are 'sitting' behind the first one.

Hope that makes sense, any help would be appreciated!

EDIT - Simplified StackBlitz Mock Up

https://stackblitz.com/edit/react-wmfcbp?

like image 308
LAT89 Avatar asked Oct 16 '22 03:10

LAT89


1 Answers

The problem is that your .image class set the same z-index and animation properties for all images.

Try to play with set delay like this:

{images.map((image, index) => (
        <img
          id={index}
          className="image"
          src={image}
          style={{
            animationDelay: `${index * 4}s`, // delay animation for next images
            zIndex: `-${index + 1}` // make images as layers, not same layer -1
            }}
        />
      ))}

But if you are using React, you could also try to make it in React by interval and hooks, and animate slides in css by applied class to active slide:

import React, { useRef, useState, useEffect } from "react";
import  "./Hero.css";

const images = [ 
 " https://images.unsplash.com/photo-1470165451736-166cb1cc909d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2552&q=80",

 "https://images.unsplash.com/photo-1537367075546-0529d021d198?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80",
"https://images.unsplash.com/photo-1590064589331-f19edc8bed34?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80"
]


function Hero(props) {
const slidePresentationTime = 3000 // after how many ms slide will change - now 3s / 3000ms
const [currentSlide, setCurrentSlide] = useState(0) // value and function to set currrent slide index
const sliderInterval = useRef() // interval ref

useEffect(() => {
  sliderInterval = setInterval(() => {
      setCurrentSlide((currentSlide + 1) % images.length); // change current slide to next slide after 'slidePresentationTime'
    }, slidePresentationTime);

    // cleanup interval when your component will unmount
    return () => {
      clearInterval(sliderInterval)
    }
})

  return (
    <div className="root">
      <div className="content">
        <h1 className="title">Title</h1>
        <div>
          {images.map((image, index) => (
            <img
              id={index}
              key={index}
              className={index === currentSlide ? 'image active' : 'image'}
              src={image}
              style={{
                zIndex: `-${index + 1}`
              }}
            />
          ))}
        </div>
      </div>
    </div>
  );
}


export default Hero;

And here it would be much easier to use transition instead of animation and just change the transition property (opacity) by class.

 //styles

.image {
  position: absolute;
  left: 0;
  top: 0;
  overflow: hidden;
  display: block;
  width: 100vw;
  height: 100vh;
  object-fit: cover;
  transition: opacity 1s ease;
  opacity: 0;
}

.active {
    opacity: 1;
}

Here is demo https://stackblitz.com/edit/react-2dd6iu

like image 118
Robson Avatar answered Oct 19 '22 00:10

Robson