DEV Community

Thomas G. Lopes for Appwrite

Posted on

How we implemented the card animation in Appwrite Cloud Public Beta

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

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

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!

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:

<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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
devarshishimpi profile image
Devarshi Shimpi

Thanks for sharing! 🙏

Collapse
 
jon_snow789 profile image
Jon Snow

Amazing
I am also made a CSS 3D Card Hover and Flip Effect

Collapse
 
abhishek_writes profile image
Abhishek Kumar

Amazing