More Buttons Snippets
Emoji Reaction Bar — HTML CSS JS Snippet
Emoji Reaction Bar · Buttons · Plain HTML, CSS & JS · Live preview
What's included
Features
.open class toggled by tap for touch, so it works on every device.scale(.9) with a bottom-right origin, appearing to grow out of the React button.content: attr(data-label) on a pseudo-element labels each emoji on hover — no tooltip library or extra markup.pickReaction enforces one reaction at a time and removes it when re-clicked, matching real social platforms.updateReact rewrites the trigger to your emoji and label and adjusts the total by one in a single pass.stopPropagation keeping the trigger from self-closing.About this UI Snippet
Emoji Reaction Bar — Hover-Reveal Popover, Tooltip Reactions & Overlapping Count Cluster

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
- 1Paste HTML, CSS, and JSA social post card appears with a reaction summary (24 reactions) and a "🙂 React" button in the footer.
- 2Reveal the reactionsHover the React button (or tap it on touch) — a rounded popover of six emoji springs up above it.
- 3Hover an emojiEach emoji scales up and lifts, and a tooltip shows its name ("Love", "Haha", "Wow") above it.
- 4Pick 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.
- 5Change or remove itPick a different emoji to switch, or click your current reaction again to remove it — the count and cluster update accordingly.
- 6Dismiss the popoverClick anywhere outside to close the picker without changing your reaction.
Real-world uses
Common Use Cases
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
<!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><!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>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>
</>
);
}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>
</>
);
}<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>// @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 });
}
}