I know I'm missing something with the rotation of the inside cover when the book is "clicked on" to open, but I am not seeing it.
The expectation is that when the book is clicked on, the following happens:
All in all I am trying to get a 3D Animation of a book opening when it is clicked on by the user.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Opening Book with GSAP</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #f0f4f8;
overflow: hidden;
}
.font-serif {
font-family: 'Playfair Display', serif;
}
/* The scene is the 3D space for the book */
.scene {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
perspective: 2500px;
}
/* The wrapper handles positioning and can be clicked */
.book-wrapper {
position: relative;
cursor: pointer;
}
/* The book container holds all the 3D pieces */
.book {
width: 350px;
height: 500px;
position: relative;
transform-style: preserve-3d;
}
/* The front cover, which will flip open */
.front-cover {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
transform-origin: left center;
transform-style: preserve-3d;
z-index: 10;
}
.cover-face {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 0.5rem;
background-color: #4a3a32;
}
.cover-face-front {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
position: relative;
overflow: hidden;
}
/* The inside of the cover matches the outside */
.cover-face-back {
background-color: #4a3a32;
transform: rotateY(-180deg);
}
/* Crease styles */
.cover-face-front::before,
.cover-face-front::after {
content: '';
position: absolute;
top: 0;
height: 100%;
width: 2px;
background-color: rgba(0, 0, 0, 0.2);
box-shadow: 1px 0 5px rgba(0, 0, 0, 0.35);
}
.cover-face-front::before {
left: 31px;
}
.cover-face-front::after {
left: 35px;
}
/* NEW: This is the actual back cover board */
.back-cover {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: #4a3a32;
border-radius: 0.5rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
z-index: 1;
}
/* The static page block that sits on top of the back cover */
.pages {
position: absolute;
width: calc(100% - 1rem);
height: calc(100% - 1rem);
top: 0.5rem;
left: 0.5rem;
background-color: #f3f0e9;
border-radius: 0.25rem;
z-index: 5;
}
/* Styles for the flipping pages */
.flipping-page {
position: absolute;
width: calc(100% - 1rem);
height: calc(100% - 1rem);
top: 0.5rem;
left: 0.5rem;
background-color: #fdfaf2;
transform-origin: left center;
border-radius: 0.25rem;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1) inset;
border-right: 1px solid #e0d9cd;
z-index: 6;
}
/* Decorative elements */
.cover-design {
border: 4px double #d4af37;
width: 100%;
height: 100%;
border-radius: 0.25rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 1rem;
color: #d4af37;
}
.cover-title {
font-family: 'Playfair Display', serif;
font-size: 2.75rem;
font-weight: 700;
letter-spacing: 1px;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4);
}
.cover-author {
margin-top: 1.5rem;
font-size: 1.125rem;
font-style: italic;
border-top: 1px solid rgba(212, 175, 55, 0.5);
padding-top: 1.5rem;
}
</style>
</head>
<body>
<div class="scene">
<!-- The new wrapper handles positioning -->
<div id="book-wrapper" class="book-wrapper">
<!-- The book itself only handles 3D animations -->
<div id="book" class="book">
<!-- NEW: Added a dedicated back cover element -->
<div class="back-cover"></div>
<div class="pages"></div>
<div class="flipping-page" id="page-3"></div>
<div class="flipping-page" id="page-2"></div>
<div class="flipping-page" id="page-1"></div>
<div class="front-cover">
<div class="cover-face cover-face-front">
<div class="cover-design">
<h1 class="cover-title">About the Author</h1>
<p class="cover-author"></p>
</div>
</div>
<div class="cover-face cover-face-back"></div>
</div>
</div>
</div>
</div>
<script>
// Select the elements to animate
const bookWrapper = document.getElementById('book-wrapper');
const frontCover = document.querySelector('.front-cover');
const flippingPages = gsap.utils.toArray('.flipping-page');
// Set the default transform origin for the cover and pages
gsap.set([frontCover, flippingPages], {
transformOrigin: "left center"
});
// Create a GSAP timeline, paused by default
const timeline = gsap.timeline({
paused: true
});
// Add animations to the timeline
timeline
// 1. Move the entire book wrapper to the right to center the spine
.to(bookWrapper, {
x: 175,
duration: 1.2,
ease: "power2.inOut"
})
// 2. Flip the cover open at the same time
.to(frontCover, {
rotationY: -180,
duration: 1.2,
ease: "power2.inOut"
}, "<") // "<" starts at the same time as the previous animation
// 3. Flip the pages with a stagger effect, starting slightly after the cover begins to open
.to(flippingPages, {
rotationY: -180,
duration: 0.8,
ease: "power1.inOut",
stagger: 0.1
}, "<0.2"); // "<0.2" starts 0.2s after the previous animation's start
// Event listener to control the timeline
bookWrapper.addEventListener('click', () => {
if (timeline.reversed() || timeline.progress() === 0) {
timeline.play();
} else {
timeline.reverse();
}
});
</script>
</body>
</html>
I was able to reproduce and fix the issue by making only two changes to your file. The issue is that when the book opens, the back side of the front cover shows below the book instead of being under the flipped page. I resolved it by adding position, width, height, top, and left to existing class cover-face-back
:
.cover-face-back {
background-color: #4a3a32;
transform: rotateY(-180deg);
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
Now the cover-face-back
element is in the correct position, but it covers the flipped page. To solve this, I modified the timeline applied to the flippingPages
: added zIndex
.to(flippingPages, {
zIndex: 11, // display flipped page on top of the front cover
rotationY: -180,
duration: 0.8,
ease: "power1.inOut",
stagger: 0.1
}, "<0.2"); // "<0.2" starts 0.2s after the previous animation's start
Now the book opens as you would expect (you can adjust the position if you don't like that the left border looks a little wider than the right, but to me it looks natural):