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:
- When a book is hovered over, it slightly lifts (translate-y).
- When clicked, the selected book expands to show details.
- 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:
- Removing layout from motion.li—but this breaks the animations.
- Disabling Tailwind transition-all classes—this did not fix the issue.
- 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!