What steps should I take to address the animation issue with Framer Motion in my React application?

I've been working on replicating a similar user interface to create questions like the one found in TypeForm.

If you take a look at their editor, you'll notice that the right side scrolls smoothly up and down when using the arrows. https://i.sstatic.net/tdJVF.gif

For my application, I want to achieve a similar scrolling effect by clicking on the questions on the left instead of using arrows.

Most of it is working well, but there's a slight glitch in the animation when clicking on a question in reverse order for the first time - they overlap, causing an issue. I'm trying to ensure that the animated questions never overlap each other. Can anyone assist with this?

The current approach involves changing the initial, animate, and exit values for the animation based on whether a 'previous' question has been selected or not.

https://i.sstatic.net/hM1Pb.gif

Here is a link to a codesandbox as I am facing issues running the snippet below correctly...

import React, { useState, useEffect, useRef } from "react";

import { motion, AnimatePresence } from "framer-motion"
import uuid from 'uuid/v4';
import './styles.css';

const App = () => {
  const [display, setDisplay] = useState(false);
  const [questions, setQuestions] = useState([{id:uuid(), q: '', active:true}]);
  const [isPrevious, setIsPrevious] = useState(false);
  const [prev, setPrev] = useState(0);
  const toggleMenu = () => setDisplay(!display);
  const selectType = ()=> {
    setDisplay(false);
    setPrev(questions.findIndex(x=>x.active === true))
    setQuestions(prevState => {
      let updated = prevState.map(question=>{
        question.active = false;
        return question
      })
      updated.push({id:uuid(), q: '', active:true})
      return updated
    })
  }
  useEffect(()=>{
    setPrev(questions.findIndex(x=>x.active === true))
  },[questions])
  const updateQuestions = ({id, q}) => setQuestions(prevState => prevState.map((question, index) =>{
    question.active = false;
    if(question.id === id) {
      console.log({'prev': prev, 'current': index, 'isPrev?': isPrevious})
      if(prev > index) {
        setIsPrevious(true)
      } else {
        setIsPrevious(false)
      }
      question.q = q
      question.active = true
    } 
    return question
  }))
  const removeQuestion = (id) => setQuestions(prevState => prevState.filter(question => question.id !== id))
  return (
    <> 
      <Navbar />
      <main className="flex items-stretch justify-between">
        <div className="w-1/2">
          {questions.map(question=>
            <Qshort key={question.id} removeQuestion={removeQuestion} data={question} updateQuestion={updateQuestions}/>
          )}
          <div className="relative mt-10 px-4">
            <button onClick={toggleMenu} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Add your first question</button>
            <div className={(display ? "flex " : "hidden ") + "absolute bg-white p-3 flex-col shadow-md w-1/2 rounded-lg"}>
              <button onClick={() => selectType("1")} className="my-3 mt-0 p-3 hover:bg-gray-200 rounded">Short question</button>             
            </div>
          </div>
        </div>
        <div className="rightSide relative w-1/2 bg-gray-200 flex flex-col items-center justify-center">
          <AnimatePresence finishBeforeExit>
          {questions.length &&
              <motion.div
              transition={{ duration: 1, type: "tween" }}
                key={questions[questions.findIndex(x=>x.active)].id}
                
                initial={{opacity:0, bottom:isPrevious ? '100%' : '-50%'}} 
                animate={{opacity:1, bottom:'50%'}} 
                exit={{opacity:0, bottom:isPrevious ? '-20%' : '100%'}} 
                className="absolute flex flex-col w-64 w-2/3"
              >
              <p className="text-2xl pl-3 text-gray-600">{questions[questions.findIndex(x=>x.active)].q}</p>
              <input 
                placeholder="Type your answer here...." 
                type="text" 
                className="mt-3 w-full p-3 bg-transparent border-b-4 text-2xl"
              />
            </motion.div>
          }
          </AnimatePresence>
          {questions.length && 
            <button className="absolute mb-3 mr-3 bottom-0 right-0 bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-6 rounded">Next step</button>
          }
        </div>
      </main>
    </>
  );
};


