Spotlight Quotes
A wall of quotes rendered as one flowing paragraph, each seeded with the author's avatar inline. Hovering a quote sharpens it while the rest blur back, and a small name/role card lifts out of the avatar. Only the hovered id drives state, so just the active quote and its neighbours ever re-style.
TextReactMotionTailwind CSS
CSSTailwind
Manual
Create a file and paste the following code into it.
spotlight-quotes.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"use client";
import { useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { cn } from "@/lib/cn";
/* ------------------------------------------------------------------ */
/* SpotlightQuotes */
/* ------------------------------------------------------------------ */
export interface Testimonial {
id: string;
text: string;
author: { name: string; role: string; avatar: string };
}
interface SpotlightQuotesProps {
testimonials: Testimonial[];
/** Color of the lifted author name — the one accent in the flow [Optional, default: "#f97316"] */
accentColor?: string;
/** Blur applied to non-hovered quotes in px [Optional, default: 5] */
blurAmount?: number;
/** Opacity of blurred quotes [Optional, default: 0.25] */
blurOpacity?: number;
/** Avatar diameter in px [Optional, default: 32] */
avatarSize?: number;
/** Base font size in px [Optional, default: 30] */
fontSize?: number;
className?: string;
}
/**
* A wall of quotes rendered as one flowing paragraph, each seeded with the
* author's avatar inline. Hovering a quote sharpens it while the rest blur
* back, and a small name/role card lifts out of the avatar. State is the one
* hovered id, so only the active quote and its neighbours ever re-style.
* @param {Testimonial[]} testimonials - Quotes with inline author avatars [Required]
* @param {number} fontSize - Base font size in px [Optional, default: 30]
*/
export function SpotlightQuotes({
testimonials,
accentColor = "#f97316",
blurAmount = 5,
blurOpacity = 0.25,
avatarSize = 32,
fontSize = 30,
className,
}: SpotlightQuotesProps) {
const [hovered, setHovered] = useState<string | null>(null);
return (
<div
className={cn("font-medium tracking-tight text-zinc-900 dark:text-zinc-100", className)}
style={{ fontSize: `${fontSize}px`, lineHeight: 1.35 }}
>
{testimonials.map((item, index) => {
const isActive = hovered === item.id;
const isDimmed = hovered !== null && !isActive;
const restOpacity = index % 2 !== 0 ? 0.7 : 1;
return (
<span
key={item.id}
onMouseEnter={() => setHovered(item.id)}
onMouseLeave={() => setHovered(null)}
style={{
filter: isDimmed ? `blur(${blurAmount}px)` : "none",
opacity: isDimmed ? blurOpacity : isActive ? 1 : restOpacity,
transition: "filter 0.3s ease, opacity 0.3s ease",
cursor: "default",
}}
>
<span
className="relative inline-block"
style={{ width: avatarSize, height: avatarSize, verticalAlign: "middle", marginRight: 3 }}
>
<img
src={item.author.avatar}
alt={item.author.name}
width={avatarSize}
height={avatarSize}
style={{ width: avatarSize, height: avatarSize, borderRadius: "50%", objectFit: "cover", display: "block" }}
/>
<AnimatePresence>
{isActive && (
<motion.span
initial={{ opacity: 0, x: -6 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -6 }}
transition={{ duration: 0.16, ease: "easeOut" }}
style={{
position: "absolute",
left: avatarSize / 2,
bottom: "150%",
transform: "translateY(-50%)",
display: "inline-flex",
flexDirection: "column",
gap: 3,
whiteSpace: "nowrap",
pointerEvents: "none",
zIndex: 20,
}}
>
<span className="text-[12px] font-semibold leading-tight" style={{ color: accentColor }}>
{item.author.name}
</span>
<span className="text-[9px] font-medium uppercase leading-tight tracking-[0.1em] text-zinc-500 dark:text-zinc-400">
{item.author.role}
</span>
</motion.span>
)}
</AnimatePresence>
</span>
<span>{item.text} </span>
</span>
);
})}
</div>
);
}
Update the import paths to match your project setup.
Similar components
Install via CLI
Resource details
PublishedJuly 17, 2026
CategoryText
ReactMotionTailwind CSS
Install via CLI
Resource details
PublishedJuly 17, 2026
CategoryText
ReactMotionTailwind CSS