React + MotionMicrointeractions

Floating emoji reactions

A Dock‑style reaction bar where emojis grow as you approach, and each tap sends a few of them floating upwards.

Settings

3

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={"React with " + 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 for AI

Paste it into ChatGPT, Claude or Cursor and it will adapt the block to your project with these settings, even if you don’t use React.

More in Microinteractions

Where to use it

Floating emoji reactions give a live moment some noise: a livestream, a webinar, a virtual event, even the end of a blog post. The bar magnifies like the macOS Dock under the cursor, and every tap releases one to six emojis that drift and spin as they rise, like a real crowd.

The magnification needs a mouse; on phones people just tap and the emojis float up as usual. Stick to five or six reactions. Emojis look a bit different on every system, so check the set on both an iPhone and an Android device.