Réactions emoji flottantes
Une barre de réactions façon Dock : les emojis grossissent à l’approche et chaque toucher en fait monter quelques-uns.
Réglages
Code
import { useRef, useState } from "react";
import { AnimatePresence, motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "framer-motion";
import type { MotionValue } from "framer-motion";
interface Floater {
id: number;
emoji: string;
x: number;
drift: number;
rot: number;
size: number;
rise: number;
delay: number;
}
interface DockEmojiProps {
emoji: string;
mouseX: MotionValue<number>;
onReact: (emoji: string, clientX: number) => void;
}
function DockEmoji({ emoji, mouseX, onReact }: DockEmojiProps) {
const ref = useRef<HTMLButtonElement>(null);
const distance = useTransform(mouseX, (x) => {
const r = ref.current?.getBoundingClientRect();
return r ? x - (r.left + r.width / 2) : Infinity;
});
const size = useSpring(useTransform(distance, [-96, 0, 96], [40, 56, 40]), {
stiffness: 420,
damping: 30,
mass: 0.4,
});
const fontSize = useTransform(size, (s) => s * 0.56);
return (
<motion.button
ref={ref}
type="button"
aria-label={"Réagir avec " + emoji}
onClick={(e) => onReact(emoji, e.currentTarget.getBoundingClientRect().left + e.currentTarget.offsetWidth / 2)}
whileTap={{ scale: 0.8 }}
transition={{ type: "spring", stiffness: 600, damping: 20 }}
style={{ width: size, height: size, fontSize }}
className="grid shrink-0 place-items-center rounded-full leading-none outline-none focus-visible:ring-2 focus-visible:ring-black/20 dark:focus-visible:ring-white/30"
>
{emoji}
</motion.button>
);
}
interface LiveReactionsProps {
emojis?: string;
burst?: number;
}
export default function LiveReactions({
emojis = "👍 ❤️ 😂 😮 🔥",
burst = 3,
}: LiveReactionsProps) {
const list = emojis.split(/\s+/).filter(Boolean).slice(0, 6);
const [floaters, setFloaters] = useState<Floater[]>([]);
const stage = useRef<HTMLDivElement>(null);
const nextId = useRef(0);
const mouseX = useMotionValue(Infinity);
const reduce = useReducedMotion();
const react = (emoji: string, clientX: number) => {
const left = stage.current?.getBoundingClientRect().left ?? 0;
const items = Array.from({ length: burst }, (_, i) => ({
id: nextId.current++,
emoji,
x: clientX - left + (Math.random() - 0.5) * 14,
drift: (Math.random() - 0.5) * 56,
rot: (Math.random() - 0.5) * 40,
size: 22 + Math.random() * 12,
rise: 120 + Math.random() * 60,
delay: i * 0.08,
}));
setFloaters((f) => [...f, ...items].slice(-36));
};
return (
<div ref={stage} className="relative flex h-60 w-full max-w-xs items-end justify-center">
<div className="pointer-events-none absolute inset-0" aria-hidden="true">
<AnimatePresence>
{floaters.map((p) => (
<motion.span
key={p.id}
className="absolute bottom-14 select-none leading-none"
style={{ left: p.x, fontSize: p.size, marginLeft: -p.size / 2 }}
initial={{ y: 0, opacity: 0, scale: 0.4 }}
animate={{
y: reduce ? -40 : -p.rise,
x: reduce ? 0 : [0, p.drift * 0.5, -p.drift * 0.3, p.drift],
rotate: reduce ? 0 : [0, p.rot, -p.rot * 0.5, p.rot * 0.3],
opacity: [0, 1, 1, 0],
scale: [0.4, 1.1, 1, 0.9],
}}
transition={{ duration: 1.8, delay: p.delay, ease: [0.22, 1, 0.36, 1] }}
onAnimationComplete={() => setFloaters((f) => f.filter((q) => q.id !== p.id))}
>
{p.emoji}
</motion.span>
))}
</AnimatePresence>
</div>
<div
onPointerMove={(e) => mouseX.set(e.clientX)}
onPointerDown={(e) => mouseX.set(e.clientX)}
onPointerLeave={() => mouseX.set(Infinity)}
className="relative flex items-end gap-1 rounded-full bg-white/85 p-1.5 shadow-[0_0_0_1px_rgba(0,0,0,0.05),0_1px_2px_rgba(0,0,0,0.05),0_12px_32px_-8px_rgba(0,0,0,0.16)] backdrop-blur-xl dark:bg-[#2c2c2e]/85 dark:shadow-[0_0_0_1px_rgba(255,255,255,0.08),0_12px_32px_-8px_rgba(0,0,0,0.6)]"
>
{list.map((emoji, i) => (
<DockEmoji key={emoji + i} emoji={emoji} mouseX={mouseX} onReact={react} />
))}
</div>
</div>
);
}
Prompt pour l’IA
Collez-le dans ChatGPT, Claude ou Cursor : le bloc sera adapté à votre projet avec ces réglages, même sans React.
Plus dans Micro-interactions
Où l’utiliser
Les réactions emoji flottantes donnent de la vie à un moment en direct : un live, un webinaire, un événement en ligne ou même la fin d’un article. La barre s’agrandit comme le Dock de macOS sous le curseur, et chaque toucher libère un à six emojis qui montent en tournant, comme un vrai public.
L’effet loupe demande une souris ; sur mobile, on touche et les emojis montent quand même. Restez à cinq ou six réactions. Les emojis changent un peu d’un système à l’autre, vérifiez donc votre sélection sur un iPhone et sur un Android.