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.
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.
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.
Result
After clicking the next button once, we have the view of the result below.
Resources
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.
Split apps into components to make app development easier, and enjoy the best experience for the workflows you want: