Million Miles Technologies

Building a 3D Card Flip Animation with CSS Houdini — SitePoint


element with the class progress serves as the canvas, while the data-progress attribute dynamically stores the current progress value:

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Moving Circular Progress Bartitle>
<link rel="stylesheet" href="styles.css">
head>
<body>
<div class="progress" data-progress="0">div>
<script src="app.js">script>
body>

The CSS snippet below employs Houdini’s custom properties to craft a circular progress bar. The @property rule introduces --progress with a syntax, initialized at 0%, ensuring non-inheritance. Subsequently, the .progress class styles the circular container, utilizing a conic gradient to depict progress dynamically. This concise code harnesses the flexibility of Houdini custom properties for creating visually engaging circular progress elements in web development:

@property --progress {
 syntax: '';
 inherits: false;
 initial-value: 0%;
}
.progress {
 --progress: 0%;
 width: 200px;
 height: 200px;
 border-radius: 50%;
 background: conic-gradient(rgb(255, 58, 255) 0%, rgb(255, 58, 255) var(--progress), transparent var(--progress), transparent 100%);
 position: relative;
 overflow: hidden;
}
.progress::before {
 content: attr(data-progress);
 position: absolute;
 top: 50%;
 left: 50%;
 transform: translate(-50%, -50%);
 font-size: 24px;
 font-weight: bolder;
 color: purple;
 text-align: center;
}

Next, we have the custom property definition (@property rule):

@property --progress {
 syntax: '';
 inherits: false;
 initial-value: 0%;
}

Some things to note in the code above:

  • The @property rule is part of the Houdini CSS Typed OM specification.
  • It defines a custom CSS property named --progress with the syntax of .
  • inherits: false; specifies that the custom property doesn’t inherit its value from its parent elements.
  • initial-value: 0%; sets the initial value of the custom property to 0%.

Next, let’s style the circular progress bar:

