javascript - Why is there a weird stretching animation when clicking on an item in my bookshelf UI using Framer Motion and Tailw

admin2025-05-01  0

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:

  • The motion.li element uses layout from Framer Motion, which might be causing the stretching effect.
  • The Image component dynamically calculates its size with naturalWidth and naturalHeight. Could this recalculation be contributing to the issue?
  • Tailwind’s transition and transform classes (translate-y, transition-all, etc.) might be conflicting with Framer Motion’s layout.

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:

  • Why is this stretching animation happening when a book is clicked?
  • How can I prevent the weird resizing/stretching effect while keeping the animations for hover and select intact?

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!

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:

  • The motion.li element uses layout from Framer Motion, which might be causing the stretching effect.
  • The Image component dynamically calculates its size with naturalWidth and naturalHeight. Could this recalculation be contributing to the issue?
  • Tailwind’s transition and transform classes (translate-y, transition-all, etc.) might be conflicting with Framer Motion’s layout.

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:

  • Why is this stretching animation happening when a book is clicked?
  • How can I prevent the weird resizing/stretching effect while keeping the animations for hover and select intact?

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!

Share Improve this question asked Jan 2 at 20:03 Tyler MoralesTyler Morales 1,8586 gold badges30 silver badges70 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

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.

转载请注明原文地址:http://anycun.com/QandA/1746098933a91650.html