const Qshort = ({updateQuestion,removeQuestion,data}) => {
  return (
    <div className={(data.active ? 'border-green-500 ':'border-transparent')+" border relative bg-gray-300 py-8 pb-12 pl-4 mb-2"}>
      <input 
        onFocus={(e)=>updateQuestion({id:data.id, q:e.target.value})} 
        onChange={(e)=>updateQuestion({id:data.id, q:e.target.value})} 
        type="text" 
        className="w-full bg-transparent" 
        placeholder="Type your question here..."
      />
      <button onClick={()=>removeQuestion(data.id)} className="absolute bottom-0 right-0 mr-3 mb-3 text-xs text-blue-500 hover:text-blue-700">Remove</button>
    </div> 
  );
};

const Navbar = () => {
  return (
    <nav className="relative flex items-center justify-between flex-wrap bg-blue-500 p-6">
      <div className="flex items-center flex-shrink-0 text-white mr-6">
        <span className="font-semibold text-xl tracking-tight">Aquire</span>
      </div>
      <div className="block lg:hidden">
        <button className="flex items-center px-3 py-2 border rounded text-teal-200 border-teal-400 hover:text-white hover:border-white">
          <svg
            className="fill-current h-3 w-3"
            viewBox="0 0 20 20"
            xmlns="http://www.w3.org/2000/svg"
          >
            <title>Menu</title>
            <path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z" />
          </svg>
        </button>
      </div>
    </nav>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/portfolio/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/framer-motion/dist/framer-motion.js"></script>

  <div id="root"></div>

Answer №1

Exploring new tools is always intriguing, and my first experience with framer motion has been quite interesting and cool. However, I noticed a limitation in updating the element exit property once it's been set. Hopefully, this issue will be resolved in future updates.

To address your concern, I refactored the solution using react-spring, which I find more familiar and mature. It works seamlessly with this library. If you're open to changing libraries for this project, this could be a viable option.

You can define the transition as shown below:

const transitions = useTransition(
  questions.filter(x => x.active),
  item => item.id,
  {
    from: { opacity: 0, bottom: isPrevious.current ? "100%" : "-50%" },
    enter: { opacity: 1, bottom: "50%" },
    leave: { opacity: 0, bottom: isPrevious.current ? "-20%" : "100%" }
  }
);

Here's how you can implement it:

{questions.length &&
  transitions.map(({ item, props, key }) => (
    <animated.div
      className="absolute flex flex-col w-64 w-2/3"
      key={key}
      style={props}
    >
      <p className="text-2xl pl-3 text-gray-600">{item.q}</p>
      <input
        placeholder="Type your answer here...."
        type="text"
        className="mt-3 w-full p-3 bg-transparent border-b-4 text-2xl"
      />
    </animated.div>
  ))
}

I made sure to update the reference of 'isPrevious' to sync it with the other states:

const isPrevious = useRef(false);

Check out the updated code here

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

With Crypto-JS, a fresh hash is always generated

I'm looking to integrate crypto-js into my Angular 8 application. Here is a snippet of my code: import {Injectable} from '@angular/core'; import * as CryptoJS from 'crypto-js'; @Injectable() export class HprotoService { public ...

The JSX component is unable to utilize the object

When working with Typescript in a react-three-fiber scene, I encountered an error that GroundLoadTextures cannot be used as a JSX component. My aim is to create a texture loader component that loads textures for use in other components. The issue arises f ...

Learn the steps to transform your Magento 2 store into a Progressive Web App for seamless user experience

I am currently running a Magento 2 store and I am interested in implementing the PWA feature on it. ...

A step-by-step guide on resolving the number type coerce error 50035 that occurs during command registration

I am delving into the world of creating a discord bot, and it has been a while since my last project using slash commands. However, I encountered an issue while trying to register my commands: DiscordAPIError[50035]: Invalid Form Body application_id[NUMBER ...

Tips for effectively utilizing axios without triggering the 400 (Bad Request) message on the console

In my project, I've integrated an API using axios with Ruby on Rails. The code for the apiCaller.js file is shown below: import axios from 'axios'; import { API_URL } from './../constants/config'; export default function callApi( ...

RxJs Subject is failing to activate subscriptions in the production environment, however, it functions properly in development and testing

After successfully deploying our Angular app to our test server, my team encountered an issue when trying to deploy it to the production server. While the app functions correctly in development and on the test server, the subjects are not triggering their ...

Node.js is being utilized to upload data to an FTP server with the help of js

I'm attempting to send a file to an FTP server using Node.js with the help of jsftp module.exports = async function() { const jsftp = require("jsftp"); console.log(__dirname) const ftp = new jsftp({ host: "host.name", ...

Utilizing the Redis client in NestJS: A step-by-step guide

I am currently working with a microservice in NestJs and wanted to share my main.ts file: async function bootstrap() { const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, { transport: Transport.REDIS, options: { ...

Error: Unable to call this.loadCategories function in React due to uncaught TypeError in Promise

Currently, I am working on a React application. I encountered an error in the handleCategoryUpdated function. Can you help me figure out why? The error message reads: categoryManager.jsx:22 Uncaught (in promise) TypeError: this.loadCategories is not a fu ...

Error occurred while attempting to upload a file using multipart form data: TypeError message displayed - Unable to access properties of undefined (specifically '0')

I am encountering an issue while trying to send multipart/form-data using axios from the frontend. The same post request works fine in Postman/Insomnia but fails when executed from the frontend. The backend shows the following error: node:events:505 ...

Avoid applying the active class to specific elements while scrolling

I encountered the following issue: While trying to add logic to apply the active class on list elements in my navigation bar as I scroll, I utilized this code snippet: $(window).bind('scroll', function () { var scrollPos = $(window).scrollTop ...

What is the best way to set one property to be the same as another in Angular?

In my project, I have defined a class called MyClass which I later utilize in an Angular component. export class MyClass { prop: string; constructor(val){ this.prop = val; } public getLength(str: string): number { return str.length; } ...

Issue with Vue JS Components: Button function not working on second click with @click

I've been working with Vue JS and Laravel to create a modal. The first time I press the button with @click, it works fine but the next time I encounter an error. Here's my Laravel Blade code: <div class="col-span-12 mb-5" id="a ...

Using Node.js to update information within Firebase

Here's a problem I'm facing: I have a cron job running in Node.js that sends data to a Firebase database every minute. The issue is, even when there are no changes, the database still receives information. Take a look at my code snippet below: l ...

CreateAsyncModule using an import statement from a variable

When trying to load a component using defineAsyncComponent, the component name that should be rendered is retrieved from the backend. I have created a function specifically for loading the component. const defineAsyncComponentByName = (componentName: strin ...

When attempting to utilize a global variable in a POST request, it may be found to

My dilemma is that I can successfully access the global variable in other requests such as 'GET', but it becomes undefined when used in a 'POST' request. var dirName; app.post("/addFace", function (req, res) { //create directory con ...

Issue: Incompatibility in metadata versions detected for module .../ngx-masonry/ngx-masonry.d.ts. Level 4 version identified, whereas level 3 version

When using ngx-masonry, I encountered the following error message- ERROR in Error: Metadata version mismatch for module .../ngx-masonry/ngx-masonry.d.ts, found version 4, expected 3 Specifications: Angular 4 ngx-masonry 1.1.4 ...

Is there a way to eliminate the standard mm/dd/yyyy format from a TextField in material-ui?

Using the material-ui library, I am trying to create a datetime-picker. However, I only want the label "Date" to be visible if the user does not select anything. Unfortunately, default characters like mm/dd/yyyy are showing up in the input field as seen in ...

The creation of the Angular project was unsuccessful due to the outdated circular-json dependency

After attempting to create a new Angular project with the command: ng new hello-world I encountered an error message: npm WARN deprecated <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="b5d6dcc7d6c0d9d4c798dfc6dadbf5859b809b8 ...

jQuery tooltip anchored to the left side of the screen, ignoring all attempts to adjust its position

I have a jQuery tooltip that is supposed to hover over the table header, but it is displaying on the far left side of my screen with no formatting - just black text. I have attempted to use the placement and position jQuery attributes, but they do not seem ...