.progress {
 --progress: 0%;
 width: 200px;
 height: 200px;
 border-radius: 50%;
 background: conic-gradient(#ccc 0%, #ccc var(--progress), transparent var(--progress), transparent 100%);
 position: relative;
 overflow: hidden;
}

Some things to note above:

  • --progress: 0%; initializes the custom property to 0%.
  • The .progress class styles the circular progress bar.
  • width and height set the dimensions of the circular container.
  • border-radius: 50%; creates a perfect circle.
  • background uses a conic gradient to create the circular progress effect, with the progress determined by the --progress property.
  • position: relative; and overflow: hidden; are used for positioning and overflow management.

Next, we’ll create our paint worklet.

Our circular progress bar springs to life through the dynamic partnership of CSS Houdini and JavaScript. Leveraging CSS Houdini, we define a custom property, --progress, while the paint worklet takes charge of custom painting. This synergy enables real-time updates to our progress bar based on the evolving value of the custom property. This collaboration not only enhances flexibility but also provides a potent avenue for creating unique rendering effects, resulting in an engaging and visually captivating circular progress bar for our web application:

class PaintWorklet {
 paint(ctx, { width, height, progress }) {
 ctx.clearRect(0, 0, width, height);
 ctx.beginPath();
 ctx.arc(width / 2, height / 2, width / 2, 0, (Math.PI * 2 * progress) / 100);
 ctx.fillStyle = '#42f445';
 ctx.fill();
 }
}

Here are some points to not in the code above:

  • class PaintWorklet is a JavaScript class representing a paint worklet, part of the Houdini Paint API.
  • The paint method defines the custom painting logic for the circular progress bar.
  • ctx is the 2D rendering context, and it’s used to draw the circular progress.

Next, we register the paint worklet and custom property:

CSS.paintWorklet.addModule('paint-worklet.js');
const progressElements = document.querySelectorAll('.progress');
progressElements.forEach(element => {
 const paintWorklet = new PaintWorklet();
 CSS.registerProperty({
 name: '--progress',
 syntax: '',
 inherits: false,
 initialValue: '0%',
 paint: (ctx, geometry, properties) => {
 paintWorklet.paint(ctx, {
 width: geometry.width,
 height: geometry.height,
 progress: parseFloat(properties.get('--progress').toString()),
 });
 },
 });
});

Some points to note in the code above:

  • CSS.paintWorklet.addModule('paint-worklet.js'); loads the paint worklet module.
  • CSS.registerProperty registers the custom property --progress and associates it with the paint worklet.
  • The paint method is called to provide the custom painting logic based on the current value of --progress.

Now let’s set the progress over time:

let currentProgress = 0;
function updateProgress() {
 currentProgress += 0.1; 
 if (currentProgress > 100) {
 currentProgress = 0;
 }
 progressElements.forEach(element => {
 element.dataset.progress = currentProgress.toFixed(2);
 element.style.setProperty('--progress', `${currentProgress.toFixed(2)}%`);
 });
 requestAnimationFrame(updateProgress);
}
updateProgress();

Some points to note in the code above:

  • currentProgress is incremented over time to simulate the progress.
  • element.dataset.progress and element.style.setProperty update the DOM and custom property to reflect the progress.
  • requestAnimationFrame ensures smooth animation by requesting the next frame.

circular progress bar

Introducing the Paint API

The Paint API is integral to CSS Houdini, and it revolutionizes web painting by enabling dynamic and customized visual styles. It empowers developers to create on-the-fly designs using user-defined custom properties. Explore its secrets to unleash unparalleled potential.

Simplified foundations

Here are some Paint API features:

  • Paint worklet. A JavaScript function that acts as your artistic genie, conjuring up visuals based on your instructions.
  • Custom properties. Variables you define using CSS’s var() syntax, holding values that can be dynamically referenced and manipulated.
  • The paint() function. The magic wand that calls upon your paint worklet to weave its visual enchantment onto elements.

Painting with code: a practical example

To illustrate “painting with code” in action, let’s dive into a practical example that showcases the power of the CSS Paint API.

This code snippet below demonstrates how developers can create dynamic and customizable patterns that break free from the constraints of static images, breathing life into web experiences:

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<title>Paint API Demotitle>
head>
<body>
<section id="screen">
section>
<script>
CSS.paintWorklet.addModule('./app.js');
script>
body>
html>

Linking the paint worklet

CSS.paintWorklet.addModule('./app.js') registers the custom paint worklet defined in app.js, enabling its use in CSS.

The CSS snippet provided below exemplifies the utilization of the Paint API in CSS Houdini. The background-image: paint(awesomePattern) property integrates the awesomePattern paint worklet, exemplifying the power and simplicity of dynamically generating visuals:

body {
  margin: 0px;
  padding: 0px;
}
#screen {
  box-sizing: border-box;
  margin: 10px;
  padding: 0px;
  width: calc(100vw - 20px);
  height: calc(100vh - 20px);
  background-color: #111;
  background-image: paint(awesomePattern);
}

Applying the paint worklet

background-image: paint(awesomePattern) applies the registered paint worklet as a background image to the #screen element, showcasing CSS Houdini’s ability to create dynamic visuals.

CSS properties can control the pattern’s appearance:

  • --numShapes: number of circles
  • --shapeSize: size of circles
  • --colors: color palette

In the JavaScript code below, the Shape class takes center stage. Its paint method, fueled by user-defined properties like --numShapes, --shapeSize and --colors, orchestrates the creation of a canvas adorned with randomized shapes. The registration of the awesomePattern paint worklet solidifies the integration of CSS and JavaScript, delivering a seamless synergy of dynamic visual elements:

class Shape {
paint(ctx, geom, properties) {
  
  const numShapes = properties.get('--numShapes') || 30;
  const shapeSize = properties.get('--shapeSize') || 50;
  const colors = properties.get('--colors') || ['#F28500', '#00FFFF', 'limegreen'];
  
  const getRandomColor = () => colors[Math.floor(Math.random() * colors.length)];
  for (let i = 0; i < numShapes; i++) {
    const x = Math.random() * geom.width;
    const y = Math.random() * geom.height;
    const radius = Math.random() * shapeSize;
    const color = getRandomColor();
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, 2 * Math.PI);
    ctx.fill();
    ctx.closePath();
    }
  }
}
registerPaint('awesomePattern', Shape);

