Animated Cards
In this craft, we will create a testimonial card that flips on clicks and shows the testimonial.

Faisal Husain
Sarah ChenProduct Manager at TechFlow
The attention to detail and innovative features have completely transformed our workflow. This is exactly what we've been looking for.This is just a extenstion of my previous craft 3D Flip Card where we created a 3D flip card. In this craft we will create a testimonial card that flips on clicks and shows the testimonial.
Here is code for the component -
'use client'
import { useState } from 'react'
import { AnimatePresence, motion } from 'motion/react'
import { ArrowBigLeft, ArrowBigRight } from 'lucide-react'
const GRID_SIZE = 10
const COUNT = GRID_SIZE * GRID_SIZE
const testimonials = [
{
quote:
"The attention to detail and innovative features have completely transformed our workflow. This is exactly what we've been looking for.",
name: 'Sarah Chen',
designation: 'Product Manager at TechFlow',
src:
'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=3560&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},
{
quote:
"Implementation was seamless and the results exceeded our expectations. The platform's flexibility is remarkable.",
name: 'Michael Rodriguez',
designation: 'CTO at InnovateSphere',
src:
'https://images.unsplash.com/photo-1560250097-0b93528c311a?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8cHJvZmVzc2lvbmFsJTIwbWFufGVufDB8fDB8fHww',
},
{
quote:
"This solution has significantly improved our team's productivity. The intuitive interface makes complex tasks simple.",
name: 'Emily Watson',
designation: 'Operations Director at CloudScale',
src:
'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8cHJvZmVzc2lvbmFsJTIwd29tYW58ZW58MHx8MHx8fDA%3D',
},
{
quote:
"Outstanding support and robust features. It's rare to find a product that delivers on all its promises.",
name: 'James Kim',
designation: 'Engineering Lead at DataPro',
src:
'https://images.unsplash.com/photo-1636041293178-808a6762ab39?q=80&w=3464&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},
{
quote:
'The scalability and performance have been game-changing for our organization. Highly recommend to any growing business.',
name: 'Lisa Thompson',
designation: 'VP of Technology at FutureNet',
src:
'https://images.unsplash.com/photo-1624561172888-ac93c696e10c?q=80&w=2592&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},
]
const Testimonial3DCard = () => {
const [flipped, setFlipped] = useState(false)
const [clickPosition, setClickPosition] = useState({ x: 0, y: 0 })
const [isAnimating, setIsAnimating] = useState(false)
const [currentIndex, setCurrentIndex] = useState(0)
const [nextIndex, setNextIndex] = useState(1)
const getManhattanDistance = (
x1: number,
y1: number,
x2: number,
y2: number,
) => {
return Math.abs(x2 - x1) + Math.abs(y2 - y1)
}
const maxDistance = getManhattanDistance(0, 0, GRID_SIZE - 1, GRID_SIZE - 1)
const totalAnimationDuration = maxDistance * 0.1 + 0.5
const getRandomGridPosition = () => ({
x: Math.floor(Math.random() * GRID_SIZE),
y: Math.floor(Math.random() * GRID_SIZE),
})
const triggerFlip = (randomPosition?: { x: number; y: number }) => {
const position = randomPosition || getRandomGridPosition()
setClickPosition(position)
setFlipped((prev) => !prev)
setIsAnimating(true)
setTimeout(() => {
if (!flipped) {
setCurrentIndex((pre) => (pre + 2) % testimonials.length)
} else {
setNextIndex((pre) => (pre + 2) % testimonials.length)
}
setIsAnimating(false)
}, totalAnimationDuration * 1000)
}
const handleNextClick = () => {
if (isAnimating) return
triggerFlip()
}
const handlePrevClick = () => {
if (isAnimating) return
triggerFlip()
}
return (
<div
className="bg-black ring-inset ring-2 ring-zinc-800 rounded-2xl items-center justify-center w-full grid md:grid-cols-2 grid-cols-1 p-5 "
style={
{
'--current-image': `url(${testimonials[currentIndex].src})`,
'--next-image': `url(${testimonials[nextIndex].src})`,
} as React.CSSProperties
}
>
<div className="flex flex-col items-center justify-center px-10 max-w-xl">
<AnimatePresence mode="wait">
<motion.div className=" text-white w-full h-40 ">
{flipped ? (
<motion.div
key="next"
initial={{ y: 20, opacity: 0, filter: 'blur(20px)' }}
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
exit={{ y: -20, opacity: 0, filter: 'blur(20px)' }}
transition={{
ease: 'easeInOut',
duration: totalAnimationDuration * 0.75,
}}
className="flex flex-col items-start space-y-4"
>
<div className="flex flex-col items-start">
<span className="text-2xl font-bold">
{testimonials[nextIndex].name}
</span>
<span className="text-xm text-gray-400 font-light">
{testimonials[nextIndex].designation}
</span>
</div>
<span className="text-sm">{testimonials[nextIndex].quote}</span>
</motion.div>
) : (
<motion.div
key="current"
initial={{ y: 20, opacity: 0, filter: 'blur(20px)' }}
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
exit={{ y: -20, opacity: 0, filter: 'blur(20px)' }}
transition={{
ease: 'easeInOut',
duration: totalAnimationDuration * 0.75,
}}
className="flex flex-col items-start space-y-4"
>
<div className="flex flex-col items-start">
<span className="text-2xl font-bold">
{testimonials[currentIndex].name}
</span>
<span className="text-xm text-gray-400 font-light">
{testimonials[currentIndex].designation}
</span>
</div>
<span className="text-sm">
{testimonials[currentIndex].quote}
</span>
</motion.div>
)}
</motion.div>
</AnimatePresence>
<div className="flex gap-4 mt-10 w-full z-50 ">
<button
onClick={handlePrevClick}
disabled={isAnimating}
className="size-8 rounded-full bg-gray-100 dark:bg-neutral-800 flex items-center justify-center group/button disabled:opacity-50"
>
<ArrowBigLeft className=" size-6 text-black dark:text-neutral-400 group-hover/button:rotate-12 transition-transform duration-300" />
</button>
<button
onClick={handleNextClick}
disabled={isAnimating}
className="size-8 rounded-full bg-gray-100 dark:bg-neutral-800 flex items-center justify-center group/button disabled:opacity-50"
>
<ArrowBigRight className="size-6 text-black dark:text-neutral-400 group-hover/button:-rotate-12 transition-transform duration-300" />
</button>
</div>
</div>
<div
className={`grid grid-cols-10 md:size-[300px] size-[300px] mx-auto ${
isAnimating ? 'pointer-events-none' : ''
} `}
>
{Array.from({ length: COUNT }, (_, index) => {
const x = index % GRID_SIZE
const y = Math.floor(index / GRID_SIZE)
return (
<motion.div
key={index}
className={`relative w-full h-full cursor-pointer [transform-style:preserve-3d] transition-transform duration-500`}
initial="initial"
animate={{
rotateX: flipped ? 180 : 0,
transition: {
rotateX: {
duration: 0.1,
delay:
getManhattanDistance(
clickPosition.x,
clickPosition.y,
x,
y,
) * 0.1,
},
},
}}
style={
{
'--x': x,
'--y': y,
'--grid-size': GRID_SIZE,
} as React.CSSProperties
}
>
<div
className="absolute w-full h-full bg-blue-500 text-center flex items-center justify-center text-xs md:text-base [backface-visibility:hidden] [transform:rotateY(0deg)]"
style={{
backgroundImage: 'var(--current-image)',
backgroundPosition: `calc(var(--x, 0) * -100%) calc(var(--y, 0) * -100%)`,
backgroundSize: `calc(var(--grid-size, 10) * 100%)`,
}}
/>
<div
className="absolute w-full h-full bg-red-500 text-center flex items-center justify-center text-xs md:text-base [backface-visibility:hidden] [transform:rotateX(180deg)]"
style={{
backgroundImage: 'var(--next-image)',
backgroundPosition: `calc(var(--x, 0) * -100%) calc(var(--y, 0) * -100%)`,
backgroundSize: `calc(var(--grid-size, 10) * 100%)`,
}}
/>
</motion.div>
)
})}
</div>
</div>
)
}
export default Testimonial3DCard
Resources and Important Links
- Link to my Craft. Click here
Want to hire me as a freelancer? Let's discuss.
Drop a message and let's discuss
Drop in your email ID and I will get back to you.