Tyler Morales
Tyler Morales

Reputation: 1838

Why is there a weird stretching animation when clicking on an item in my bookshelf UI using Framer Motion and Tailwind CSS in Next.js?

I’m working on a bookshelf UI in Next.js using Framer Motion for animations and Tailwind CSS for styling. Each book is an interactive li element with hover and click functionality. The problem I’m facing is that when I click on a book, there’s a weird “stretching” animation happening, and I can’t figure out why.

Behavior:

  1. When a book is hovered over, it slightly lifts (translate-y).
  2. When clicked, the selected book expands to show details.
  3. However, on clicking, the book appears to “stretch” or resize unexpectedly before settling into its final state.

Here is the current state of this issue: website

Code:

Here is the relevant code:

// Book.js
import { motion } from "framer-motion";
import Image from "next/image";
import { useState } from "react";

export default function Book({ data, isSelected, onSelect, isAnyHovered, onHover }) {
  const { title, author, route, year } = data;
  const [isHovered, setIsHovered] = useState(false);
  const [imageSize, setImageSize] = useState({ width: 0, height: 0 });

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

  const getImageClassName = () => {
    let className = "transition-all duration-800";

    if (isHovered) {
      className += " opacity-100 -translate-y-2";
    } else {
      className += " opacity-40 translate-y-0";
    }

    return className;
  };

  return (
    <motion.li
      initial={{ x: -50, opacity: 0 }}
      animate={{ x: 0, opacity: 1 }}
      exit={{ x: 50, opacity: 0 }}
      transition={{ duration: 0.4 }}
      layout
      className="relative flex gap-2 items-end"
    >
      <button onClick={() => onSelect(data)}>
        <Image
          alt={`Book spine of ${title}`}
          width={imageSize.width}
          height={imageSize.height}
          src={`/images/${route}`}
          onLoad={handleImageLoad}
          className={getImageClassName()}
          onMouseEnter={() => {
            setIsHovered(true);
            onHover(data);
          }}
          onMouseLeave={() => {
            setIsHovered(false);
            onHover(null);
          }}
        />
      </button>
      {isSelected && (
        <div className="pr-2">
          <h3 className="text-2xl font-bold">{title}</h3>
          <span>by {author}</span>
          <span>{year}</span>
        </div>
      )}
    </motion.li>
  );
}
// page.js
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
import Book from "./Book";

export default function Home() {
  const [books, setBooks] = useState([]);
  const [selectedBook, setSelectedBook] = useState(null);
  const [hoveredBook, setHoveredBook] = useState(null);

  const handleSelectBook = (book) => {
    setSelectedBook(selectedBook === book ? null : book);
  };

  return (
    <ul className="flex relative overflow-x-scroll">
      <AnimatePresence>
        {books.map((book) => (
          <Book
            key={book.id}
            data={book}
            isSelected={selectedBook === book}
            onSelect={handleSelectBook}
            isAnyHovered={hoveredBook !== null}
            onHover={setHoveredBook}
          />
        ))}
      </AnimatePresence>
    </ul>
  );
}

Observations:

What I’ve Tried:

  1. Removing layout from motion.li—but this breaks the animations.
  2. Disabling Tailwind transition-all classes—this did not fix the issue.
  3. Hardcoding the Image width and height instead of calculating them dynamically—this reduced, but did not eliminate, the stretching effect.

Question:

Any insights into how Tailwind CSS and Framer Motion might be interacting (or conflicting) here would be much appreciated. Let me know if additional context or code is needed!

Upvotes: 0

Views: 54

Answers (1)

Kushal
Kushal

Reputation: 11

I think the error is happening for the imageSize is initially width: 0, height: 0 and calculating and updating it at run time. Avoid dynamically calculating the image size at runtime. Instead, predefine static sizes for the Image component.

<Image
  alt={`Book spine of ${title}`}
  width={100}
  height={150}
  src={`/images/${route}`}
  className={getImageClassName()}
  onMouseEnter={() => {
    setIsHovered(true);
    onHover(data);
  }}
  onMouseLeave={() => {
    setIsHovered(false);
    onHover(null);
  }}
/>

Be explicit about which properties you want Tailwind to transition, e.g., transition-opacity Remove transition-all transition-all applies a blanket transition to all properties, which may interfere with Framer Motion’s animations.

let className = "transition-opacity duration-800";

Let me know it's works or not.

Upvotes: 0

Related Questions