How we implemented the card animation in Appwrite Cloud Public Beta
Tags: webdev, frontend, svelte, css
Published: 2023-05-09 11:54:31

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


![Recording of the cloud card on console](https://cloud.appwrite.io/v1/storage/buckets/645a32f6c1de73d0babb/files/645a337ec39e649e2063/view?project=645a32f155b26e6044f9)


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!


![Designers happy with their design, while frontend developer drowns in fear](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ze0ul8qow977nquqba0o.png)



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/](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/](https://appwrite-card-snippets.vercel.app/)

If you’re curious, you can also check out the [source code](https://github.com/appwrite/console/blob/cloud-cards/src/routes/card/Card.svelte) 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! 

![https://media3.giphy.com/media/Gjy7TJTRD3dixQNNB7/giphy.gif?cid=ecf05e471wtvkifjomcdhcyk6tmnz9yh28ep7zleqe3cj0cq&ep=v1_gifs_search&rid=giphy.gif&ct=g](https://media3.giphy.com/media/Gjy7TJTRD3dixQNNB7/giphy.gif?cid=ecf05e471wtvkifjomcdhcyk6tmnz9yh28ep7zleqe3cj0cq&ep=v1_gifs_search&rid=giphy.gif&ct=g)

The result of this markup is the following:

```html
<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](https://appwrite-card-snippets.vercel.app/structure). 

### 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.

```html
<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](https://appwrite-card-snippets.vercel.app/popup).

### 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.

```html
<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](https://appwrite-card-snippets.vercel.app/rotate).

### Glare

Less but not least, we can also add a little bit of shine to the card, augmenting the 3D feel.

```html
<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](https://appwrite-card-snippets.vercel.app/glare).

## 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!