Flipping Pages with CSS
One of my favorite apps to read the news is Flipboard. There’s something about the skeuomorphic user experience of turning the pages of an infinitely expanding newspaper that’s both incredibly peaceful and addictive at the same time. I created a simple reading experience web app using Vue.js to recreate the page flipping interface of Flipboard with content served from The New York Times ArticleSearch API.
HTML
// Page.vue
<template lang="html">
<div class="page">
<div class="flip-container" :style="{zIndex: this.flippedIndex}" :class="{flip: flip}">
<div class="flipper">
<div class="front" @click="this.flipCard">
<articleCard class="bottom" :article="this.front" />
</div>
<div class="back" @click="this.flipCard">
<articleCard class="top" :article="this.back" />
</div>
</div>
</div>
</div>
</template>
The page component’s html template is pretty simple: A container hold a .flipper
div that has a .front
and .back
sections that are displayed based on which side is front-facing. You can see that the @click
directive on the .front
and .back
divs places a "click”
event listener on each side of the page. When the div is clicked, it calls a flipCard
method that toggles a flag variable on the component, flip
. This variable is bound to the .flip-container
element’s .flip
CSS class. When flip
returns false
the .flip
class is not applied, and vice versa. This .flip
class is essentially what triggers the transform
CSS transition that flips the page.
Speaking of the CSS, notice the .flip-container
element also has inline styles bound to a style object on the component’s data object. This dynamically generates inline styling based off of where the article appears in the array of articles received from the articleSearch API’s JSON response.
CSS
<style lang="scss">
/* import app-wide sass variables */
@import "../assets/settings.scss";/* entire container, keeps perspective */
.flip-container {
position: absolute;
bottom: 0;
perspective: 100vw;
perspective-origin: center top;
/* fixes the perspective of the transform transition on the top center of the .flip-container */
.flipper {
transform-origin: 0% 50%;
}
}
/* flip the pane when clicked */
.flip-container.flip .flipper {
transform: rotateX(180deg);
transform-origin: 100% 50%;
}/* in mobile view, the pages flip upwards and therefore need to take up half the height. */
.flip-container, .front, .back {
height: 50%;
}/* determines the speed of the transition */
.flipper {
transition: 0.6s;
transform-style: preserve-3d;
transform-origin: 100% 50%;
position: relative;
}/* hides the face of the page that is facing away from the reader */
.front, .back {
backface-visibility: hidden;
position: absolute;
background-color: white;
}/* when the back is reversed, you have to rotate is so that when it is displayed it isn't backwards */
.back {
transform: rotate3D(0, 1, 0, 180deg);
transform-origin: center;
.article.top {
transform: rotate(180deg);
}
}/* separate rules for desktop views */
@media (min-width: 450px) {
.flip-container {
position:absolute;
right: 0;
top: 0;
perspective: 100vw;
perspective-origin: left top;
.flipper {
transform-origin: 0% 50%;
}
}
.flip-container, .front, .back {
width: 50vw;
}
.flip-container.flip .flipper {
transform: rotateY(-180deg);
transform-origin: 0% 0%;
}
.front, .back {
backface-visibility: hidden;
position: absolute;
background-color: white;
height: $article_height;
}
.front {
/* for firefox 31 */
transform: rotateY(0deg);
}
.back {
transform: rotateY(180deg);
.article.top {
transform: rotate(0deg);
}
}
}
</style>
The more important thing to remember about these CSS rules is that the .flip-container
element holds the 3-D perspective and allows the .flipper
element to rotate in 3-D space. The .front
and .back
elements are attached to the .flipper
element’s two sides and are rendered invisible when reversed.
Javascript
flip: function (newState, oldState) {
if (newState === true) {
setTimeout(() => {
this.flippedIndex = this.index + 1
let indexViewed = this.index + 2
this.$emit('pageFlip', indexViewed) }, 600)
} else if (newState === false) {
this.flippedIndex = 999 - this.index
this.$emit('pageFlip', this.index)
}
}
There’s a lot of javascript in my implementation, but one I’ll highlight for this interface is this method that is called when the flip
variable is changed. This method changes the z-index
of the page based off the index of the article in the array of articles returned from the articleSearch response. If the page has been flipped, the flipped pages should be ordered be descending order so that the latest page to be flipped is visible, and if the page has not been flipped, they should be ordered by ascending order. Also, when flipping a page, the z-index should only be changed after the transition concludes. So achieve that, simply use a setTimeout
function to delay the execution of the method.
If you’re interested in seeing the application in action, check out the LIVE DEMO and the Github REPO for all the code nitty-gritty.
Also feel free to check out my website for more of my portfolio.