# Floating emoji reactions

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

- Category: [Microinteractions](https://library.aimran.es/components/microinteractions)
- Built with: React + Motion
- Live editor: https://library.aimran.es/blocks/live-floating-reactions
- Library: AIMRAN Library (https://library.aimran.es), free, no sign-up, by Aimran Oukham

## 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.

## Code

### React (Motion)

```tsx
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 assistants

Paste this into ChatGPT, Claude, Cursor, v0 or Lovable to adapt the block to your stack (Tailwind, Vue, Svelte…):

````text
I want to add the “Floating emoji reactions” block (Microinteractions) from AIMRAN Library to my project.

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

## Chosen settings
- Emojis (space separated): "👍 ❤️ 😂 😮 🔥"
- Per tap: 3 (from 1 to 6)

## What I need
1. Add it to my project with the same look and animation as the reference code: same sizes, colours, radii, shadows, timings and easing.
2. It is built with React + TypeScript, with framer-motion for the animation and lucide-react for the icons. If my project uses something else (Vue, Svelte, Astro, plain HTML…), translate it without losing any behaviour. If I use Tailwind, turn the styles into classes; if not, keep the CSS in its own file.
3. Expose the settings above as props (or CSS variables) with those values as defaults.
4. Make it work well on phones: nothing that depends only on hover, touch targets of at least 44 px and no overflow on 320 px screens.
5. Make it look right in light and dark mode.
6. Keep it accessible: semantic HTML, roles and aria-* where needed, visible focus and keyboard support.
7. Respect prefers-reduced-motion: with reduced motion, no big animations but the same end result.

## Reference code
```tsx
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>
  );
}
```

Give me the complete final code, ready to paste, and tell me in one line where to put it in my project.
````
