Hand‑by‑hand Development of a Stunning Interactive Card Effect with React and Tailwind
This tutorial demonstrates how to create a visually striking card component with light‑following and 3D parallax effects using React, Tailwind CSS, and custom hooks, covering setup of three boxes, mouse event handling, CSS transforms, and reusable hook encapsulation.
In this guide we recreate the eye‑catching card animations seen on the Koudi AI platform, including a light‑following effect and a 3D perspective tilt. The implementation uses react components and tailwind for styling, with each card positioned relative so the light can be positioned absolutely.
Prepare three boxes
export default function Home() {
return (
)
}Implement the light‑following effect
'use client'
import { useRef, useState } from 'react'
export default function Home() {
const cardRef = useRef
(null)
const lightRef = useRef
(null)
const [isShowLight, setIsShowLight] = useState(false)
const [pos, setPos] = useState({ left: '0px', top: '0px' })
return (
) => {
if (cardRef.current) {
setIsShowLight(true)
const { x, y } = cardRef.current.getBoundingClientRect()
const { clientX, clientY } = e
setPos({
left: clientX - x - 100 + 'px',
top: clientY - y - 100 + 'px'
})
}
}}
onMouseLeave={() => setIsShowLight(false)}
ref={cardRef}
>
)
}Add 3D parallax rotation
onMouseMove={(e) => {
if (cardRef.current) {
const { x, y } = cardRef.current.getBoundingClientRect()
const { clientX, clientY } = e
const offsetX = clientX - x
const offsetY = clientY - y
const maxXRotation = 10
const maxYRotation = 10
const rangeX = 400 / 2
const rangeY = 400 / 2
const rotateX = ((offsetY - rangeY) / rangeY) * maxXRotation
const rotateY = -1 * ((offsetX - rangeX) / rangeX) * maxYRotation
cardRef.current.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`
}
}}Encapsulate the logic into a reusable Hook
'use client'
import { useRef, useState, useEffect } from 'react'
const useCardAnimation = () => {
const cardRef = useRef
(null)
const lightRef = useRef
(null)
const [isShowLight, setIsShowLight] = useState(false)
const [pos, setPos] = useState({ left: '0px', top: '0px' })
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (cardRef.current) {
setIsShowLight(true)
const { x, y } = cardRef.current.getBoundingClientRect()
const { clientX, clientY } = e
const offsetX = clientX - x
const offsetY = clientY - y
setPos({ left: offsetX - 100 + 'px', top: offsetY - 100 + 'px' })
const maxXRotation = 5
const maxYRotation = 5
const rangeX = 400 / 2
const rangeY = 400 / 2
const rotateX = ((offsetY - rangeY) / rangeY) * maxXRotation
const rotateY = -1 * ((offsetX - rangeX) / rangeX) * maxYRotation
cardRef.current.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`
}
}
const handleMouseLeave = () => {
setIsShowLight(false)
if (cardRef.current) {
cardRef.current.style.transform = `perspective(1000px) rotateX(0deg) rotateY(0deg)`
}
}
cardRef.current?.addEventListener('mousemove', handleMouseMove)
cardRef.current?.addEventListener('mouseleave', handleMouseLeave)
return () => {
cardRef.current?.removeEventListener('mousemove', handleMouseMove)
cardRef.current?.removeEventListener('mouseleave', handleMouseLeave)
}
}, [])
return { cardRef, lightRef, isShowLight, pos }
}
export default function Home() {
const { cardRef: cardRef1, lightRef: lightRef1, isShowLight: isShowLight1, pos: pos1 } = useCardAnimation()
const { cardRef: cardRef2, lightRef: lightRef2, isShowLight: isShowLight2, pos: pos2 } = useCardAnimation()
const { cardRef: cardRef3, lightRef: lightRef3, isShowLight: isShowLight3, pos: pos3 } = useCardAnimation()
return (
{/* three cards with their own hooks */}
)
}The article concludes with a demonstration of three independent cards each using the useCardAnimation hook, showing how the effect can be scaled across multiple components.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.