Emoji Reaction Bar — HTML CSS JS Snippet

Emoji Reaction Bar · Buttons · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Hover + tap reveal
The popover shows on hover for pointers and via an .open class toggled by tap for touch, so it works on every device.
Spring-up popover
The picker animates from scale(.9) with a bottom-right origin, appearing to grow out of the React button.
Scale-and-lift emojis
Each reaction grows to 1.45× and lifts on hover, the tactile bounce that makes the picker feel responsive and fun.
CSS-only tooltips
content: attr(data-label) on a pseudo-element labels each emoji on hover — no tooltip library or extra markup.
Single-reaction toggle
pickReaction enforces one reaction at a time and removes it when re-clicked, matching real social platforms.
Live count and trigger
updateReact rewrites the trigger to your emoji and label and adjusts the total by one in a single pass.
Overlapping count cluster
Top reactions stack as overlapping circles via negative margins; your new reaction is unshifted and ringed with a spring animation.
Outside-click dismissal
A document listener closes the popover on any outside click, with stopPropagation keeping the trigger from self-closing.

About this UI Snippet

Emoji Reaction Bar — Hover-Reveal Popover, Tooltip Reactions & Overlapping Count Cluster

Screenshot of the Emoji Reaction Bar snippet rendered live

A single "Like" button is binary; a reaction bar lets people respond with nuance — love, laughter, surprise, sadness, anger. Popularised by Facebook, the hover-reveal reaction picker is now a standard pattern in social feeds, comment threads, and chat. This snippet implements it in plain HTML, CSS, and vanilla JavaScript: a trigger that reveals a popover of emoji reactions on hover or tap, scale-and-tooltip animations on each emoji, single-reaction toggle logic, and an overlapping count cluster that reflects your choice.

Hover-reveal popover that also works on touch

The reactions live in an .er-pop popover positioned above the trigger. On devices with a pointer it appears on hover via .er-react:hover .er-pop, springing up from scale(.9) with its transform origin at the bottom-right. Hover does not exist on touch screens, so the trigger also has an onclick calling togglePop, which toggles an .open class that reveals the same popover — and a document click listener closes it when tapping elsewhere. stopPropagation on the trigger keeps that outside-click handler from immediately re-closing it.

Tactile emoji interactions

Each reaction is a button that scales to 1.45× and lifts on hover (transform: scale(1.45) translateY(-4px)), the playful "bounce" that makes the picker feel alive. A CSS-only tooltip — content: attr(data-label) on the ::after — shows the reaction name ("Love", "Haha") above the emoji on hover, so the meaning is never ambiguous. No tooltip library, no extra markup.

Single-reaction toggle

Real reaction systems let you have exactly one reaction at a time, and clicking your current reaction removes it. pickReaction implements this with one line: myReaction = (myReaction === emoji) ? null : emoji. updateReact then rewrites the trigger to show your chosen emoji and its label (or reverts to the neutral "React"), adjusts the total count by one, and rebuilds the summary cluster.

Overlapping count cluster

The summary shows the familiar overlapping circle cluster of top reactions with a total count. updateReact builds it from a base set of reactions; when you react with something not already shown, your emoji is unshifted to the front and highlighted with a ring and a spring er-bub animation, so you can immediately see your contribution. The overlap is pure CSS — negative margin-left with white borders to fake the stacked-coins look.

Everything is driven by a single myReaction variable and the updateReact function, so wiring it to a backend is a matter of POSTing the reaction and seeding real counts. Pair this with a favorite button for simple likes, a comment thread for discussions, or a social post card feed.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA social post card appears with a reaction summary (24 reactions) and a "🙂 React" button in the footer.
  2. 2
    Reveal the reactionsHover the React button (or tap it on touch) — a rounded popover of six emoji springs up above it.
  3. 3
    Hover an emojiEach emoji scales up and lifts, and a tooltip shows its name ("Love", "Haha", "Wow") above it.
  4. 4
    Pick a reactionClick one — the trigger changes to that emoji and label, the count ticks to 25, and your reaction joins the cluster with a highlighted ring.
  5. 5
    Change or remove itPick a different emoji to switch, or click your current reaction again to remove it — the count and cluster update accordingly.
  6. 6
    Dismiss the popoverClick anywhere outside to close the picker without changing your reaction.

Real-world uses

Common Use Cases

Social feeds and posts
Let users react to posts with nuance beyond a like. Drop it into a social post card or activity feed.
Comment threads
Add reactions to individual comments; pair with a comment thread so each reply can be reacted to.
Chat and messaging
Tap-and-hold style emoji reactions on chat bubbles — the popover and toggle logic map directly to message reactions.
Docs and knowledge bases
"Was this helpful?" with emoji reactions instead of a plain thumbs pair; a richer alternative to a favorite button.
Polls and feedback widgets
Quick sentiment capture on announcements or changelog entries; combine with a poll widget for structured votes.
Live events and streams
Floating emoji reactions during a broadcast; reuse the picker to choose which reaction to send.

Got questions?

Frequently Asked Questions

In pickReaction, after updating myReaction, POST the change to your API (the post id and the new reaction, or null to remove). Update optimistically as the snippet does, and reconcile with the server's authoritative counts on response. Seed BASE_COUNT and the cluster from real data on load so the UI reflects everyone's reactions, not just yours.

Replace the static BASE array and BASE_COUNT with data from your API: sort reaction types by count, take the top three for the cluster, and use the true total. Rebuild the cluster in updateReact from that data, still unshifting and highlighting the current user's reaction so it stands out.

