Flipping Pages with CSS

Jason Tseng
Undefined Methods
Published in
4 min readApr 12, 2018
Check out the LIVE DEMO and the Github REPO

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.

--

--