Dynamic Styling with Lit: Animated Book Flap

In this article, I will share an example using Lit that addresses the frequent need for dynamic styling based on incoming data when front-end coding.

Murat Özalp
Bits and Pieces

--

I have come across many examples of book animations similar to the one you see in the image. To achieve this, some knowledge of HTML, CSS, and a bit of JavaScript would suffice. You can find the link to the example I found in the resources section at the most below.

Now let’s explore how we can effectively create this example using Lit with dynamic data and styling.

💡 Tip: If you want to reuse your UI components across multiple projects then alongside Lit, you could consider using an open-source toolchain such as Bit. With Bit, you can pack your web components into independent packages, which can be shared and reused across the entire project, helping to maintain consistency in design. Bit provides an integrated dev environment (compiler, tester, linter, documentation, CI, dev server, and packaging/dependency management/bundler all-in-one) for building your apps. To find out more, check out the documentation.

Learn more:

Let us create a Lit project using the project generator. As an example, you can see here.

Step 1: Create model and assign initial values in constructor. Also, import necessary modules.

/* BookFlap.js */

import { html, css, LitElement } from 'lit';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';


// Data Model

const PageStatus = {
ACTIVE: 'active',
PASSIVE: 'passive',
FLIPPED: 'flipped',
};

const pages = [
{ page: 1, status: PageStatus.PASSIVE },
{ page: 2, status: PageStatus.PASSIVE },
{ page: 3, status: PageStatus.PASSIVE },
{ page: 4, status: PageStatus.PASSIVE },
];

export class BookFlap extends LitElement {

static styles = css`
:host {
display: block;
}
`;


static properties = {
pages: { type: Array },
navIndex: { type: Number },
};

constructor() {
super();
this.bookStyle = {}; // set book style to open and close in initial and last pages
this.pages = [...pages]; // set the data as a property to update the status and see the flip change
this.navIndex = 0; // set the initial nav index. Every time page flips , update it
this.pages[this.navIndex].status = PageStatus.ACTIVE; // set the initial page status as active
}
...

}

Step 2: Add the HTML of the main container and its style.

/* BookFlap.js */

export class BookFlap extends LitElement {

static styles = css`
:host {
display: block;
}

.main-container {
display: flex;
align-items: center;
justify-content: center;
flex-flow: column;
gap: 1rem;
}

`;

// ...

render() {
return html`<div class="main-container">
${this.bookContainer}
<div class="nav-container">
${this.navigatorPrev} ${this.navigatorNext}
</div>
</div> `;
}

Step 3: Add navigator buttons and import wired-icon-button and Google icons optionally as modules.

/* index.html */
// ...

<head>
<meta charset="utf-8" />
<style>
body {
background: powderblue;
}
</style>
<script
type="module"
src="https://unpkg.com/wired-elements?module"
></script>
<link
href="https://fonts.googleapis.com/css?family=Material+Icons&display=block"
rel="stylesheet"
/>
</head>

// ...

<script type="module">
import { html, render } from 'lit';
import '@material/mwc-icon/mwc-icon.js';
import '../book-flap.js';

render(html` <book-flap> </book-flap> `, document.querySelector('#demo'));
</script>
/* BookFlap.js */

// ...

navNext() {
// manage page flip of nextwith dynamic styling and set nav index to next
}

navPrev() {
// manage page flip of previous with dynamic styling and set nav index to prev
}

get navigatorNext() {
return html`
<wired-icon-button @click=${this.navNext}>
<mwc-icon>navigate_next</mwc-icon>
</wired-icon-button>
`;
}

get navigatorPrev() {
return html`
<wired-icon-button @click=${this.navPrev}>
<mwc-icon>navigate_before</mwc-icon>
</wired-icon-button>
`;
}

// ...

Step 4: Add HTML and static styles of book and page container by mapping data of pages.