Defining the paint worklet

  • class Shape { ... } defines a class with a paint() method, the core of the Paint API.
  • properties.get() retrieves customization options from CSS, demonstrating Houdini’s integration with CSS properties.
  • The paint() method uses canvas-like drawing commands to create the dynamic circle pattern, highlighting Houdini’s ability to extend CSS with custom rendering capabilities.
  • registerPaint('awesomePattern', Shape) registers the Shape class as a paint worklet, making it available for use in CSS.

large dots

Crafting Interactive 3D Animations with CSS Houdini

This is our process of building a captivating 3D card flip animation using CSS Houdini, worklets, Paint API, and custom Houdini properties. CSS Houdini allows for the creation of custom paint worklets, enabling a more flexible and dynamic approach to styling. The animation is triggered by a hover event, showcasing the power of Houdini in seamlessly combining both the visual and interactive aspects of web development.

In the code below, you’ll find the complete code, with concise explanations of the CSS Houdini elements:

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<script type="module" src="app.js" defer>script>
head>
<body>
<div class="card" id="flip-card">
<div class="card-inner" id="flip-card-inner">
<div class="card-front">
Front Content
div>
<div class="card-back">
Back Content
div>
div>
div>
body>
html>

The CSS code below establishes the foundational structure for a card element in a web project. The layout centers within the viewport using Flexbox properties, and the card itself is defined with specific dimensions and a three-dimensional perspective.

Notably, the Houdini feature, paint: card-flip;, applies a custom paint worklet to the .card-inner element, introducing a dynamic flip effect on hover. The transition is controlled by the transform property, smoothly animating a 180-degree rotation. Styling details include vibrant background colors, font properties, and border-radius for a polished appearance on both front and back faces. The code achieves a visually appealing and interactive card design:

body {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  margin: 0;
  background-color: #f0f0f0;
}
.card {
  width: 200px;
  height: 300px;
  perspective: 1000px;
}
.card-inner {
  --card-rotation: 0deg;
  paint: card-flip;
  paint-order: normal;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 0.6s;
  transform: rotateY(var(--card-rotation));
}
.card:hover .card-inner {
  --card-rotation: 180deg;
}
.card-front,
.card-back {
  width: 100%;
  height: 100%;
  position: absolute;
  backface-visibility: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  color: white;
  border-radius: 10px;
}
.card-front {
  background-color: #3498db;
}
.card-back {
  background-color: #e74c3c;
  transform: rotateY(180deg);
}

In the JavaScript code below — from app.js — the script checks if the browser supports the paintWorklet feature, and if so, it adds a paint worklet module named paintWorklet.js. Additionally, an event listener is attached to the element with the ID flip-card, toggling the flipped class on a click event:

if ('paintWorklet' in CSS) {
  CSS.paintWorklet.addModule('paintWorklet.js')
  .catch(error => console.error('Failed to register paint worklet:', error));
}
document.getElementById('flip-card').addEventListener('click', function () {
  this.classList.toggle('flipped');
});

The paintWorklet.js file extends the JavaScript functionality by registering a custom paint worklet named card-flip with CSS. This worklet, seamlessly integrated with the existing code, dynamically paints the flipping animation of the card element using canvas operations.

The --card-rotation property controls the rotation angle. Together with the interactive click event listener, this modular approach enhances the overall visual appeal of the web project.

paintWorklet.js

class CardFlipPainter {
  static get inputProperties() {
    return ['--card-rotation'];
  }
  paint(ctx, geom, properties) {
    const rotation = properties.get('--card-rotation').toString();
    ctx.clearRect(0, 0, geom.width, geom.height);
    ctx.fillStyle = '#3498db';
    ctx.fillRect(0, 0, geom.width, geom.height);
    ctx.fillStyle = '#e74c3c';
    ctx.save();
    ctx.translate(geom.width / 2, geom.height / 2);
    ctx.rotate((parseFloat(rotation) || 0) * (Math.PI / 180));
    ctx.fillRect(-geom.width / 2, -geom.height / 2, geom.width, geom.height);
    ctx.restore();
  }
}
registerPaint('card-flip', CardFlipPainter);
  • class CardFlipPainter { ... } defines the custom paint worklet.
  • static get inputProperties() { ... } accepts the --card-rotation property.
  • paint(ctx, geom, properties) { ... } performs the custom painting logic using canvas operations.
  • registerPaint('card-flip', CardFlipPainter); registers the worklet with CSS.

