shadimage
xcn
componentanimated-testimonialssource: ok · refreshed

Animated Testimonials

Animated testimonials with blur effect.

npx shadcn add "https://ui.radiumcoders.com/r/xcn/animated-testimonials.json"
free

Where it goes

testimonial

Visual style

glass-blur

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion

Files1 · source cached 10/8/2026, 4:44:25 AM

components/xcn/animated-testimonials.tsx
import { motion } from "motion/react"
import { useState } from "react"
import { cn } from "@/lib/utils"

export type Testimonial = {
  content: string
  image?: string
}

type AnimatedTestimonialsProps = {
  testimonials: Array<Testimonial>
  className?: string
  itemClassName?: string
  avatarClassName?: string
  initialBlur?: number
  inactiveBlur?: number
  inactiveOpacity?: number
  transitionDuration?: number
  onActiveIndexChange?: (index: number | null) => void
}

export function AnimatedTestimonials({
  testimonials,
  className,
  itemClassName,
  avatarClassName,
  initialBlur = 10,
  inactiveBlur = 4,
  inactiveOpacity = 0.5,
  transitionDuration = 0.2,
  onActiveIndexChange,
}: AnimatedTestimonialsProps) {
  const [activeIndex, setActiveIndex] = useState<number | null>(null)

  const setIndex = (index: number | null) => {
    setActiveIndex(index)
    onActiveIndexChange?.(index)
  }

  return (
    <div
      className={cn(
        "p-2 text-lg text-balance sm:text-xl md:text-2xl lg:text-3xl",
        className
      )}
    >
      {testimonials.map((testimonial, idx) => (
        <motion.span
          key={testimonial.content}
          className={cn("inline", itemClassName)}
          initial={{ filter: `blur(${initialBlur}px)`, opacity: 0 }}
          animate={{
            filter:
              activeIndex === null
                ? "blur(0px)"
                : activeIndex === idx
                  ? "blur(0px)"
                  : `blur(${inactiveBlur}px)`,
            opacity:
              activeIndex === null ? 1 : activeIndex === idx ? 1 : inactiveOpacity,
          }}
          transition={{ duration: transitionDuration }}
          onMouseEnter={() => setIndex(idx)}
          onMouseLeave={() => setIndex(null)}
        >
          {testimonial.image ? (
            <img
              src={testimonial.image}
              alt={testimonial.content}
              className={cn(
                "mr-2 inline-block size-9 rounded-full bg-primary align-middle",
                avatarClassName
              )}
            />
          ) : (
            <span
              className={cn(
                "mr-2 inline-block size-9 rounded-full bg-primary align-middle",
                avatarClassName
              )}
            />
          )}
          {testimonial.content}{" "}
        </motion.span>
      ))}
    </div>
  )
}

More from xcn

Browse all components in this library →