 // ...

static styles = css`
// ...

/* Book */
.book {
position: relative;
width: 60vh;
height: 80vh;
transition: transform 1s;
}

/* Page */
.paper {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
perspective: 1500px;
}

.front,
.back {
display: flex;
background-color: white;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
transform-origin: left;
transition: transform 1s;
}

.front {
z-index: 1;
backface-visibility: hidden;
border-left: 3px solid powderblue;
}

.back {
z-index: 0;
}

.content {
margin: 1rem;
font-size: 50%;
max-width: 50%;
height: fit-content;
}

.back .content {
transform: rotateY(180deg);
}

`;


// ...

pageContainer({ status, page : currentPage}) {

const flipperStyle = {}; // set dynamically for flipped class
const paperStyle = {}; // set dynamically by calculating zIndex value

const contentStyle = {}; // set style for cover image of the current page

return html`
<div
class="paper ${classMap(flipperStyle)}"
style="${styleMap(paperStyle)}"
>
<div class="front" style="${styleMap(contentStyle)}">
<div class="content">Front Page ${currentPage}</div>
</div>
<div class="back">
<div class="content">Back Page ${currentPage}</div>
</div>
</div>
`;
}

get bookContainer() {
return html` <div
id="book"
class="book"
style="${styleMap(this.bookStyle)}"
>
${this.pages.map(item => this.pageContainer(item))}
</div>`;
}

// ...

Step 5: Add page flip styles dynamically.

// ...

static styles = css`
// ...

/* Paper flip effect */
.flipped .front,
.flipped .back {
transform: rotateY(-180deg);
}

// ...

`;

// ...

pageContainer({ status, page : currentPage}) {

const flipperStyle = { flipped: status === PageStatus.FLIPPED };

const zIndex =
status !== PageStatus.FLIPPED ? this.pages.length - currentPage : currentPage;

const paperStyle = {
'z-index': zIndex,
};

const contentStyle = {
'background-image': `url(assets/images/pages/page${currentPage}.jpg)`,
'background-position': 'center' /* Center the image */,
'background-repeat': 'no-repeat' /* Do not repeat the image */,
'background-size':
'cover' /* Resize the background image to cover the entire container */,
};

return html`
<div
class="paper ${classMap(flipperStyle)}"
style="${styleMap(paperStyle)}"
>
<div class="front" style="${styleMap(contentStyle)}">
<div class="content">Front Page ${currentPage}</div>
</div>
<div class="back">
<div class="content">Back Page ${currentPage}</div>
</div>
</div>
`;
}

// ...

Note: In the example, you see the cover image only added for the content of the front page. You can also add it to the back page in the same way.

background image file location in the project source

Step 6: Add the necessary logic for actions of navigation.

// ...

navNext() {
if (this.navIndex >= this.pages.length) {
return;
}
// if (this.navIndex === 0)
// open book if it is first page

this.pages[this.navIndex].status = PageStatus.FLIPPED;
this.navIndex += 1;
if (this.navIndex < this.pages.length)
this.pages[this.navIndex].status = PageStatus.ACTIVE;
// else
// close book if the current page is last

}

navPrev() {
if (this.navIndex === 0) return;

// if (this.navIndex === this.pages.length)
// open book if it is last page

if (this.navIndex < this.pages.length) {
this.pages[this.navIndex].status = PageStatus.PASSIVE;
}

this.navIndex -= 1;
this.pages[this.navIndex].status = PageStatus.ACTIVE;

// if (this.navIndex === 0)
// close book if the current page is first

}


// ...

Step 7: Add book container styles dynamically once the book opens and closes.

 // ...

// added
openBook() {
this.bookStyle = { transform: 'translateX(50%)' };
}

closeBook(lastPage) {

if (!lastPage) {
this.bookStyle = { transform: 'translateX(0%)' };
} else {
this.bookStyle = { transform: 'translateX(100%)' };
}
}

// added
navNext() {
if (this.navIndex >= this.pages.length) {
return;
}
if (this.navIndex === 0) {
this.openBook(); // updated
}
this.pages[this.navIndex].status = PageStatus.FLIPPED;
this.navIndex += 1;
if (this.navIndex < this.pages.length)
this.pages[this.navIndex].status = PageStatus.ACTIVE;
else {
this.closeBook(true); // updated
}
}

navPrev() {
if (this.navIndex === 0) return;

if (this.navIndex === this.pages.length) {
this.openBook(); // updated
}

if (this.navIndex < this.pages.length) {
this.pages[this.navIndex].status = PageStatus.PASSIVE;
}

this.navIndex -= 1;
this.pages[this.navIndex].status = PageStatus.ACTIVE;

if (this.navIndex === 0) {
this.closeBook(false); // updated
}
}

// ...

Here we solved the problem when the book opens, we should change the position of the book according to the x-axis.

Problem 1: once the book opens from the initial page, the navigators stay in the wrong position.
Problem 2: once the book closes from the last page, the navigators stay in the wrong position.

Result

After clicking the next button once, we have the view of the result below.

page, status, z-index values according to last changes

Resources

  1. Git Repo for the example project.
  2. Git Repo for the result.

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--

Passionate software dev. Let's share knowledge to finding near-perfect solutions!