Telerik blogs

Learn how to create a 3D card flip animation using Tailwind CSS.

CSS has come a long way in the past 10 years. It is now easier than ever to create 3D animations. Tailwind makes this even easier by including your CSS directly in your class attributes.

animated card flip. front is blue and back is green

TL;DR

It requires minimal JavaScript to create animations. Tailwind 4 will add 3D space functions, but you can achieve this today with arbitrary values in Tailwind 3 or using external Tailwind plugins. Creating a rotating card has never been simpler.

Tailwind and Framework Setup

This demo can work in any framework, although I am using SvelteKit for simplicity. After installing Tailwind, we will use some helper classes.

  • npm i -D tailwind-merge clsx
  • Tailwind Merge allows you to compile the classes you use and eliminate the ones you don’t when passing classes between components.
  • clsx is a tiny utility allowing you to construct HTML class conditionals.

Together, these two elements have been normalized to create the cn function in large UI frameworks like shadcn.

// lib/utils.ts

import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
	return twMerge(clsx(inputs));
}

Components

We need a back and a front component to display.

Front

<div
	class="flex h-full w-full flex-col items-center justify-center rounded-xl border bg-blue-700 text-white"
>
	Front
</div>

Back

<div
	class="flex h-full w-full flex-col items-center justify-center rounded-xl bg-emerald-700 text-white"
>
	Back
</div>

You can use whatever classes you want to format the card, but you will need the size-full or both w-full and h-full to make sure the card gets covered completely.

Basic Button Container

Because we’ll flip the card by clicking, the best practice for accessibility is to use a button element to avoid confusion with screen readers.

<button type="button" onclick={() => {}} class="size-96 outline-none">
	<div class="relative size-full">
		<div class="absolute inset-0 size-full">
			<Front />
		</div>
		<div class="absolute inset-0 size-full">
			<Back />
		</div>
	</div>
</button>
  • We want type="button" to ensure a click action and not a page refresh.
  • We will add an onclick action later.
  • Chrome adds an extra border for standard buttons on select events, so we remove this with outline-none.
  • We have an outer container with a relative display position, with two absolute and inset-0 child containers, for exact sizing when we use our 3D transitions.
  • We want size-full on all three div elements to match our maximum card sizes.

Add the 3D Effect

<button type="button" onclick={() => {}} class="size-96 outline-none [perspective:100rem]">
	<div class={cn('relative size-full transition duration-500 [transform-style:preserve-3d]')}>
		<div class="absolute inset-0 size-full [backface-visibility:hidden]">
			<Front />
		</div>
		<div
			class="absolute inset-0 size-full [backface-visibility:hidden] [transform:rotateY(180deg)]"
		>
			<Back />
		</div>
	</div>
</button>
  • We add [perspective:50rem] for setting the root 3D space perspective. The Tailwind 4 equivalent is perspective-midrange.
  • We add [transform-style:preserve-3d] to confirm the child elements will still be in the 3D space. The Tailwind 4 equivalent is transform-3d.
  • We add [backface-visibility:hidden] to both sides of the card to hide the other side of the element when each side is visible. The Tailwind 4 equivalent is backface-hidden.
  • We add [transform:rotateY(180deg)] to the back side of the card to set the backside as the back. The Tailwind 4 equivalent is rotate-y-180.

If you’re using an older version of Tailwind, you may prefer the tailwind-3dtransform-plugin, among others. However, I prefer to put the CSS directly in the class with [] until Tailwind 4 is stable and widely used.

The Flip

To flip the card, we need a front and back state. The best way to represent this is with a boolean that can be reactive.

const card = $state({
	showBack: false,
	flip() {
		this.showBack = !this.showBack;
	}
});

I’m using a $state() variable in Svelte, but this could easily be translated to useState or other types of signal() functions, depending on your framework.

  • We flip the card with card.flip().
  • We must dynamically add [transform:rotateY(180deg)] to our root element when we want to flip. Remember this is rotate-y-180 in Tailwind 4.
  • We can add transition duration-1000 to have a second animation effect.
<button type="button" onclick={() => card.flip()} class="size-96 outline-none [perspective:50rem]">
	<div
		class={cn(
			'relative size-full transition duration-1000 [transform-style:preserve-3d]',
			card.showBack && '[transform:rotateY(180deg)]'
		)}
	>
		<div class="absolute inset-0 size-full [backface-visibility:hidden]">
			<Front />
		</div>
		<div
			class="absolute inset-0 size-full [backface-visibility:hidden] [transform:rotateY(180deg)]"
		>
			<Back />
		</div>
	</div>
</button>

Notice we programmatically add the rotation thanks to our nifty cn() utility function.

card.showBack && '[transform:rotateY(180deg)]'

Bonus: Flip with the Space Bar

If we’re trying to show the back of the card for studying a flashcard, and we want to flip the card with a keystroke, we need to call the flip() function from that keystroke.

useKey

We can manually listen to a keystroke with the JS event listener, but we can also make this easy.

import { onMount } from 'svelte';

export function useKey(
    key: string | string[],
    callback: (event: KeyboardEvent) => void,
    options: {
        event?: 'keydown' | 'keyup' | 'keypress',
        preventDefault?: boolean,
    } = {}
) {
    const {
        event = 'keydown',
        preventDefault = false,
    } = options;

    function handler(e: KeyboardEvent) {
        const keys = Array.isArray(key) ? key : [key];
        if (keys.includes(e.key)) {
            if (preventDefault) {
                e.preventDefault();
            }
            callback(e);
        }
    }

    onMount(() => {
        addEventListener(event, handler);
        return () => {
            removeEventListener(event, handler);
        };
    });
}

I modified useKey to work with Svelte, but there are prewritten packages and templates for most frameworks:

Flip with Space

useKey(' ', () => card.flip());

That’s It!

Beautiful, right?


CSS
About the Author

Jonathan Gamble

Jonathan Gamble has been an avid web programmer for more than 20 years. He has been building web applications as a hobby since he was 16 years old, and he received a post-bachelor’s in Computer Science from Oregon State. His real passions are language learning and playing rock piano, but he never gets away from coding. Read more from him at https://code.build/.

 

 

Related Posts

Comments

Comments are disabled in preview mode.