Hover is fast and discoverable on desktop, but touch devices have no hover state — relying on it alone makes the picker unusable on phones. The snippet shows the popover on hover via CSS and also toggles it with a tap via togglePop, so both input types get a natural way to open it.

Make the emoji buttons keyboard-reachable (they are real <button>s) and give each an aria-label matching its data-label, since an emoji alone is not descriptive. Open the popover on focus as well as hover, support Escape to close, and announce reaction/count changes via an aria-live region so screen-reader users get feedback.

In React, hold myReaction and open in useState, derive the trigger label and cluster from myReaction, and add a click-outside effect. In Vue, use refs with @click/@mouseenter and a computed cluster. In Angular, track state on the component and bind [class.open]/[class.reacted]. The popover and emoji-hover CSS port unchanged.

Emoji Reaction Bar — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Emoji Reaction Bar</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
    .er-card{background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:18px;width:100%;max-width:380px;box-shadow:0 14px 44px rgba(15,23,42,.07)}
    .er-post{display:flex;align-items:center;gap:11px;margin-bottom:12px}
    .er-avatar{width:42px;height:42px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-weight:800;font-size:17px;display:flex;align-items:center;justify-content:center}
    .er-author{font-size:14px;font-weight:700;color:#1e293b}
    .er-time{font-size:11px;color:#94a3b8}
    .er-text{font-size:14px;color:#334155;line-height:1.55;margin-bottom:16px}
    
    .er-footer{display:flex;align-items:center;justify-content:space-between;border-top:1px solid #f1f5f9;padding-top:12px}
    .er-summary{display:flex;align-items:center;gap:8px}
    .er-cluster{display:flex}
    .er-bubble{width:24px;height:24px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.15);display:flex;align-items:center;justify-content:center;font-size:13px;margin-left:-7px;border:1.5px solid #fff}
    .er-bubble:first-child{margin-left:0}
    .er-bubble.mine{box-shadow:0 0 0 2px #6366f1;animation:er-bub .3s cubic-bezier(.2,1.6,.4,1)}
    @keyframes er-bub{from{transform:scale(0)}to{transform:scale(1)}}
    .er-total{font-size:12px;color:#64748b;font-weight:600}
    
    .er-react{position:relative}
    .er-trigger{display:flex;align-items:center;gap:6px;background:#f1f5f9;border:none;border-radius:9px;padding:8px 14px;font-size:13px;font-weight:700;color:#475569;cursor:pointer;font-family:inherit;transition:background .15s,color .15s}
    .er-trigger:hover{background:#e2e8f0}
    .er-trigger.reacted{background:#eef2ff;color:#4f46e5}
    .er-temoji{font-size:16px;line-height:1}
    
    .er-pop{position:absolute;bottom:calc(100% + 9px);right:0;display:flex;gap:3px;background:#fff;border:1px solid #e2e8f0;border-radius:999px;padding:6px 8px;box-shadow:0 10px 28px rgba(15,23,42,.18);opacity:0;visibility:hidden;transform:translateY(8px) scale(.9);transform-origin:bottom right;transition:opacity .2s,transform .2s,visibility .2s;z-index:5}
    .er-react:hover .er-pop,.er-react.open .er-pop{opacity:1;visibility:visible;transform:translateY(0) scale(1)}
    .er-emoji{position:relative;width:38px;height:38px;border:none;background:none;font-size:24px;cursor:pointer;border-radius:50%;transition:transform .15s;line-height:1}
    .er-emoji:hover{transform:scale(1.45) translateY(-4px)}
    .er-emoji::after{content:attr(data-label);position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:#1e293b;color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s}
    .er-emoji:hover::after{opacity:1}
  </style>
</head>
<body>
  <div class="er-card">
    <div class="er-post">
      <div class="er-avatar">N</div>
      <div>
        <div class="er-author">Nadia Okonkwo</div>
        <div class="er-time">2 hours ago</div>
      </div>
    </div>
    <p class="er-text">Just shipped the new dashboard 🚀 Months of work — would love your honest reactions!</p>
  
    <div class="er-footer">
      <div class="er-summary">
        <div class="er-cluster" id="erCluster"></div>
        <span class="er-total"><span id="erTotal">24</span> reactions</span>
      </div>
  
      <div class="er-react" id="erReact">
        <div class="er-pop" id="erPop">
          <button class="er-emoji" data-emoji="👍" data-label="Like" onclick="pickReaction(this)">👍</button>
          <button class="er-emoji" data-emoji="❤️" data-label="Love" onclick="pickReaction(this)">❤️</button>
          <button class="er-emoji" data-emoji="😂" data-label="Haha" onclick="pickReaction(this)">😂</button>
          <button class="er-emoji" data-emoji="😮" data-label="Wow" onclick="pickReaction(this)">😮</button>
          <button class="er-emoji" data-emoji="😢" data-label="Sad" onclick="pickReaction(this)">😢</button>
          <button class="er-emoji" data-emoji="😡" data-label="Angry" onclick="pickReaction(this)">😡</button>
        </div>
        <button class="er-trigger" id="erTrigger" onclick="togglePop(event)"><span class="er-temoji">🙂</span>React</button>
      </div>
    </div>
  </div>
  <script>
    var REACTIONS = { '👍': 'Like', '❤️': 'Love', '😂': 'Haha', '😮': 'Wow', '😢': 'Sad', '😡': 'Angry' };
    var BASE = ['👍', '❤️', '😂'];
    var BASE_COUNT = 24;
    var myReaction = null;
    var react = document.getElementById('erReact');
    
    function togglePop(e) {
      e.stopPropagation();
      react.classList.toggle('open');
    }
    
    function closePop() { react.classList.remove('open'); }
    
    function pickReaction(el) {
      var emoji = el.dataset.emoji;
      myReaction = (myReaction === emoji) ? null : emoji;
      updateReact();
      closePop();
    }
    
    function updateReact() {
      var trigger = document.getElementById('erTrigger');
      if (myReaction) {
        trigger.innerHTML = '<span class="er-temoji">' + myReaction + '</span>' + REACTIONS[myReaction];
        trigger.classList.add('reacted');
      } else {
        trigger.innerHTML = '<span class="er-temoji">🙂</span>React';
        trigger.classList.remove('reacted');
      }
    
      document.getElementById('erTotal').textContent = BASE_COUNT + (myReaction ? 1 : 0);
    
      var list = (myReaction && BASE.indexOf(myReaction) === -1) ? [myReaction].concat(BASE).slice(0, 3) : BASE.slice();
      document.getElementById('erCluster').innerHTML = list.map(function (e) {
        var mine = e === myReaction ? ' mine' : '';
        return '<span class="er-bubble' + mine + '">' + e + '</span>';
      }).join('');
    }
    
    document.addEventListener('click', function (e) {
      if (!react.contains(e.target)) closePop();
    });
    
    updateReact();
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Emoji Reaction Bar</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    @keyframes er-bub{from{transform:scale(0)}to{transform:scale(1)}}

    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
    }

    .er-bubble {
      width:24px;height:24px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.15);display:flex;align-items:center;justify-content:center;font-size:13px;margin-left:-7px;border:1.5px solid #fff
    }

    .er-bubble:first-child {
      margin-left:0
    }

    .er-bubble.mine {
      box-shadow:0 0 0 2px #6366f1;animation:er-bub .3s cubic-bezier(.2,1.6,.4,1)
    }

    .er-trigger.reacted {
      background:#eef2ff;color:#4f46e5
    }

    .er-temoji {
      font-size:16px;line-height:1
    }

    .er-react:hover .er-pop,.er-react.open .er-pop {
      opacity:1;visibility:visible;transform:translateY(0) scale(1)
    }

    .er-emoji::after {
      content:attr(data-label);position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:#1e293b;color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s
    }

    .er-emoji:hover::after {
      opacity:1
    }
  </style>
</head>
<body>
  <div class="bg-[#fff] border border-[#e2e8f0] rounded-2xl p-[18px] w-full max-w-[380px] shadow-[0_14px_44px_rgba(15,23,42,.07)]">
    <div class="flex items-center gap-[11px] mb-3">
      <div class="w-[42px] h-[42px] rounded-full [background:linear-gradient(135deg,#6366f1,#8b5cf6)] text-[#fff] font-extrabold text-[17px] flex items-center justify-center">N</div>
      <div>
        <div class="text-sm font-bold text-[#1e293b]">Nadia Okonkwo</div>
        <div class="text-[11px] text-[#94a3b8]">2 hours ago</div>
      </div>
    </div>
    <p class="text-sm text-[#334155] leading-[1.55] mb-4">Just shipped the new dashboard 🚀 Months of work — would love your honest reactions!</p>
  
    <div class="flex items-center justify-between border-t border-t-[#f1f5f9] pt-3">
      <div class="flex items-center gap-2">
        <div class="flex" id="erCluster"></div>
        <span class="text-xs text-[#64748b] font-semibold"><span id="erTotal">24</span> reactions</span>
      </div>
  
      <div class="er-react relative" id="erReact">
        <div class="er-pop absolute bottom-[calc(100% + 9px)] right-0 flex gap-[3px] bg-[#fff] border border-[#e2e8f0] rounded-[999px] py-1.5 px-2 shadow-[0_10px_28px_rgba(15,23,42,.18)] opacity-0 invisible [transform:translateY(8px)_scale(.9)] [transform-origin:bottom_right] [transition:opacity_.2s,transform_.2s,visibility_.2s] z-[5]" id="erPop">
          <button class="er-emoji relative w-[38px] h-[38px] border-0 bg-transparent text-2xl cursor-pointer rounded-full [transition:transform_.15s] leading-none hover:[transform:scale(1.45)_translateY(-4px)]" data-emoji="👍" data-label="Like" onclick="pickReaction(this)">👍</button>
          <button class="er-emoji relative w-[38px] h-[38px] border-0 bg-transparent text-2xl cursor-pointer rounded-full [transition:transform_.15s] leading-none hover:[transform:scale(1.45)_translateY(-4px)]" data-emoji="❤️" data-label="Love" onclick="pickReaction(this)">❤️</button>
          <button class="er-emoji relative w-[38px] h-[38px] border-0 bg-transparent text-2xl cursor-pointer rounded-full [transition:transform_.15s] leading-none hover:[transform:scale(1.45)_translateY(-4px)]" data-emoji="😂" data-label="Haha" onclick="pickReaction(this)">😂</button>
          <button class="er-emoji relative w-[38px] h-[38px] border-0 bg-transparent text-2xl cursor-pointer rounded-full [transition:transform_.15s] leading-none hover:[transform:scale(1.45)_translateY(-4px)]" data-emoji="😮" data-label="Wow" onclick="pickReaction(this)">😮</button>
          <button class="er-emoji relative w-[38px] h-[38px] border-0 bg-transparent text-2xl cursor-pointer rounded-full [transition:transform_.15s] leading-none hover:[transform:scale(1.45)_translateY(-4px)]" data-emoji="😢" data-label="Sad" onclick="pickReaction(this)">😢</button>
          <button class="er-emoji relative w-[38px] h-[38px] border-0 bg-transparent text-2xl cursor-pointer rounded-full [transition:transform_.15s] leading-none hover:[transform:scale(1.45)_translateY(-4px)]" data-emoji="😡" data-label="Angry" onclick="pickReaction(this)">😡</button>
        </div>
        <button class="er-trigger flex items-center gap-1.5 bg-[#f1f5f9] border-0 rounded-[9px] py-2 px-3.5 text-[13px] font-bold text-[#475569] cursor-pointer font-[inherit] [transition:background_.15s,color_.15s] hover:bg-[#e2e8f0]" id="erTrigger" onclick="togglePop(event)"><span class="er-temoji">🙂</span>React</button>
      </div>
    </div>
  </div>
  <script>
    var REACTIONS = { '👍': 'Like', '❤️': 'Love', '😂': 'Haha', '😮': 'Wow', '😢': 'Sad', '😡': 'Angry' };
    var BASE = ['👍', '❤️', '😂'];
    var BASE_COUNT = 24;
    var myReaction = null;
    var react = document.getElementById('erReact');
    
    function togglePop(e) {
      e.stopPropagation();
      react.classList.toggle('open');
    }
    
    function closePop() { react.classList.remove('open'); }
    
    function pickReaction(el) {
      var emoji = el.dataset.emoji;
      myReaction = (myReaction === emoji) ? null : emoji;
      updateReact();
      closePop();
    }
    
    function updateReact() {
      var trigger = document.getElementById('erTrigger');
      if (myReaction) {
        trigger.innerHTML = '<span class="er-temoji">' + myReaction + '</span>' + REACTIONS[myReaction];
        trigger.classList.add('reacted');
      } else {
        trigger.innerHTML = '<span class="er-temoji">🙂</span>React';
        trigger.classList.remove('reacted');
      }
    
      document.getElementById('erTotal').textContent = BASE_COUNT + (myReaction ? 1 : 0);
    
      var list = (myReaction && BASE.indexOf(myReaction) === -1) ? [myReaction].concat(BASE).slice(0, 3) : BASE.slice();
      document.getElementById('erCluster').innerHTML = list.map(function (e) {
        var mine = e === myReaction ? ' mine' : '';
        return '<span class="er-bubble' + mine + '">' + e + '</span>';
      }).join('');
    }
    
    document.addEventListener('click', function (e) {
      if (!react.contains(e.target)) closePop();
    });
    
    updateReact();
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to EmojiReactionBar.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.er-card{background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:18px;width:100%;max-width:380px;box-shadow:0 14px 44px rgba(15,23,42,.07)}
.er-post{display:flex;align-items:center;gap:11px;margin-bottom:12px}
.er-avatar{width:42px;height:42px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-weight:800;font-size:17px;display:flex;align-items:center;justify-content:center}
.er-author{font-size:14px;font-weight:700;color:#1e293b}
.er-time{font-size:11px;color:#94a3b8}
.er-text{font-size:14px;color:#334155;line-height:1.55;margin-bottom:16px}

.er-footer{display:flex;align-items:center;justify-content:space-between;border-top:1px solid #f1f5f9;padding-top:12px}
.er-summary{display:flex;align-items:center;gap:8px}
.er-cluster{display:flex}
.er-bubble{width:24px;height:24px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.15);display:flex;align-items:center;justify-content:center;font-size:13px;margin-left:-7px;border:1.5px solid #fff}
.er-bubble:first-child{margin-left:0}
.er-bubble.mine{box-shadow:0 0 0 2px #6366f1;animation:er-bub .3s cubic-bezier(.2,1.6,.4,1)}
@keyframes er-bub{from{transform:scale(0)}to{transform:scale(1)}}
.er-total{font-size:12px;color:#64748b;font-weight:600}

.er-react{position:relative}
.er-trigger{display:flex;align-items:center;gap:6px;background:#f1f5f9;border:none;border-radius:9px;padding:8px 14px;font-size:13px;font-weight:700;color:#475569;cursor:pointer;font-family:inherit;transition:background .15s,color .15s}
.er-trigger:hover{background:#e2e8f0}
.er-trigger.reacted{background:#eef2ff;color:#4f46e5}
.er-temoji{font-size:16px;line-height:1}

.er-pop{position:absolute;bottom:calc(100% + 9px);right:0;display:flex;gap:3px;background:#fff;border:1px solid #e2e8f0;border-radius:999px;padding:6px 8px;box-shadow:0 10px 28px rgba(15,23,42,.18);opacity:0;visibility:hidden;transform:translateY(8px) scale(.9);transform-origin:bottom right;transition:opacity .2s,transform .2s,visibility .2s;z-index:5}
.er-react:hover .er-pop,.er-react.open .er-pop{opacity:1;visibility:visible;transform:translateY(0) scale(1)}
.er-emoji{position:relative;width:38px;height:38px;border:none;background:none;font-size:24px;cursor:pointer;border-radius:50%;transition:transform .15s;line-height:1}
.er-emoji:hover{transform:scale(1.45) translateY(-4px)}
.er-emoji::after{content:attr(data-label);position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:#1e293b;color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s}
.er-emoji:hover::after{opacity:1}
`;

export default function EmojiReactionBar() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var REACTIONS = { '👍': 'Like', '❤️': 'Love', '😂': 'Haha', '😮': 'Wow', '😢': 'Sad', '😡': 'Angry' };
    var BASE = ['👍', '❤️', '😂'];
    var BASE_COUNT = 24;
    var myReaction = null;
    var react = document.getElementById('erReact');
    
    function togglePop(e) {
      e.stopPropagation();
      react.classList.toggle('open');
    }
    
    function closePop() { react.classList.remove('open'); }
    
    function pickReaction(el) {
      var emoji = el.dataset.emoji;
      myReaction = (myReaction === emoji) ? null : emoji;
      updateReact();
      closePop();
    }
    
    function updateReact() {
      var trigger = document.getElementById('erTrigger');
      if (myReaction) {
        trigger.innerHTML = '<span class="er-temoji">' + myReaction + '</span>' + REACTIONS[myReaction];
        trigger.classList.add('reacted');
      } else {
        trigger.innerHTML = '<span class="er-temoji">🙂</span>React';
        trigger.classList.remove('reacted');
      }
    
      document.getElementById('erTotal').textContent = BASE_COUNT + (myReaction ? 1 : 0);
    
      var list = (myReaction && BASE.indexOf(myReaction) === -1) ? [myReaction].concat(BASE).slice(0, 3) : BASE.slice();
      document.getElementById('erCluster').innerHTML = list.map(function (e) {
        var mine = e === myReaction ? ' mine' : '';
        return '<span class="er-bubble' + mine + '">' + e + '</span>';
      }).join('');
    }
    
    document.addEventListener('click', function (e) {
      if (!react.contains(e.target)) closePop();
    });
    
    updateReact();
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };

    // Expose handlers used by inline JSX events to the global scope
    if (typeof pickReaction === 'function') window.pickReaction = pickReaction;
    if (typeof togglePop === 'function') window.togglePop = togglePop;
  }, []);

  return (
    <>
      <style>{css}</style>
      <div className="er-card">
        <div className="er-post">
          <div className="er-avatar">N</div>
          <div>
            <div className="er-author">Nadia Okonkwo</div>
            <div className="er-time">2 hours ago</div>
          </div>
        </div>
        <p className="er-text">Just shipped the new dashboard 🚀 Months of work — would love your honest reactions!</p>
      
        <div className="er-footer">
          <div className="er-summary">
            <div className="er-cluster" id="erCluster"></div>
            <span className="er-total"><span id="erTotal">24</span> reactions</span>
          </div>
      
          <div className="er-react" id="erReact">
            <div className="er-pop" id="erPop">
              <button className="er-emoji" data-emoji="👍" data-label="Like" onClick={(event) => { pickReaction(event.currentTarget) }}>👍</button>
              <button className="er-emoji" data-emoji="❤️" data-label="Love" onClick={(event) => { pickReaction(event.currentTarget) }}>❤️</button>
              <button className="er-emoji" data-emoji="😂" data-label="Haha" onClick={(event) => { pickReaction(event.currentTarget) }}>😂</button>
              <button className="er-emoji" data-emoji="😮" data-label="Wow" onClick={(event) => { pickReaction(event.currentTarget) }}>😮</button>
              <button className="er-emoji" data-emoji="😢" data-label="Sad" onClick={(event) => { pickReaction(event.currentTarget) }}>😢</button>
              <button className="er-emoji" data-emoji="😡" data-label="Angry" onClick={(event) => { pickReaction(event.currentTarget) }}>😡</button>
            </div>
            <button className="er-trigger" id="erTrigger" onClick={(event) => { togglePop(event) }}><span className="er-temoji">🙂</span>React</button>
          </div>
        </div>
      </div>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function EmojiReactionBar() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var REACTIONS = { '👍': 'Like', '❤️': 'Love', '😂': 'Haha', '😮': 'Wow', '😢': 'Sad', '😡': 'Angry' };
    var BASE = ['👍', '❤️', '😂'];
    var BASE_COUNT = 24;
    var myReaction = null;
    var react = document.getElementById('erReact');
    
    function togglePop(e) {
      e.stopPropagation();
      react.classList.toggle('open');
    }
    
    function closePop() { react.classList.remove('open'); }
    
    function pickReaction(el) {
      var emoji = el.dataset.emoji;
      myReaction = (myReaction === emoji) ? null : emoji;
      updateReact();
      closePop();
    }
    
    function updateReact() {
      var trigger = document.getElementById('erTrigger');
      if (myReaction) {
        trigger.innerHTML = '<span class="er-temoji">' + myReaction + '</span>' + REACTIONS[myReaction];
        trigger.classList.add('reacted');
      } else {
        trigger.innerHTML = '<span class="er-temoji">🙂</span>React';
        trigger.classList.remove('reacted');
      }
    
      document.getElementById('erTotal').textContent = BASE_COUNT + (myReaction ? 1 : 0);
    
      var list = (myReaction && BASE.indexOf(myReaction) === -1) ? [myReaction].concat(BASE).slice(0, 3) : BASE.slice();
      document.getElementById('erCluster').innerHTML = list.map(function (e) {
        var mine = e === myReaction ? ' mine' : '';
        return '<span class="er-bubble' + mine + '">' + e + '</span>';
      }).join('');
    }
    
    document.addEventListener('click', function (e) {
      if (!react.contains(e.target)) closePop();
    });
    
    updateReact();
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };

    // Expose handlers used by inline JSX events to the global scope
    if (typeof pickReaction === 'function') window.pickReaction = pickReaction;
    if (typeof togglePop === 'function') window.togglePop = togglePop;
  }, []);

  return (
    <>
      <style>{`
@keyframes er-bub{from{transform:scale(0)}to{transform:scale(1)}}

* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}

.er-bubble {
  width:24px;height:24px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.15);display:flex;align-items:center;justify-content:center;font-size:13px;margin-left:-7px;border:1.5px solid #fff
}

.er-bubble:first-child {
  margin-left:0
}

.er-bubble.mine {
  box-shadow:0 0 0 2px #6366f1;animation:er-bub .3s cubic-bezier(.2,1.6,.4,1)
}

.er-trigger.reacted {
  background:#eef2ff;color:#4f46e5
}

.er-temoji {
  font-size:16px;line-height:1
}

.er-react:hover .er-pop,.er-react.open .er-pop {
  opacity:1;visibility:visible;transform:translateY(0) scale(1)
}

.er-emoji::after {
  content:attr(data-label);position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:#1e293b;color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s
}

.er-emoji:hover::after {
  opacity:1
}
      `}</style>
      <div className="bg-[#fff] border border-[#e2e8f0] rounded-2xl p-[18px] w-full max-w-[380px] shadow-[0_14px_44px_rgba(15,23,42,.07)]">
        <div className="flex items-center gap-[11px] mb-3">
          <div className="w-[42px] h-[42px] rounded-full [background:linear-gradient(135deg,#6366f1,#8b5cf6)] text-[#fff] font-extrabold text-[17px] flex items-center justify-center">N</div>
          <div>
            <div className="text-sm font-bold text-[#1e293b]">Nadia Okonkwo</div>
            <div className="text-[11px] text-[#94a3b8]">2 hours ago</div>
          </div>
        </div>
        <p className="text-sm text-[#334155] leading-[1.55] mb-4">Just shipped the new dashboard 🚀 Months of work — would love your honest reactions!</p>
      
        <div className="flex items-center justify-between border-t border-t-[#f1f5f9] pt-3">
          <div className="flex items-center gap-2">
            <div className="flex" id="erCluster"></div>
            <span className="text-xs text-[#64748b] font-semibold"><span id="erTotal">24</span> reactions</span>
          </div>
      
          <div className="er-react relative" id="erReact">
            <div className="er-pop absolute bottom-[calc(100% + 9px)] right-0 flex gap-[3px] bg-[#fff] border border-[#e2e8f0] rounded-[999px] py-1.5 px-2 shadow-[0_10px_28px_rgba(15,23,42,.18)] opacity-0 invisible [transform:translateY(8px)_scale(.9)] [transform-origin:bottom_right] [transition:opacity_.2s,transform_.2s,visibility_.2s] z-[5]" id="erPop">
              <button className="er-emoji relative w-[38px] h-[38px] border-0 bg-transparent text-2xl cursor-pointer rounded-full [transition:transform_.15s] leading-none hover:[transform:scale(1.45)_translateY(-4px)]" data-emoji="👍" data-label="Like" onClick={(event) => { pickReaction(event.currentTarget) }}>👍</button>
              <button className="er-emoji relative w-[38px] h-[38px] border-0 bg-transparent text-2xl cursor-pointer rounded-full [transition:transform_.15s] leading-none hover:[transform:scale(1.45)_translateY(-4px)]" data-emoji="❤️" data-label="Love" onClick={(event) => { pickReaction(event.currentTarget) }}>❤️</button>
              <button className="er-emoji relative w-[38px] h-[38px] border-0 bg-transparent text-2xl cursor-pointer rounded-full [transition:transform_.15s] leading-none hover:[transform:scale(1.45)_translateY(-4px)]" data-emoji="😂" data-label="Haha" onClick={(event) => { pickReaction(event.currentTarget) }}>😂</button>
              <button className="er-emoji relative w-[38px] h-[38px] border-0 bg-transparent text-2xl cursor-pointer rounded-full [transition:transform_.15s] leading-none hover:[transform:scale(1.45)_translateY(-4px)]" data-emoji="😮" data-label="Wow" onClick={(event) => { pickReaction(event.currentTarget) }}>😮</button>
              <button className="er-emoji relative w-[38px] h-[38px] border-0 bg-transparent text-2xl cursor-pointer rounded-full [transition:transform_.15s] leading-none hover:[transform:scale(1.45)_translateY(-4px)]" data-emoji="😢" data-label="Sad" onClick={(event) => { pickReaction(event.currentTarget) }}>😢</button>
              <button className="er-emoji relative w-[38px] h-[38px] border-0 bg-transparent text-2xl cursor-pointer rounded-full [transition:transform_.15s] leading-none hover:[transform:scale(1.45)_translateY(-4px)]" data-emoji="😡" data-label="Angry" onClick={(event) => { pickReaction(event.currentTarget) }}>😡</button>
            </div>
            <button className="er-trigger flex items-center gap-1.5 bg-[#f1f5f9] border-0 rounded-[9px] py-2 px-3.5 text-[13px] font-bold text-[#475569] cursor-pointer font-[inherit] [transition:background_.15s,color_.15s] hover:bg-[#e2e8f0]" id="erTrigger" onClick={(event) => { togglePop(event) }}><span className="er-temoji">🙂</span>React</button>
          </div>
        </div>
      </div>
    </>
  );
}
Vue
<template>
  <div class="er-card">
    <div class="er-post">
      <div class="er-avatar">N</div>
      <div>
        <div class="er-author">Nadia Okonkwo</div>
        <div class="er-time">2 hours ago</div>
      </div>
    </div>
    <p class="er-text">Just shipped the new dashboard 🚀 Months of work — would love your honest reactions!</p>
  
    <div class="er-footer">
      <div class="er-summary">
        <div class="er-cluster" id="erCluster"></div>
        <span class="er-total"><span id="erTotal">24</span> reactions</span>
      </div>
  
      <div class="er-react" id="erReact">
        <div class="er-pop" id="erPop">
          <button class="er-emoji" data-emoji="👍" data-label="Like" @click="pickReaction($event.currentTarget)">👍</button>
          <button class="er-emoji" data-emoji="❤️" data-label="Love" @click="pickReaction($event.currentTarget)">❤️</button>
          <button class="er-emoji" data-emoji="😂" data-label="Haha" @click="pickReaction($event.currentTarget)">😂</button>
          <button class="er-emoji" data-emoji="😮" data-label="Wow" @click="pickReaction($event.currentTarget)">😮</button>
          <button class="er-emoji" data-emoji="😢" data-label="Sad" @click="pickReaction($event.currentTarget)">😢</button>
          <button class="er-emoji" data-emoji="😡" data-label="Angry" @click="pickReaction($event.currentTarget)">😡</button>
        </div>
        <button class="er-trigger" id="erTrigger" @click="togglePop($event)"><span class="er-temoji">🙂</span>React</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

var REACTIONS = { '👍': 'Like', '❤️': 'Love', '😂': 'Haha', '😮': 'Wow', '😢': 'Sad', '😡': 'Angry' };
var BASE = ['👍', '❤️', '😂'];
var BASE_COUNT = 24;
var myReaction = null;
let react;
function togglePop(e) {
  e.stopPropagation();
  react.classList.toggle('open');
}
function closePop() { react.classList.remove('open'); }
function pickReaction(el) {
  var emoji = el.dataset.emoji;
  myReaction = (myReaction === emoji) ? null : emoji;
  updateReact();
  closePop();
}
function updateReact() {
  var trigger = document.getElementById('erTrigger');
  if (myReaction) {
    trigger.innerHTML = '<span class="er-temoji">' + myReaction + '</span>' + REACTIONS[myReaction];
    trigger.classList.add('reacted');
  } else {
    trigger.innerHTML = '<span class="er-temoji">🙂</span>React';
    trigger.classList.remove('reacted');
  }

  document.getElementById('erTotal').textContent = BASE_COUNT + (myReaction ? 1 : 0);

  var list = (myReaction && BASE.indexOf(myReaction) === -1) ? [myReaction].concat(BASE).slice(0, 3) : BASE.slice();
  document.getElementById('erCluster').innerHTML = list.map(function (e) {
    var mine = e === myReaction ? ' mine' : '';
    return '<span class="er-bubble' + mine + '">' + e + '</span>';
  }).join('');
}

onMounted(() => {
  react = document.getElementById('erReact');
  document.addEventListener('click', function (e) {
    if (!react.contains(e.target)) closePop();
  });
  updateReact();
});
</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.er-card{background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:18px;width:100%;max-width:380px;box-shadow:0 14px 44px rgba(15,23,42,.07)}
.er-post{display:flex;align-items:center;gap:11px;margin-bottom:12px}
.er-avatar{width:42px;height:42px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-weight:800;font-size:17px;display:flex;align-items:center;justify-content:center}
.er-author{font-size:14px;font-weight:700;color:#1e293b}
.er-time{font-size:11px;color:#94a3b8}
.er-text{font-size:14px;color:#334155;line-height:1.55;margin-bottom:16px}

.er-footer{display:flex;align-items:center;justify-content:space-between;border-top:1px solid #f1f5f9;padding-top:12px}
.er-summary{display:flex;align-items:center;gap:8px}
.er-cluster{display:flex}
.er-bubble{width:24px;height:24px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.15);display:flex;align-items:center;justify-content:center;font-size:13px;margin-left:-7px;border:1.5px solid #fff}
.er-bubble:first-child{margin-left:0}
.er-bubble.mine{box-shadow:0 0 0 2px #6366f1;animation:er-bub .3s cubic-bezier(.2,1.6,.4,1)}
@keyframes er-bub{from{transform:scale(0)}to{transform:scale(1)}}
.er-total{font-size:12px;color:#64748b;font-weight:600}

.er-react{position:relative}
.er-trigger{display:flex;align-items:center;gap:6px;background:#f1f5f9;border:none;border-radius:9px;padding:8px 14px;font-size:13px;font-weight:700;color:#475569;cursor:pointer;font-family:inherit;transition:background .15s,color .15s}
.er-trigger:hover{background:#e2e8f0}
.er-trigger.reacted{background:#eef2ff;color:#4f46e5}
.er-temoji{font-size:16px;line-height:1}

.er-pop{position:absolute;bottom:calc(100% + 9px);right:0;display:flex;gap:3px;background:#fff;border:1px solid #e2e8f0;border-radius:999px;padding:6px 8px;box-shadow:0 10px 28px rgba(15,23,42,.18);opacity:0;visibility:hidden;transform:translateY(8px) scale(.9);transform-origin:bottom right;transition:opacity .2s,transform .2s,visibility .2s;z-index:5}
.er-react:hover .er-pop,.er-react.open .er-pop{opacity:1;visibility:visible;transform:translateY(0) scale(1)}
.er-emoji{position:relative;width:38px;height:38px;border:none;background:none;font-size:24px;cursor:pointer;border-radius:50%;transition:transform .15s;line-height:1}
.er-emoji:hover{transform:scale(1.45) translateY(-4px)}
.er-emoji::after{content:attr(data-label);position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:#1e293b;color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s}
.er-emoji:hover::after{opacity:1}
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-emoji-reaction-bar',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="er-card">
      <div class="er-post">
        <div class="er-avatar">N</div>
        <div>
          <div class="er-author">Nadia Okonkwo</div>
          <div class="er-time">2 hours ago</div>
        </div>
      </div>
      <p class="er-text">Just shipped the new dashboard 🚀 Months of work — would love your honest reactions!</p>
    
      <div class="er-footer">
        <div class="er-summary">
          <div class="er-cluster" id="erCluster"></div>
          <span class="er-total"><span id="erTotal">24</span> reactions</span>
        </div>
    
        <div class="er-react" id="erReact">
          <div class="er-pop" id="erPop">
            <button class="er-emoji" data-emoji="👍" data-label="Like" (click)="pickReaction($event.currentTarget)">👍</button>
            <button class="er-emoji" data-emoji="❤️" data-label="Love" (click)="pickReaction($event.currentTarget)">❤️</button>
            <button class="er-emoji" data-emoji="😂" data-label="Haha" (click)="pickReaction($event.currentTarget)">😂</button>
            <button class="er-emoji" data-emoji="😮" data-label="Wow" (click)="pickReaction($event.currentTarget)">😮</button>
            <button class="er-emoji" data-emoji="😢" data-label="Sad" (click)="pickReaction($event.currentTarget)">😢</button>
            <button class="er-emoji" data-emoji="😡" data-label="Angry" (click)="pickReaction($event.currentTarget)">😡</button>
          </div>
          <button class="er-trigger" id="erTrigger" (click)="togglePop($event)"><span class="er-temoji">🙂</span>React</button>
        </div>
      </div>
    </div>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
    .er-card{background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:18px;width:100%;max-width:380px;box-shadow:0 14px 44px rgba(15,23,42,.07)}
    .er-post{display:flex;align-items:center;gap:11px;margin-bottom:12px}
    .er-avatar{width:42px;height:42px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-weight:800;font-size:17px;display:flex;align-items:center;justify-content:center}
    .er-author{font-size:14px;font-weight:700;color:#1e293b}
    .er-time{font-size:11px;color:#94a3b8}
    .er-text{font-size:14px;color:#334155;line-height:1.55;margin-bottom:16px}
    
    .er-footer{display:flex;align-items:center;justify-content:space-between;border-top:1px solid #f1f5f9;padding-top:12px}
    .er-summary{display:flex;align-items:center;gap:8px}
    .er-cluster{display:flex}
    .er-bubble{width:24px;height:24px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.15);display:flex;align-items:center;justify-content:center;font-size:13px;margin-left:-7px;border:1.5px solid #fff}
    .er-bubble:first-child{margin-left:0}
    .er-bubble.mine{box-shadow:0 0 0 2px #6366f1;animation:er-bub .3s cubic-bezier(.2,1.6,.4,1)}
    @keyframes er-bub{from{transform:scale(0)}to{transform:scale(1)}}
    .er-total{font-size:12px;color:#64748b;font-weight:600}
    
    .er-react{position:relative}
    .er-trigger{display:flex;align-items:center;gap:6px;background:#f1f5f9;border:none;border-radius:9px;padding:8px 14px;font-size:13px;font-weight:700;color:#475569;cursor:pointer;font-family:inherit;transition:background .15s,color .15s}
    .er-trigger:hover{background:#e2e8f0}
    .er-trigger.reacted{background:#eef2ff;color:#4f46e5}
    .er-temoji{font-size:16px;line-height:1}
    
    .er-pop{position:absolute;bottom:calc(100% + 9px);right:0;display:flex;gap:3px;background:#fff;border:1px solid #e2e8f0;border-radius:999px;padding:6px 8px;box-shadow:0 10px 28px rgba(15,23,42,.18);opacity:0;visibility:hidden;transform:translateY(8px) scale(.9);transform-origin:bottom right;transition:opacity .2s,transform .2s,visibility .2s;z-index:5}
    .er-react:hover .er-pop,.er-react.open .er-pop{opacity:1;visibility:visible;transform:translateY(0) scale(1)}
    .er-emoji{position:relative;width:38px;height:38px;border:none;background:none;font-size:24px;cursor:pointer;border-radius:50%;transition:transform .15s;line-height:1}
    .er-emoji:hover{transform:scale(1.45) translateY(-4px)}
    .er-emoji::after{content:attr(data-label);position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:#1e293b;color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s}
    .er-emoji:hover::after{opacity:1}
  `]
})
export class EmojiReactionBarComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var REACTIONS = { '👍': 'Like', '❤️': 'Love', '😂': 'Haha', '😮': 'Wow', '😢': 'Sad', '😡': 'Angry' };
    var BASE = ['👍', '❤️', '😂'];
    var BASE_COUNT = 24;
    var myReaction = null;
    var react = document.getElementById('erReact');
    
    function togglePop(e) {
      e.stopPropagation();
      react.classList.toggle('open');
    }
    
    function closePop() { react.classList.remove('open'); }
    
    function pickReaction(el) {
      var emoji = el.dataset.emoji;
      myReaction = (myReaction === emoji) ? null : emoji;
      updateReact();
      closePop();
    }
    
    function updateReact() {
      var trigger = document.getElementById('erTrigger');
      if (myReaction) {
        trigger.innerHTML = '<span class="er-temoji">' + myReaction + '</span>' + REACTIONS[myReaction];
        trigger.classList.add('reacted');
      } else {
        trigger.innerHTML = '<span class="er-temoji">🙂</span>React';
        trigger.classList.remove('reacted');
      }
    
      document.getElementById('erTotal').textContent = BASE_COUNT + (myReaction ? 1 : 0);
    
      var list = (myReaction && BASE.indexOf(myReaction) === -1) ? [myReaction].concat(BASE).slice(0, 3) : BASE.slice();
      document.getElementById('erCluster').innerHTML = list.map(function (e) {
        var mine = e === myReaction ? ' mine' : '';
        return '<span class="er-bubble' + mine + '">' + e + '</span>';
      }).join('');
    }
    
    document.addEventListener('click', function (e) {
      if (!react.contains(e.target)) closePop();
    });
    
    updateReact();

    // Bind inline-handler functions to the component so the template can call them
    Object.assign(this, { togglePop, closePop, pickReaction, updateReact });
  }
}