For the annoucement of the public beta of Cloud, we wanted to create something unique that everyone could call their own. We decided to create a personalized card, with animation and interactivity sprinkled it, to really give the whole experience a “special” feel.
The Cloud Beta Card page in the Appwrite Console
When our design team showed us the design, we LOVED it. But soon, reality struck, and I realized I am going to have to implement this design. It needs to look good, work on multiple browsers, and perform well. Quite the challenge!
Nevertheless, we pulled it off with the power of Svelte and CSS, and I'm excited to share what I learned with everyone.
Inspiration
Our inspiration for this feature came from https://poke-holo.simey.me/ which features a similar card animation effect for Pokemon cards. This project, like Appwrite, is open-source, and like our console, built with Svelte! Meaning we could learn a lot from it.
The Implementation Process
The final card animation can be broken down into pieces. We’ll be going over the main ones in isolation:
- “Popping up” the card on click
- Rotating the card
- Card shine
You can preview the code and output here: https://appwrite-card-snippets.vercel.app/
If you’re curious, you can also check out the source code of the card element, where we integrate all of these pieces and some extra details ✨
Setup the base HTML structure
The card needs a back and front side to it. Since we’re doing animations in the 3D space, we also use the perspective
CSS attribute. Make sure to bring your 3D glasses!
The result of this markup is the following:
<div class="card">
<div class="card-inner">
<div class="card-back">
<img
src="https://cloud.appwrite.io/v1/cards/cloud-back?mock=normal"
alt="The back of the Card"
loading="lazy"
width="450"
height="274"
/>
</div>
<div class="card-front">
<img
src="https://cloud.appwrite.io/v1/cards/cloud?mock=normal"
alt="The front of the card"
width="450"
height="274"
/>
</div>
</div>
</div>
<style>
.card {
perspective: 1000px;
}
.card-inner {
display: grid;
transition: transform 0.8s;
transform-style: preserve-3d;
}
/* Do an horizontal flip when you move the mouse over the flip box container */
.card:hover .card-inner {
transform: rotateY(180deg);
}
/* Position the front and back side */
.card-front,
.card-back {
grid-area: 1/1;
backface-visibility: hidden;
}
/* Rotate the back side 180 degrees */
.card-back {
transform: rotateY(180deg);
}
</style>
You can preview it here.
Popping up the card
We want the card to pop up when the user clicks it, so they can have a closer look. While popping up, we also want the card to spin around because it’s more fun that way 🕺
We will use Svelte’s spring
store to achieve this effect. Whenever we set the store to a new value, they’ll smoothly transition to it instead of immediately changing. We’ll need two stores, scale
for controlling the card size and rotateDelta
to control the rotation.
<script>
import { spring } from 'svelte/motion';
let active = true;
const smooth = { stiffness: 0.03, damping: 0.45 };
const scale = spring(1, smooth);
const rotateDelta = spring(0, smooth);
function popup() {
scale.set(1.45);
rotateDelta.set(360);
}
function retreat() {
scale.set(1);
rotateDelta.set(0);
}
$: if (active) {
popup();
} else {
retreat();
}
$: style = [`--scale: ${$scale}`, `--rotateDelta: ${$rotateDelta}deg`].join(';');
</script>
<div class="card" {style}>
<button class="card-inner" on:click={() => (active = !active)}>
<div class="card-back">
<img
src="https://cloud.appwrite.io/v1/cards/cloud-back?mock=normal"
alt="The back of the Card"
loading="lazy"
width="450"
height="274"
/>
</div>
<div class="card-front">
<img
src="https://cloud.appwrite.io/v1/cards/cloud?mock=normal"
alt="The front of the card"
width="450"
height="274"
/>
</div>
</button>
</div>
<style>
/* Button reset */
button {
background: none;
border: none;
padding: 0;
cursor: pointer;
outline: inherit;
}
.card {
perspective: 1000px;
}
.card-inner {
display: grid;
transform: scale(var(--scale)) rotateY(var(--rotateDelta));
transform-style: preserve-3d;
}
/* Position the front and back side */
.card-front,
.card-back {
grid-area: 1/1;
backface-visibility: hidden;
}
/* Rotate the back side 180 degrees */
.card-back {
transform: rotateY(180deg);
}
</style>
You can preview the result here.
Rotating the card with the cursor
Now comes my favorite part, rotating the card around with your cursor! We want to create an enjoyable experience by letting the user control the card beyond just popping it up and allowing them to move the card freely.
We’ll also be using the spring
store here, but we’ll only need one this time, rotate
, which has an x and y-axis.
<script lang="ts">
import { spring } from 'svelte/motion';
const smooth = { stiffness: 0.066, damping: 0.25 };
const rotate = spring({ x: 0, y: 0 }, smooth);
const round = (num: number, fix = 3) => parseFloat(num.toFixed(fix));
function getMousePosition(e: MouseEvent | TouchEvent) {
if ('touches' in e) {
return {
x: e?.touches?.[0]?.clientX,
y: e?.touches?.[0]?.clientY,
};
} else {
return {
x: e.clientX,
y: e.clientY,
};
}
}
const interact = (e: MouseEvent | TouchEvent) => {
const { x: clientX, y: clientY } = getMousePosition(e);
const el = e.target as HTMLElement;
const rect = el.getBoundingClientRect(); // get element's current size/position
const absolute = {
x: clientX - rect.left, // get mouse position from left
y: clientY - rect.top, // get mouse position from right
};
const center = {
x: round((100 / rect.width) * absolute.x) - 50,
y: round((100 / rect.height) * absolute.y) - 50,
};
rotate.set({
x: round(-(center.x / 3.5)),
y: round(center.y / 2),
});
};
const interactEnd = () => {
setTimeout(() => {
rotate.set({ x: 0, y: 0 });
}, 500);
};
$: style = [`--rotateX: ${$rotate.x}deg`, `--rotateY: ${$rotate.y}deg`].join(';');
</script>
<div class="card" {style}>
<div class="card-inner" on:pointermove={interact} on:mouseout={interactEnd} on:blur={interactEnd}>
<div class="card-back">
<img
src="https://cloud.appwrite.io/v1/cards/cloud-back?mock=normal"
alt="The back of the Card"
loading="lazy"
width="450"
height="274"
/>
</div>
<div class="card-front">
<img
src="https://cloud.appwrite.io/v1/cards/cloud?mock=normal"
alt="The front of the card"
width="450"
height="274"
/>
</div>
</div>
</div>
<style>
.card {
perspective: 1000px;
}
.card-inner {
display: grid;
transform-style: preserve-3d;
transform: rotateY(var(--rotateX)) rotateX(var(--rotateY));
transform-origin: center;
}
/* Position the front and back side */
.card-front,
.card-back {
grid-area: 1/1;
backface-visibility: hidden;
}
/* Rotate the back side 180 degrees */
.card-back {
transform: rotateY(180deg);
}
</style>
You can see the result here.
Glare
Less but not least, we can also add a little bit of shine to the card, augmenting the 3D feel.
<script lang="ts">
import { spring } from 'svelte/motion';
const smooth = { stiffness: 0.066, damping: 0.25 };
const glare = spring({ x: 0, y: 0, o: 0 }, smooth);
const round = (num: number, fix = 3) => parseFloat(num.toFixed(fix));
function getMousePosition(e: MouseEvent | TouchEvent) {
if ('touches' in e) {
return {
x: e?.touches?.[0]?.clientX,
y: e?.touches?.[0]?.clientY,
};
} else {
return {
x: e.clientX,
y: e.clientY,
};
}
}
const interact = (e: MouseEvent | TouchEvent) => {
const { x: clientX, y: clientY } = getMousePosition(e);
const el = e.target as HTMLElement;
const rect = el.getBoundingClientRect(); // get element's current size/position
const absolute = {
x: clientX - rect.left, // get mouse position from left
y: clientY - rect.top, // get mouse position from right
};
glare.set({
x: round((100 / rect.width) * absolute.x),
y: round((100 / rect.height) * absolute.y),
o: 1,
});
console.log(absolute, round((100 / rect.width) * absolute.x));
};
const interactEnd = () => {
setTimeout(() => {
glare.update((old) => ({ ...old, o: 0 }));
}, 500);
};
$: style = [`--glareX: ${$glare.x}%`, `--glareY: ${$glare.y}%`, `--glareO: ${$glare.o}`].join(
';'
);
</script>
<div class="card" {style}>
<div class="card-inner" on:pointermove={interact} on:mouseout={interactEnd} on:blur={interactEnd}>
<div class="card-back">
<img
src="https://cloud.appwrite.io/v1/cards/cloud-back?mock=normal"
alt="The back of the Card"
loading="lazy"
width="450"
height="274"
/>
</div>
<div class="card-front">
<img
src="https://cloud.appwrite.io/v1/cards/cloud?mock=normal"
alt="The front of the card"
width="450"
height="274"
/>
<div class="card-glare" />
</div>
</div>
</div>
<style>
.card {
perspective: 1000px;
}
.card-inner {
display: grid;
transform-style: preserve-3d;
transform: rotateY(var(--rotateX)) rotateX(var(--rotateY));
transform-origin: center;
}
/* Position the front and back side */
.card-front,
.card-back {
grid-area: 1/1;
backface-visibility: hidden;
}
.card-front {
display: grid;
}
.card-front > * {
grid-area: 1/1;
}
/* Rotate the back side 180 degrees */
.card-back {
transform: rotateY(180deg);
}
.card-glare {
border-radius: 14px;
transform: translateZ(1px);
z-index: 4;
background: radial-gradient(
farthest-corner circle at var(--glareX) var(--glareY),
rgba(255, 255, 255, 0.8) 10%,
rgba(255, 255, 255, 0.65) 20%,
rgba(0, 0, 0, 0.5) 90%
);
mix-blend-mode: overlay;
opacity: calc(var(--glareO) * 0.5);
}
</style>
You can view the result here.
The End Result
The end result was a smooth and visually appealing card animation that added a touch of interactivity to our dashboard.
We hope this detailed breakdown of our implementation process is helpful for other developers looking to add similar effects to their web applications. And we also hope it inspired a bit of awe with the magic that is frontend development 🪄
Thank you for choosing Appwrite Cloud Beta for your cloud computing needs!
Top comments (3)
Thanks for sharing! 🙏
Amazing
I am also made a CSS 3D Card Hover and Flip Effect
Amazing