CollaborationReactions
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises.
Book a callReactions
Quick reactions on an agent update, with counts that pop. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.
Install
Add it to your project with the CLI.
HTML, CSS and JavaScript
npx designme add reactionsReact
npx designme add reactions --reactThe CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.
About
What it is for and what it does.
Reactions puts a row of emoji reactions under a message, set up for an update the agent posted. People click a reaction to add or remove their own, or pick a new one from a small picker, and the counts roll up or down as they change. It gives the team a lightweight way to acknowledge or approve what the agent reported.
- When an agent posts a status update in a chat thread and people want to acknowledge it
- When a quick signal, such as a check or eyes, is enough and a reply would be noise
- When the team should see at a glance who has reacted to an update
- When you want an agent update to sit flat in a thread or stand alone as a card
- Clicking a reaction toggles yours: its edge turns a stronger grey and its count takes the accent's text colour, the count rolls up, and it gets a small jump.
- Removing the last reaction of a kind fades it out and shrinks it before it leaves the row.
- The add button opens a picker of
emojis; picking one you already have pulses it instead of counting twice. - Each reaction's tooltip names who reacted, like "You, Maya, Jon and 2 more", using
peopleandmeLabel. - Reactions that arrive after the first render pop in.
bare(the default) keeps it flat for a chat thread; set it to false to give it its own card.
- Each reaction is a button with
aria-pressedshowing whether you reacted with it. - The add button has an
aria-labeland opens amenuofmenuitememoji, witharia-expandedon the trigger. - Escape closes the picker and returns focus to the add button.
- From the keyboard, Enter, Space or the down arrow opens the picker on its first emoji; the arrow keys move through the grid (up and down a row at a time) and wrap, Home and End jump to the ends, and Tab closes it.
- The tooltips naming who reacted use
role="tooltip"and also open on keyboard focus.
Usage
Import it and drive it from your own state.
Import
import { Reactions } from '@/components/designme/reactions/Reactions';Example
The demo above as a React component, 29 lines you can copy.
'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useRef, useState } from 'react';
import { Reactions, type ReactionsItem } from './Reactions';
import { useStory } from '../lib/useScript';
/* ---------- Reactions: three reactions on an agent update, and you add a rocket after a moment ---------- */
const START: ReactionsItem[] = [
{ emoji: '๐', count: 4, people: ['Maya', 'Jon', 'Ari', 'Sam'] },
{ emoji: '๐', count: 2, people: ['Jon', 'Dana'] },
{ emoji: 'โ
', count: 1, people: ['Maya'] }
];
export default function ReactionsDemo() {
const [reactions, setReactions] = useState(START);
// the preview adds one reaction on its own, unless you take over first
const ref = useRef<HTMLDivElement>(null);
useStory(ref, async ({ wait }) => {
await wait(1100);
setReactions(list => list.map((r, i) => i === 0 ? { ...r, mine: !r.mine, count: r.count + (r.mine ? -1 : 1) } : r).filter(r => r.count > 0));
});
return (
<div ref={ref} style={{ display: 'contents' }}>
<Reactions author="Design agent" time="10:42" reactions={reactions} onReactionsChange={setReactions}
message="Shipped the pricing update to staging. Trial to paid preview is up 8.1 points against last month." />
</div>
);
}
Props
Everything Reactions takes.
| Prop | Type | What it does |
|---|---|---|
authorrequired | ReactNode | Who wrote the message, like "Design agent" |
time | ReactNode | When, like "10:42" |
messagerequired | ReactNode | The message being reacted to |
icon | IconName | The icon on the author's square |
reactions | ReactionsItem[] | The reactions under the message. Leave it out to let clicks and the picker change them |
defaultReactions | ReactionsItem[] | |
onReactionsChange | (reactions: ReactionsItem[]) => void | |
emojis | string[] | The emoji in the picker |
meLabel | string | How you are named in the tooltips |
addLabel | string | The add button's tooltip |
bare | boolean | Sits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own |
className | string |
Code
The full source, as the CLI installs it.
Reactions.tsxReact ยท 167 lines
'use client';
import { useEffect, useRef, useState, type AnimationEvent, type ReactNode } from 'react';
import { cx } from '../lib/cx';
import { useControllable } from '../lib/hooks';
import { Icon, type IconName } from '../lib/icons';
import { Tip } from '../lib/motion';
import { usePopover } from '../lib/popover';
import '../designme.css';
import './reactions.css';
export interface ReactionsItem {
emoji: string;
/** How many people reacted with it, you included */
count: number;
/** Who else reacted, for the tooltip, like ['Maya', 'Jon'] */
people?: string[];
/** Whether you reacted with it. Your reactions are tinted and their count is accent coloured */
mine?: boolean;
}
export interface ReactionsProps {
/** Who wrote the message, like "Design agent" */
author: ReactNode;
/** When, like "10:42" */
time?: ReactNode;
/** The message being reacted to */
message: ReactNode;
/** The icon on the author's square */
icon?: IconName;
/** The reactions under the message. Leave it out to let clicks and the picker change them */
reactions?: ReactionsItem[];
defaultReactions?: ReactionsItem[];
onReactionsChange?: (reactions: ReactionsItem[]) => void;
/** The emoji in the picker */
emojis?: string[];
/** How you are named in the tooltips */
meLabel?: string;
/** The add button's tooltip */
addLabel?: string;
/** Sits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own */
bare?: boolean;
className?: string;
}
const EMOJIS = ['๐', 'โค๏ธ', '๐', '๐', '๐', 'โ
', '๐ฅ', '๐ฏ', '๐', '๐ค', 'โจ', '๐'];
/** "You, Maya, Jon and 2 more" */
const whoOf = (r: ReactionsItem, me: string) => {
const who = r.mine ? [me, ...(r.people ?? [])] : r.people ?? [];
return who.slice(0, 3).join(', ') + (who.length > 3 ? ` and ${who.length - 3} more` : '');
};
/** A reaction on screen: the latest data, or the last data of one that is fading out */
interface Shown { item: ReactionsItem; leaving: boolean }
/** Keeps a removed reaction in its place while it fades and shrinks, then lets it go */
function useLeaving(items: ReactionsItem[], ms: number) {
const [shown, setShown] = useState<Shown[]>(() => items.map(item => ({ item, leaving: false })));
const [prev, setPrev] = useState(items);
if (prev !== items) {
setPrev(items);
const now = new Set(items.map(r => r.emoji));
const next: Shown[] = items.map(item => ({ item, leaving: false }));
shown.forEach((s, i) => { if (!now.has(s.item.emoji)) next.splice(Math.min(i, next.length), 0, { item: s.item, leaving: true }); });
setShown(next);
}
const leaving = shown.filter(s => s.leaving).map(s => s.item.emoji).join('\u0000');
useEffect(() => {
if (!leaving) return;
const t = setTimeout(() => setShown(list => list.filter(s => !s.leaving)), ms);
return () => clearTimeout(t);
}, [leaving, ms]);
return shown;
}
function Reaction({ item, leaving, fresh, pulse, me, onToggle }: { item: ReactionsItem; leaving: boolean; fresh: boolean; pulse: number; me: string; onToggle: () => void }) {
const ref = useRef<HTMLButtonElement>(null);
const [isNew, setNew] = useState(fresh);
// the count rolls up or down instead of the whole row re-rendering under the pointer
const [nums, setNums] = useState([{ k: 0, v: item.count, c: '' }]);
const last = useRef(item.count);
useEffect(() => {
if (item.count === last.current || leaving) return;
const up = item.count > last.current; last.current = item.count;
setNums(ns => { const top = ns[ns.length - 1]; return [{ ...top, c: up ? 'dm-ou' : 'dm-od' }, { k: top.k + 1, v: item.count, c: up ? 'dm-iu' : 'dm-id' }]; });
const t = setTimeout(() => setNums(ns => ns.slice(-1)), 380);
return () => clearTimeout(t);
}, [item.count, leaving]);
// a ring and a little jump when you react, or pick one you already have
const [bumped, setBumped] = useState(false);
const bump = () => {
const el = ref.current;
if (!el || !el.classList.contains('dm-bump')) { setBumped(true); return; }
el.classList.remove('dm-bump'); void el.offsetWidth; el.classList.add('dm-bump');
};
const wasMine = useRef(!!item.mine);
useEffect(() => {
const mine = !!item.mine;
if (mine && !wasMine.current && !leaving) bump();
wasMine.current = mine;
}, [item.mine, leaving]);
useEffect(() => { if (pulse) bump(); }, [pulse]);
useEffect(() => { if (fresh && item.mine) bump(); }, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<Tip label={whoOf(item, me)} off={leaving}>
<button type="button" ref={ref} className={cx('dm-rxx-r', item.mine && 'dm-me', isNew && 'dm-new', bumped && 'dm-bump', leaving && 'dm-gone')}
aria-pressed={!!item.mine} onClick={leaving ? undefined : onToggle}
onAnimationEnd={(e: AnimationEvent) => { if (e.target === e.currentTarget && e.animationName.endsWith('popin')) setNew(false); }}>
<span className="dm-e">{item.emoji}</span>
<span className="dm-n dm-num">{nums.map(n => <i key={n.k} className={n.c || undefined}>{n.v}</i>)}</span>
</button>
</Tip>
);
}
/** Quick reactions on an agent update, with counts that pop. */
export function Reactions({ author, time, message, icon = 'spark', reactions, defaultReactions = [], onReactionsChange, emojis = EMOJIS, meLabel = 'You', addLabel = 'Add reaction', bare = true, className }: ReactionsProps) {
const [list, setList] = useControllable(reactions, defaultReactions, onReactionsChange);
const shown = useLeaving(list, 320);
const menu = usePopover();
// reactions that arrive after the first render pop in
const [initial] = useState(() => new Set(list.map(r => r.emoji)));
const [pulses, setPulses] = useState<Record<string, number>>({});
const toggle = (emoji: string) => setList(list.flatMap(r => {
if (r.emoji !== emoji) return [r];
const mine = !r.mine, count = r.count + (mine ? 1 : -1);
return count > 0 ? [{ ...r, mine, count }] : [];
}));
const pick = (emoji: string) => {
const had = list.find(r => r.emoji === emoji);
if (!had) setList([...list, { emoji, count: 1, people: [], mine: true }]);
else if (!had.mine) toggle(emoji);
else setPulses(p => ({ ...p, [emoji]: (p[emoji] ?? 0) + 1 }));
menu.closeSoon();
};
return (
<div data-dm="" className={cx('dm-rxx', !bare && 'dm-panel', className)}>
<div className="dm-rxx-m">
<span className="dm-rxx-ic"><Icon name={icon} size="xs" /></span>
<div className="dm-rxx-b">
<span className="dm-rxx-n"><b>{author}</b>{time && <span className="dm-muted">{time}</span>}</span>
<p>{message}</p>
</div>
</div>
<div className="dm-rxx-row">
<div className="dm-rxx-list">
{shown.map(({ item, leaving }) => (
<Reaction key={item.emoji} item={item} leaving={leaving} fresh={!initial.has(item.emoji)} pulse={pulses[item.emoji] ?? 0} me={meLabel} onToggle={() => toggle(item.emoji)} />
))}
</div>
<Tip label={addLabel} off={menu.open}>
<button type="button" className="dm-rxx-add" aria-label={addLabel} {...menu.trigger}><Icon name="smile" size="xs" /></button>
</Tip>
</div>
<div {...menu.pop} className={cx(menu.pop.className, 'dm-rxx-pk')} role="menu" aria-label={addLabel}>
<div className="dm-rxx-grid">
{emojis.map(e => <button type="button" key={e} role="menuitem" onClick={() => pick(e)}>{e}</button>)}
</div>
</div>
</div>
);
}
reactions.cssReact ยท 42 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Reactions ---------- */
/* an agent message in the feed: no container; with .panel it becomes a card */
.dm-rxx{padding:14px 0;display:flex;flex-direction:column;gap:10px}
.dm-rxx.dm-panel{padding:14px}
.dm-rxx-m{display:flex;gap:10px}
.dm-rxx-ic{width:30px;height:30px;border-radius:10px;background:var(--dm-accent);color:var(--dm-on-accent);display:grid;place-items:center;flex:none}
.dm-rxx-b{display:flex;flex-direction:column;font-size:13.5px;line-height:1.5;min-width:0}
.dm-rxx-n{display:flex;gap:6px;align-items:baseline;font-size:13px}
.dm-rxx-n b{font-weight:500}
.dm-rxx-n .dm-muted{font-size:11.5px}
.dm-rxx-b p{margin:2px 0 0;color:var(--dm-fg2)}
.dm-rxx-row{display:flex;flex-wrap:wrap;gap:5px;padding-left:40px;align-items:center}
.dm-rxx-list{display:contents}
.dm-rxx-r{position:relative;display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 9px;border-radius:var(--dm-r-pill);background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-line) 50%,transparent);font-size:13px;transition:background .2s,box-shadow .2s,transform .15s,opacity .3s}
/* white pills with a hairline, so they stand out on a grey page; yours has a stronger grey edge and its count in the accent */
@media (hover:hover) and (pointer:fine){.dm-rxx-r:hover{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-rail) 50%,transparent)}}
.dm-rxx-r:active{transform:scale(.94)}
.dm-rxx-r.dm-me{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-pick-on) 50%,transparent)}
.dm-rxx-r .dm-n{display:inline-grid;overflow:hidden;height:16px;font-size:12px;line-height:16px;color:var(--dm-fg2);min-width:8px}
.dm-rxx-r .dm-n i{grid-area:1/1;font-style:normal;text-align:center}
.dm-rxx-r.dm-me .dm-n{color:var(--dm-accent-fg,var(--dm-accent));font-weight:500}
.dm-rxx-r .dm-n .dm-iu{animation:dm-rxiu .38s var(--dm-ease) both}.dm-rxx-r .dm-n .dm-ou{animation:dm-rxou .38s var(--dm-ease) both}
.dm-rxx-r .dm-n .dm-id{animation:dm-rxid .38s var(--dm-ease) both}.dm-rxx-r .dm-n .dm-od{animation:dm-rxod .38s var(--dm-ease) both}
@keyframes dm-rxiu{from{transform:translateY(100%);opacity:0}}@keyframes dm-rxou{to{transform:translateY(-100%);opacity:0}}
@keyframes dm-rxid{from{transform:translateY(-100%);opacity:0}}@keyframes dm-rxod{to{transform:translateY(100%);opacity:0}}
.dm-rxx-r::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0}
.dm-rxx-r.dm-bump::after{animation:dm-rxring .6s var(--dm-ease)}
@keyframes dm-rxring{0%{opacity:.5;box-shadow:0 0 0 0 var(--dm-accent-mark,var(--dm-accent))}100%{opacity:0;box-shadow:0 0 0 7px var(--dm-accent-mark,var(--dm-accent))}}
.dm-rxx-r.dm-new{animation:dm-popin .45s var(--dm-ease) backwards}
.dm-rxx-r.dm-gone{opacity:0;transform:scale(.8)}
.dm-rxx-r .dm-e{display:inline-block}
.dm-rxx-r.dm-bump .dm-e{animation:dm-rxp .5s var(--dm-ease)}
@keyframes dm-rxp{0%{transform:scale(1)}35%{transform:scale(1.5) rotate(-12deg)}60%{transform:scale(.92) rotate(4deg)}100%{transform:scale(1)}}
.dm-rxx-add{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;color:var(--dm-muted);transition:background .2s,color .2s}
.dm-rxx-add[aria-expanded="true"]{background:var(--dm-row);color:var(--dm-fg)}
@media (hover:hover) and (pointer:fine){.dm-rxx-add:hover{background:var(--dm-row);color:var(--dm-fg)}}
.dm-rxx-pk{min-width:0;padding:6px}
.dm-rxx-grid{display:grid;grid-template-columns:repeat(6,32px);gap:2px}
.dm-rxx-grid button{width:32px;height:32px;border-radius:var(--dm-r-sm);font-size:17px;transition:background .15s,transform .2s var(--dm-ease)}
@media (hover:hover) and (pointer:fine){.dm-rxx-grid button:hover{background:var(--dm-row);transform:scale(1.15)}}
reactions.htmlHTML ยท 10 lines
<div class="card bare" data-c="reactions">
<div class="stage-in">
<div class="rxx">
<div class="rxx-m"><span class="rxx-ic"><svg class="i xs"><use href="#i-spark"/></svg></span><div class="rxx-b"><span class="rxx-n"><b>Design agent</b><span class="muted">10:42</span></span><p>Shipped the pricing update to staging. Trial to paid preview is up 8.1 points against last month.</p></div></div>
<div class="rxx-row"><div class="rxx-list"></div><button class="rxx-add" data-pop=".rxx-pk" aria-label="Add reaction" data-tip="Add reaction"><svg class="i xs"><use href="#i-smile"/></svg></button></div>
<div class="pop rxx-pk" role="menu"><div class="rxx-grid"></div></div>
</div>
</div>
</div>
reactions.cssHTML ยท 41 lines
/* ---------- Reactions ---------- */
/* an agent message in the feed: no container; with .panel it becomes a card */
.rxx{padding:14px 0;display:flex;flex-direction:column;gap:10px}
.rxx.panel{padding:14px}
.rxx-m{display:flex;gap:10px}
.rxx-ic{width:30px;height:30px;border-radius:10px;background:var(--accent);color:var(--on-accent);display:grid;place-items:center;flex:none}
.rxx-b{display:flex;flex-direction:column;font-size:13.5px;line-height:1.5;min-width:0}
.rxx-n{display:flex;gap:6px;align-items:baseline;font-size:13px}
.rxx-n b{font-weight:500}
.rxx-n .muted{font-size:11.5px}
.rxx-b p{margin:2px 0 0;color:var(--fg2)}
.rxx-row{display:flex;flex-wrap:wrap;gap:5px;padding-left:40px;align-items:center}
.rxx-list{display:contents}
.rxx-r{position:relative;display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 9px;border-radius:var(--r-pill);background:var(--surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--line) 50%,transparent);font-size:13px;transition:background .2s,box-shadow .2s,transform .15s,opacity .3s}
/* white pills with a hairline, so they stand out on a grey page; yours has a stronger grey edge and its count in the accent */
@media (hover:hover) and (pointer:fine){.rxx-r:hover{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--rail) 50%,transparent)}}
.rxx-r:active{transform:scale(.94)}
.rxx-r.me{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--pick-on) 50%,transparent)}
.rxx-r .n{display:inline-grid;overflow:hidden;height:16px;font-size:12px;line-height:16px;color:var(--fg2);min-width:8px}
.rxx-r .n i{grid-area:1/1;font-style:normal;text-align:center}
.rxx-r.me .n{color:var(--accent-fg,var(--accent));font-weight:500}
.rxx-r .n .iu{animation:rxiu .38s var(--ease) both}.rxx-r .n .ou{animation:rxou .38s var(--ease) both}
.rxx-r .n .id{animation:rxid .38s var(--ease) both}.rxx-r .n .od{animation:rxod .38s var(--ease) both}
@keyframes rxiu{from{transform:translateY(100%);opacity:0}}@keyframes rxou{to{transform:translateY(-100%);opacity:0}}
@keyframes rxid{from{transform:translateY(-100%);opacity:0}}@keyframes rxod{to{transform:translateY(100%);opacity:0}}
.rxx-r::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0}
.rxx-r.bump::after{animation:rxring .6s var(--ease)}
@keyframes rxring{0%{opacity:.5;box-shadow:0 0 0 0 var(--accent-mark,var(--accent))}100%{opacity:0;box-shadow:0 0 0 7px var(--accent-mark,var(--accent))}}
.rxx-r.new{animation:popin .45s var(--ease) backwards}
.rxx-r.gone{opacity:0;transform:scale(.8)}
.rxx-r .e{display:inline-block}
.rxx-r.bump .e{animation:rxp .5s var(--ease)}
@keyframes rxp{0%{transform:scale(1)}35%{transform:scale(1.5) rotate(-12deg)}60%{transform:scale(.92) rotate(4deg)}100%{transform:scale(1)}}
.rxx-add{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;color:var(--muted);transition:background .2s,color .2s}
.rxx-add[aria-expanded="true"]{background:var(--row);color:var(--fg)}
@media (hover:hover) and (pointer:fine){.rxx-add:hover{background:var(--row);color:var(--fg)}}
.rxx-pk{min-width:0;padding:6px}
.rxx-grid{display:grid;grid-template-columns:repeat(6,32px);gap:2px}
.rxx-grid button{width:32px;height:32px;border-radius:var(--r-sm);font-size:17px;transition:background .15s,transform .2s var(--ease)}
@media (hover:hover) and (pointer:fine){.rxx-grid button:hover{background:var(--row);transform:scale(1.15)}}
reactions.jsHTML ยท 21 lines
DM.def('reactions', (r, c) => {
r.innerHTML = r.innerHTML.replace('{spark}', I('spark','xs')).replace('{smile}', I('smile','xs'));
const R = [['๐',4,['Maya','Jon','Ari','Sam'],0],['๐',2,['Jon','Dana'],0],['โ
',1,['Maya'],0]];
const list = $('.rxx-list', r);
const tipOf = b => { const x = b._x, who = x[3] ? ['You', ...x[2]] : x[2]; b.dataset.tip = who.slice(0, 3).join(', ') + (who.length > 3 ? ' and ' + (who.length - 3) + ' more' : ''); if (tipFor === b) showTip(b); };
function make(x){ const b = html(`<button class="rxx-r${x[3] ? ' me' : ''}" aria-pressed="${!!x[3]}"><span class="e">${x[0]}</span><span class="n num"><i>${x[1]}</i></span></button>`); b._x = x; tipOf(b); b.onclick = () => toggle(b); return b; }
/* the count rolls up or down instead of the whole row re-rendering under the pointer */
function roll(b, up){ const n = $('.n', b), old = $('i:last-child', n), nu = html(`<i class="${up ? 'iu' : 'id'}">${b._x[1]}</i>`); old.className = up ? 'ou' : 'od'; n.appendChild(nu); setTimeout(() => old.remove(), 380); }
function bump(b){ b.classList.remove('bump'); void b.offsetWidth; b.classList.add('bump'); }
function toggle(b){ const x = b._x; x[3] = x[3] ? 0 : 1; x[1] += x[3] ? 1 : -1; b.classList.toggle('me', !!x[3]); b.setAttribute('aria-pressed', !!x[3]);
if (x[1] <= 0){ R.splice(R.indexOf(x), 1); b.classList.add('gone'); hideTip(); setTimeout(() => b.remove(), 320); return; }
roll(b, !!x[3]); tipOf(b); if (x[3]) bump(b); }
R.forEach(x => list.appendChild(make(x)));
$('.rxx-grid', r).innerHTML = ['๐','โค๏ธ','๐','๐','๐','โ
','๐ฅ','๐ฏ','๐','๐ค','โจ','๐'].map(e => `<button data-close>${e}</button>`).join('');
$$('.rxx-grid button', r).forEach(b => b.addEventListener('click', () => { const e = b.textContent, x = R.find(y => y[0] === e);
if (x){ const el = $$('.rxx-r', list).find(y => y._x === x); if (!x[3]) toggle(el); else bump(el); }
else { const nx = [e, 1, [], 1]; R.push(nx); const el = make(nx); el.classList.add('new'); list.appendChild(el); bump(el); } }));
// the preview adds one reaction on its own, unless you take over first
c.auto(1100, () => toggle($('.rxx-r', list)));
});
Questions
What people ask about it.
- How do I keep reactions in sync with my backend?
- Control it with
reactionsandonReactionsChange. You get the full updated list after every toggle or pick, and can save it and pass back what the server returns. - What counts as my reaction?
- A reaction with
mine: true. Itscountincludes you, and the tooltip putsmeLabel, "You" by default, before the names inpeople. - Can I change the emoji in the picker?
- Yes. Pass
emojisas an array of strings. Twelve common ones are the default.
More in Collaboration
Threads, inboxes and boards for teams working alongside an agent.