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:
- The cover of the book mirrors across the Y-axis
- The first page is shown on the right with an border that resembles the background of the back cover.
- The inside cover is shown as a solid cover that matches the background color of the front cover.
- The pages then flip across the Y-axis
All in all I am trying to get a 3D Animation of a book opening when it is clicked on by the user.
<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>
Images: Before and after, respectively.

