Cards
FlipVault
The whole card turns over on click, Enter or Space to show its back. Reduced motion cross-fades instead.
Download
- React
- Tailwind CSS v4
- motion
components/ui/flip-vault.tsx
"use client";
import { useState, type ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type FlipVaultProps = {
/** Front face. Text only: the whole card is one button, so no links or buttons inside. */
front: ReactNode;
/** Back face, shown after a click, Enter or Space. */
back: ReactNode;
className?: string;
backClassName?: string;
};
/* FlipVault: the whole card turns over on click or keyboard to show its back. */
export function FlipVault({
front,
back,
className = "rounded-2xl border border-white/10 bg-[#16132a] p-6 text-white",
backClassName = "rounded-2xl bg-[#a78bfa] p-6 text-[#1a1530]",
}: FlipVaultProps) {
const [flipped, setFlipped] = useState(false);
const reduce = useReducedMotion();
const fade = { transition: "opacity 0.3s" };
return (
<button
type="button"
aria-pressed={flipped}
onClick={() => setFlipped((f) => !f)}
className="block w-full rounded-2xl text-left focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#a78bfa]"
style={{ perspective: 1000 }}
>
<motion.span
className="grid"
style={{ transformStyle: "preserve-3d" }}
initial={false}
animate={{ rotateY: reduce ? 0 : flipped ? 180 : 0 }}
transition={{ type: "spring", stiffness: 120, damping: 16 }}
>
<span
aria-hidden={flipped}
className={`block [grid-area:1/1] [backface-visibility:hidden] ${className}`}
style={reduce ? { ...fade, opacity: flipped ? 0 : 1 } : undefined}
>
{front}
</span>
<span
aria-hidden={!flipped}
className={`block [grid-area:1/1] [backface-visibility:hidden] ${backClassName}`}
style={reduce ? { ...fade, opacity: flipped ? 1 : 0 } : { transform: "rotateY(180deg)" }}
>
{back}
</span>
</motion.span>
</button>
);
}
1. Install the dependency.
npm i motion2. Paste the Code tab into
components/ui/flip-vault.tsx. The styles are Tailwind CSS v4 classes.3. Use it. This is the preview above:
app/flip-vault-demo.tsx
import { FlipVault } from "@/components/ui/flip-vault"; export default function FlipVaultExample() { return ( <div className="grid h-full place-items-center bg-[#0b0a14] p-6"> <div className="w-full max-w-[300px]"> <FlipVault front={ <> <span className="block font-mono text-[11px] uppercase tracking-[0.2em] text-[#a78bfa]">Question</span> <span className="mt-3 block text-2xl font-semibold">Can I use it in client work?</span> <span className="mt-2 block text-sm text-white/60">Click the card.</span> </> } back={ <> <span className="block font-mono text-[11px] uppercase tracking-[0.2em]">Answer</span> <span className="mt-3 block text-2xl font-semibold">Yes, as many projects as you like.</span> <span className="mt-2 block text-sm opacity-70">Click again to turn back.</span> </> } /> </div> </div> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| front | ReactNode | — | Front face. Text only: the card is one button, so no links or buttons inside. |
| back | ReactNode | — | Back face. |
| className | string | dark violet card | Look of the front. |
| backClassName | string | solid violet card | Look of the back. |