Reputation: 1838
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:
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:
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
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