reactjsanimationnext.jsframer-motion

How to animate slide transitions with framer motion


I am trying to implement a slide transition between books. I've tried Motion, but I am getting weird animations. All I am looking for is to slide each book across the x axis when ever they are being sorted or shuffled.

My Tech stack:

Here is my code:

// Page.js (parent)
"use client";

import Book from "./Book";
import { useEffect, useState } from "react";

export default function Home() {
  const [books, setBooks] = useState([]);
  const [sortOrder, setSortOrder] = useState({ title: "asc", dateFinished: "asc", year: "asc" });

  useEffect(() => {
    async function fetchBooks() {
      try {
        const response = await fetch("/books.json");
        if (!response.ok) {
          throw new Error(`Failed to fetch: ${response.statusText}`);
        }
        const data = await response.json();
      } catch (error) {
        console.error("Error fetching books:", error);
      }
    }
    fetchBooks();
  }, []);

  const sortBooks = (criteria) => {
    const order = sortOrder[criteria] === "asc" ? "desc" : "asc";
    const sortedBooks = [...books].sort((a, b) => {
      switch (criteria) {
        case "title":
          return order === "asc" ? a.title.localeCompare(b.title) : b.title.localeCompare(a.title);
        case "year":
          return order === "asc" ? a.year - b.year : b.year - a.year;
        // Add other sorting methods here
        default:
          return 0;
      }
    });
    setBooks(sortedBooks);
    setSortOrder({ ...sortOrder, [criteria]: order });
  };

  const handleSelectBook = (book) => {
    if (selectedBook === book) {
      setSelectedBook(null);
      return;
    }
    setSelectedBook(book);
  };

  const handleShuffleBooks = () => {
    setBooks(shuffleArray([...books]));
  };

  return (
    <div className="overflow-y-hidden max-h-[100vh] max-w-[100vw] overflow-hidden">
      <h1 className="text-center">My Bookshelf</h1>
      {/* buttons */}
      <div className="flex gap-2">
        <button
          onClick={() => sortBooks("title")}
        >
          Sort Alphabetically ({sortOrder.title === "asc" ? "A-Z" : "Z-A"})
        </button>
        <button
          onClick={() => sortBooks("year")}
        >
          Sort by Year ({sortOrder.year === "asc" ? "Oldest First" : "Newest First"})
        </button>
        <button
          onClick={handleShuffleBooks}
        >
          🔀
        </button>
      </div>

      {/* Bookshelf */}
      <ul className="max-h-[100vh] flex border-b-[40px] border-orange-900 mt-10 items-baseline mx-8 px-8 overflow-x-scroll w-full">
        {books.map((book, index) => (
          <Book
            key={index}
            data={book}
          />
        ))}
      </ul>
    </div>
  );
}
// Book.js (child)
"use client";

import React from "react";
import { useState } from "react";
import Image from "next/image";

export default function Book({ data }) {
  const [imageSize, setImageSize] = useState({ width: 0, height: 0 });

  const handleImageLoad = ({ target }) => {
    setImageSize({ width: target.naturalWidth / 4, height: target.naturalHeight / 4 });
  };

  return (
    <motion.li className="relative flex gap-2 items-end">
      <Image
        src={`/images/${route}`}
        width={imageSize.width}
        height={imageSize.height}
        unoptimized={true}
      />
    <motion./li>
  );
}


Solution

  • To achieve a smooth slide transition along the x-axis when books are sorted or shuffled, you need to ensure that the motion components are properly configured.

    Here's a step-by-step guide to help you achieve the desired slide transition:

    Page.js (parent component):

    "use client";
    
    import Book from "./Book";
    import { useEffect, useState } from "react";
    import { motion, AnimatePresence } from "framer-motion";
    
    export default function Home() {
      const [books, setBooks] = useState([]);
      const [sortOrder, setSortOrder] = useState({ title: "asc", dateFinished: "asc", year: "asc" });
    
      useEffect(() => {
        async function fetchBooks() {
          try {
            const response = await fetch("/books.json");
            if (!response.ok) {
              throw new Error(`Failed to fetch: ${response.statusText}`);
            }
            const data = await response.json();
            setBooks(data); // Set the fetched books to the state
          } catch (error) {
            console.error("Error fetching books:", error);
          }
        }
        fetchBooks();
      }, []);
    
      const sortBooks = (criteria) => {
        const order = sortOrder[criteria] === "asc" ? "desc" : "asc";
        const sortedBooks = [...books].sort((a, b) => {
          switch (criteria) {
            case "title":
              return order === "asc" ? a.title.localeCompare(b.title) : b.title.localeCompare(a.title);
            case "year":
              return order === "asc" ? a.year - b.year : b.year - a.year;
            // Add other sorting methods here
            default:
              return 0;
          }
        });
        setBooks(sortedBooks);
        setSortOrder({ ...sortOrder, [criteria]: order });
      };
    
      const handleShuffleBooks = () => {
        const shuffledBooks = [...books].sort(() => Math.random() - 0.5);
        setBooks(shuffledBooks);
      };
    
      return (
        <div className="overflow-y-hidden max-h-[100vh] max-w-[100vw] overflow-hidden">
          <h1 className="text-center">My Bookshelf</h1>
          {/* buttons */}
          <div className="flex gap-2">
            <button onClick={() => sortBooks("title")}>
              Sort Alphabetically ({sortOrder.title === "asc" ? "A-Z" : "Z-A"})
            </button>
            <button onClick={() => sortBooks("year")}>
              Sort by Year ({sortOrder.year === "asc" ? "Oldest First" : "Newest First"})
            </button>
            <button onClick={handleShuffleBooks}>
              🔀
            </button>
          </div>
    
          {/* Bookshelf */}
          <ul className="max-h-[100vh] flex border-b-[40px] border-orange-900 mt-10 items-baseline mx-8 px-8 overflow-x-scroll w-full">
            <AnimatePresence>
              {books.map((book, index) => (
                <Book key={book.id} data={book} index={index} />
              ))}
            </AnimatePresence>
          </ul>
        </div>
      );
    }
    

    Book.js (child component):

    "use client";
    
    import React, { useState } from "react";
    import Image from "next/image";
    import { motion } from "framer-motion";
    
    export default function Book({ data, index }) {
      const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
    
      const handleImageLoad = ({ target }) => {
        setImageSize({ width: target.naturalWidth / 4, height: target.naturalHeight / 4 });
      };
    
      return (
        <motion.li
          className="relative flex gap-2 items-end"
          initial={{ x: -100, opacity: 0 }}
          animate={{ x: 0, opacity: 1 }}
          exit={{ x: 100, opacity: 0 }}
          transition={{ duration: 0.5 }}
          layout
        >
          <Image
            src={`/images/${data.route}`}
            width={imageSize.width}
            height={imageSize.height}
            unoptimized={true}
            onLoad={handleImageLoad}
          />
        </motion.li>
      );
    }