React + MotionMicrointeracciones
Reacciones flotantes
Una barra de reacciones tipo Dock: los emojis crecen al acercarte y cada toque suelta unos cuantos que suben flotando.
Ajustes
3
Código
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={"Reaccionar con " + 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 para IA
Pégalo en ChatGPT, Claude o Cursor y adaptará el bloque a tu proyecto con estos ajustes, aunque no uses React.
Más en Microinteracciones
Dónde usarlo
Las reacciones con emojis flotantes le dan ruido a un momento en directo: un streaming, un webinar, un evento online o el final de un post. La barra se amplía como el Dock de macOS bajo el cursor, y cada toque suelta de uno a seis emojis que suben girando, como un público de verdad.
La lupa necesita ratón; en el móvil la gente toca y los emojis suben igual. Quédate en cinco o seis reacciones. Los emojis se ven algo distintos en cada sistema, así que revisa el conjunto en un iPhone y en un Android.