card flip

Expanding upon the Card Flip

Here’s how the card flip, created with Houdini, can be incorporated into a company site. Allow me to present a real-project scenario:

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<script type="module" src="app.js" defer>script> 
head>
<body>
<div class="flex-container">
<div class="card" id="flip-card"> 
<div class="card-inner" id="flip-card-inner">
<div class="card-front">
<div class="image">
div>
div>
<div class="card-back">
<div class="content">
<p>John Doep>
<p> Marketing Lead p>
div>
div>
div>
div>
<div class="card" id="flip-card">
<div class="card-inner" id="flip-card-inner">
<div class="card-front">
<div class="image">
div>
div>
<div class="card-back">
<div class="content">
<p>Jane Doep>
<p> Senior Dev. p>
div>
div>
div>
div>
<div class="card" id="flip-card">
<div class="card-inner" id="flip-card-inner">
<div class="card-front">
<div class="image"> 
div>
div>
<div class="card-back">
<div class="content">
<p>Mike Doep>
<p>Dev Opsp>
div>
div>
div>
div>
div>
body>
html>

I’ve designed a staff section for the website where the card flip is utilized to display the staff members’ images as the front content, with their job title and name revealed on the back.

Let’s go through a few of the changes I made to the code.

Arrange all the cards within a flex container:

<div class="flex-container">
div>

In the card front section, I’ve incorporated an image div, where you can easily integrate any image of your choice by utilizing a background image in the CSS.

<div class="card-front">
  <div class="image">
  div>
div>

In the card back section, you’ll find a content div that includes specific details like the staff member’s name (“Mike Doe”) and job title (“Dev Ops”). These elements have been assigned the class content for ease of CSS styling. Feel free to personalize this information to align with your specific content requirements.

<div class="card-back">
  <div class="content">
    <p>Mike Doep>
    <p>Dev Opsp>
  div> 
div>

The forthcoming CSS code snippet showcases practical changes to demonstrate the application of the CSS card flip in a real web project. Adjust these styles according to your preferences:

body {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  margin: 0;
  background-color: #f0f0f0;
}
.card {
  width: 200px;
  height: 300px;
  perspective: 1000px;
}
.card-inner {
  --card-rotation: 0deg;
  paint: card-flip;
  paint-order: normal;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 0.6s;
  transform: rotateY(var(--card-rotation));
}
.card:hover .card-inner {
  --card-rotation: 180deg;
}
.card-front,
.card-back {
  width: 100%;
  height: 100%;
  position: absolute;
  backface-visibility: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  color: white;
  border-radius: 10px;
}
.card-front {
  background-color: #3498db;
}
.card-back {
  background-color: #e74c3c;
  transform: rotateY(180deg);
}
.flex-container {
  display: flex;
  gap: 2rem;
} 
.content {
  text-align: center;
}
.image {
  height: 120px;
  width: 120px;
  border-radius: 50%;
  background-color: #fff;
  
}

In the container that encompasses all the cards, the CSS code utilizes flex display and sets a gap of 2 rem between the cards. Adjust the gap value according to your design preferences.

.flex-container { 
  display: flex; gap: 2rem;
}

The CSS code for the content class centers the text within the specified element. Adjust as necessary to achieve the desired alignment for your content:

.content {
  text-align: center;
}

The CSS code for the image class in the front card content sets a circular image with a height and width of 120px, a border-radius of 50%, and a white background color. Additionally, there is a commented-out line for a background image; you can uncomment this line and provide the appropriate URL for the image you want to use. Adjust the dimensions and background properties as needed to suit your design preferences:

.image { 
  height: 120px; 
  width: 120px; 
  border-radius: 50%; 
  background-color: #fff; 
   
}

3d card flip staff section

Conclusion

In summary, the article explores the transformative features of Houdini, focusing on a 3D card flip animation. It highlights the power of worklets for dynamic animations, interactive effects, and extending visual styles. Custom properties and the Paint API offer additional flexibility and creative possibilities.

The practical examples, including a circular progress bar and the integration of the 3D card flip into a company site, showcase Houdini’s real-world applications. The article encourages you to embrace Houdini’s limitless potential, providing tools to redefine web design and inspire creative development.

Related blogs