30

I have an image on a React page. When the state is updated to a new image I want to perform the following transition effect:

  • The original image should zoom in and fade out
  • The new image should also zoom in and fade in

The effect should look similar to passing through a wall to a new scene.

How am I able to do this in React?

6
  • 1
    give react-pose a spin popmotion.io/pose Commented Sep 25, 2018 at 21:54
  • 2
    This is more of a CSS question. Change the className of the element, and use CSS transitions to animate it Commented Sep 25, 2018 at 22:00
  • @Mikkel Well, except that when React re-renders the page, the old image is gone and the new one is in the state. So, a proper solution would be somehow a combination between React and CSS. Possibly something to do with componentWillUpdate combined with CSS, but it's proving a bit more difficult than I thought it was going to be. Commented Sep 25, 2018 at 22:11
  • @azium Not sure if that will work for me. I'd prefer not using a third party library if I don't have to. Also, I can't seem to find any information on it being able to handle transitions between state changes. Commented Sep 25, 2018 at 22:24
  • It sounds like you're replacing the image in the state with a new image, causing React to re-render and remove the old image immediately and therefore crushing any transitions that would go with it. Try adding a new image into the state without removing the old one, then animating both images, then removing the old image from the state? Commented Sep 26, 2018 at 4:30

4 Answers 4

29
+50

As @pgsandstrom mentioned, React Transition Group is the way to go. Unfortunately, it's not very developer friendly (pretty steep learning curve).

Here's a working example: https://codesandbox.io/s/6lmv669kz

✔ Original image zooms in while fading out

✔ New image zooms in while fading in

TransitionExample.js

import random from "lodash/random";
import React, { Component } from "react";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import uuid from "uuid/v1";

const arr = [
  {
    id: uuid(),
    url: `https://loremflickr.com/600/100?lock=${random(0, 999)}`
  },
  {
    id: uuid(),
    url: `https://loremflickr.com/600/100?lock=${random(0, 999)}`
  },
  {
    id: uuid(),
    url: `https://loremflickr.com/600/100?lock=${random(0, 999)}`
  }
];

export default class TransitionExample extends Component {
  state = {
    index: 0,
    selected: arr[0]
  };

  nextImage = () =>
    this.setState(prevState => {
      const newIndex = prevState.index < arr.length - 1 ? prevState.index + 1 : 0;
      return {
        index: newIndex,
        selected: arr[newIndex]
      };
    });

  render = () => (
    <div className="app">
      <div style={{ marginBottom: 30, height: 100 }}>
        <TransitionGroup>
          <CSSTransition
            key={this.state.selected.id}
            timeout={1000}
            classNames="messageout"
          >
            <div style={{ marginTop: 20 }}>
              <img className="centered-image" src={this.state.selected.url} />
            </div>
          </CSSTransition>
        </TransitionGroup>
      </div>
      <div style={{ textAlign: "center" }}>
        <button
          className="uk-button uk-button-primary"
          onClick={this.nextImage}
        >
          Next Image
        </button>
      </div>
    </div>
  );
}

styles.css

.app {
  margin: 0 auto;
  overflow: hidden;
  width: 700px;
  height: 800px;
}

.centered-image {
  display: block;
  margin: 0 auto;
}

/* starting ENTER animation */
.messageout-enter {
  position: absolute;
  top: 0;
  left: calc(13% + 5px);
  right: calc(13% + 5px);
  opacity: 0.01;
  transform: translateY(0%) scale(0.01);
}

/* ending ENTER animation */
.messageout-enter-active {
  opacity: 1;
  transform: translateY(0%) scale(1);
  transition: all 1000ms ease-in-out;
}

/* starting EXIT animation */
.messageout-exit {
  opacity: 1;
  transform: scale(1.01);
}

/* ending EXIT animation */
.messageout-exit-active {
  opacity: 0;
  transform: scale(4);
  transition: all 1000ms ease-in-out;
}
Sign up to request clarification or add additional context in comments.

1 Comment

Please don't use an external library and consider a simple <Animate on={value} /> component.
5

It sounds like you are looking for React Transition Group. It is the "official" way of solving these issues. Specifically I think this is what you should use. It can be a bit tricky to get a hang of, but it is really nice and powerful once you understand it.

Comments

5

This worked for me (link):

index.js:

import React from "react";
import { render } from "react-dom";

import "./styles.scss";

const src1 =
  "https://www.nba.com/dam/assets/121028030322-james-harden-traded-102712-home-t1.jpg";
const src2 = "https://www.nba.com/rockets/sites/rockets/files/wcwebsite.jpg";

var state = {
  toggle: true
};

class App extends React.Component {
  render() {
    const cn1 = "imgFrame " + (state.toggle ? "toggleOut" : "toggleIn");
    const cn2 = "imgFrame " + (state.toggle ? "toggleIn" : "toggleOut");
    return (
      <div>
        <img className={cn1} src={src1} alt={"img1"} />
        <img className={cn2} src={src2} alt={"img2"} />
        <button
          onClick={() => {
            state.toggle = !state.toggle;
            this.forceUpdate();
          }}
        >
          click me to toggle
        </button>
        <h1>Hello</h1>
      </div>
    );
  }
}

render(<App />, document.getElementById("app"));

style.scss:

html,
body {
  background-color: papayawhip;
  font-family: sans-serif;

  h1 {
    color: tomato;
  }
}

@keyframes fadeout {
  0% {
    opacity: 1;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    transform: scale(0.9);
  }
}

@keyframes fadein {
  0% {
    opacity: 0;
    transform: scale(1.1);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

.toggleOut {
  animation: fadeout 500ms;
  opacity: 0;
}

.toggleIn {
  animation: fadein 500ms;
  opacity: 1;
}

.imgFrame {
  position: absolute;
  top: 10px;
  left: 10px;
  width: 200px;
  height: 200px;
}

button {
  position: absolute;
  top: 220px;
}

1 Comment

how can I handle if I have 3 or more than 2 images?
1

Wrap with a simple <Animate on={value} /> component that triggers an animation when value changes and is not undefined.

function Animate({ children, on }) {
  return (on === undefined)
    ? <div>{children}</div>
    : <div className="fade-in" key={on}>{children}</div>
}
import { useEffect, useState } from 'react'

function TestAnimate() {
  const [value, setValue] = useState() // undefined

  // update value every second
  useEffect(() => {
    setInterval(() => setValue(new Date().toLocaleString()), 1_000)
  }, [])

  return (
    <Animate on={value}>
      Value: {value}
    </Animate>
  )
}
@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.fade-in {
  animation: fadeIn 500ms ease-in-out;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.