Baktiyar Ablaikhan

Emoji reactions

Source
After bencho.dev ↗
Author
Interaction after bencho.dev
Licence
Original code — interaction after bencho.dev

Loading…

Tap a reaction to add yours, or open the tray to pick a new one.

Code

"use client";

import { AnimatePresence, motion } from "motion/react";
import { SmilePlus } from "lucide-react";
import { useState } from "react";
import { Surface } from "../ui";

const TRAY = ["🚀", "🔥", "👏", "😍", "🎉", "👀"];

type Reaction = { emoji: string; count: number; mine: boolean };
type Float = { id: number; emoji: string; x: number; drift: number };

function Count({ value }: { value: number }) {
  return (
    <span className="relative inline-grid h-[1.3em] overflow-hidden tabular-nums">
      <AnimatePresence mode="popLayout" initial={false}>
        <motion.span
          key={value}
          initial={{ y: "-100%", opacity: 0 }}
          animate={{ y: "0%", opacity: 1 }}
          exit={{ y: "100%", opacity: 0 }}
          transition={{ type: "spring", stiffness: 400, damping: 30 }}
        >
          {value}
        </motion.span>
      </AnimatePresence>
    </span>
  );
}

export default function EmojiReactions() {
  const [items, setItems] = useState<Reaction[]>([
    { emoji: "🚀", count: 3, mine: false },
    { emoji: "🔥", count: 2, mine: false },
  ]);
  const [tray, setTray] = useState(false);
  const [floats, setFloats] = useState<Float[]>([]);
  const [fid, setFid] = useState(0);

  const burst = (emoji: string, x: number) => {
    const made = [0, 1, 2].map((k) => ({ id: fid + k, emoji, x, drift: (k - 1) * 1.4 }));
    setFid((n) => n + 3);
    setFloats((f) => [...f, ...made]);
  };

  const toggle = (emoji: string, el?: HTMLElement) => {
    const x = el ? el.offsetLeft + el.offsetWidth / 2 : 40;
    setItems((list) => {
      const hit = list.find((r) => r.emoji === emoji);
      if (!hit) return [...list, { emoji, count: 1, mine: true }];
      return list
        .map((r) => (r.emoji === emoji ? { ...r, mine: !r.mine, count: r.count + (r.mine ? -1 : 1) } : r))
        .filter((r) => r.count > 0);
    });
    const hit = items.find((r) => r.emoji === emoji);
    if (!hit || !hit.mine) burst(emoji, x);
  };

  return (
    <Surface>
      <div className="relative w-[19em]">
        <div className="mb-[0.6em] w-fit rounded-[1.1em] bg-white px-[1.1em] py-[0.7em] text-[0.95em] shadow-[0_6px_20px_-14px_rgba(0,0,0,0.3)]">
          Shipped the new onboarding flow
        </div>
        <motion.div layout className="relative flex flex-wrap items-center gap-[0.45em]">
          <AnimatePresence initial={false}>
            {items.map((r) => (
              <motion.button
                layout
                key={r.emoji}
                type="button"
                initial={{ scale: 0, opacity: 0 }}
                animate={{ scale: 1, opacity: 1 }}
                exit={{ scale: 0, opacity: 0 }}
                whileTap={{ scale: 0.88 }}
                onClick={(e) => toggle(r.emoji, e.currentTarget)}
                className={`flex h-[2.3em] items-center gap-[0.45em] rounded-full px-[0.8em] text-[0.9em] transition-colors ${r.mine ? "bg-[#e8ecfb] text-[#2B46B8] shadow-[inset_0_0_0_1.5px_#2B46B8]" : "bg-white shadow-[0_2px_8px_-4px_rgba(0,0,0,0.2)]"}`}
              >
                <motion.span key={`${r.emoji}-${r.mine}`} initial={{ scale: 0.6, rotate: -20 }} animate={{ scale: 1, rotate: 0 }} transition={{ type: "spring", stiffness: 500, damping: 12 }}>
                  {r.emoji}
                </motion.span>
                <Count value={r.count} />
              </motion.button>
            ))}
          </AnimatePresence>
          <motion.button
            layout
            type="button"
            aria-label="Add reaction"
            onClick={() => setTray((t) => !t)}
            whileTap={{ scale: 0.88 }}
            className={`grid h-[2.3em] w-[2.3em] place-items-center rounded-full ${tray ? "bg-[#151515] text-white" : "bg-white text-[#8a8984]"} shadow-[0_2px_8px_-4px_rgba(0,0,0,0.2)]`}
          >
            <SmilePlus className="h-[1.1em] w-[1.1em]" />
          </motion.button>

          {floats.map((f) => (
            <motion.span
              key={f.id}
              className="pointer-events-none absolute top-0 text-[1.3em]"
              style={{ left: f.x }}
              initial={{ y: 0, x: "-50%", opacity: 1, scale: 0.6 }}
              animate={{ y: "-3.5em", x: `calc(-50% + ${f.drift}em)`, opacity: 0, scale: 1.2 }}
              transition={{ duration: 0.9, ease: "easeOut", delay: Math.abs(f.drift) * 0.05 }}
              onAnimationComplete={() => setFloats((all) => all.filter((x) => x.id !== f.id))}
            >
              {f.emoji}
            </motion.span>
          ))}
        </motion.div>
        <AnimatePresence>
          {tray && (
            <motion.div
              initial={{ opacity: 0, y: 8, scale: 0.95 }}
              animate={{ opacity: 1, y: 0, scale: 1 }}
              exit={{ opacity: 0, y: 8, scale: 0.95 }}
              className="absolute left-0 top-full mt-[0.6em] flex gap-[0.2em] rounded-full bg-white p-[0.3em] shadow-[0_10px_30px_-12px_rgba(0,0,0,0.3)]"
            >
              {TRAY.map((e, i) => (
                <motion.button
                  key={e}
                  type="button"
                  initial={{ opacity: 0, y: 6 }}
                  animate={{ opacity: 1, y: 0, transition: { delay: i * 0.03 } }}
                  whileHover={{ scale: 1.35, y: -4 }}
                  whileTap={{ scale: 0.9 }}
                  onClick={() => {
                    toggle(e);
                    setTray(false);
                  }}
                  className="grid h-[2em] w-[2em] place-items-center rounded-full text-[1.1em] hover:bg-[#f1f0ec]"
                >
                  {e}
                </motion.button>
              ))}
            </motion.div>
          )}
        </AnimatePresence>
      </div>
    </Surface>
  );
}
Prompt

Build "Emoji reactions", a button animation. What it does: Tap a reaction to add yours, or open the tray to pick a new one. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using lucide-react, motion. Technique: an HTML canvas, the Motion library, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://bencho.dev (Interaction after bencho.dev, Original code — interaction after bencho.dev).