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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Hand‑by‑hand Development of a Stunning Interactive Card Effect with React and Tailwind

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 (
    <main className="min-h-screen p-24 flex justify-center items-center bg-black gap-5">
      <div className="w-[384px] h-[384px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F]"></div>
      <div className="w-[384px] h-[384px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F]"></div>
      <div className="w-[384px] h-[384px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F]"></div>
    </main>
  )
}

Implement the light‑following effect

'use client'
import { useRef, useState } from 'react'

export default function Home() {
  const cardRef = useRef<HTMLDivElement>(null)
  const lightRef = useRef<HTMLDivElement>(null)
  const [isShowLight, setIsShowLight] = useState(false)
  const [pos, setPos] = useState({ left: '0px', top: '0px' })

  return (
    <main className="h-screen p-24 flex justify-center items-center bg-black gap-5">
      <div
        className="w-[384px] h-[384px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F] relative overflow-hidden"
        onMouseMove={(e: React.MouseEvent<HTMLDivElement>) => {
          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}
      >
        <div
          className={`${isShowLight ? '' : 'hidden'} absolute h-[200px] w-[200px] rounded-full bg-[#ff4132] blur-[150px] filter`}
          ref={lightRef}
          style={pos}
        ></div>
      </div>
      <!-- additional boxes omitted for brevity -->
    </main>
  )
}

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<HTMLDivElement>(null)
  const lightRef = useRef<HTMLDivElement>(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 (
    <main className="h-screen p-24 flex justify-center items-center bg-black gap-5">
      {/* three cards with their own hooks */}
    </main>
  )
}

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontendJavaScriptReacthookscss3Tailwind CSSUI animation
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.