Open demo

CSS code

HTML
<div><span class="lbl">Group thread with reactions, an emoji picker and quoted replies</span>
<div class="gc" data-group>
  <div class="gc-head">
    <div><h2 id="gc-title">#launch-week</h2><p>Fernway app launch, 6 members</p></div>
    <div class="gc-stack" aria-hidden="true"><span class="gc-av">ID</span><span class="gc-av c2">DP</span><span class="gc-av c3">KM</span><span class="gc-av c4">AS</span></div>
  </div>
  <div class="gc-log" role="log" aria-live="polite" aria-labelledby="gc-title" tabindex="0" data-log>
    <p class="gc-day"><time datetime="2026-10-14">Today</time></p>
    <div class="gc-msg" data-msg data-author="Ines Duarte">
      <span class="gc-av" aria-hidden="true">ID</span>
      <div class="gc-col">
        <div class="gc-b"><p class="gc-name n1">Ines Duarte <time datetime="2026-10-14T09:12">09:12</time></p><p data-body>Can we move the demo to Thursday? Staging data won't be ready until Wednesday night.</p></div>
        <div class="gc-acts">
          <div class="gc-acts" role="group" aria-label="Reactions" data-reacts>
            <button class="gc-react" type="button" aria-pressed="false" data-react="👍"><span aria-hidden="true">👍</span><span class="sr-only" data-label>Thumbs up</span> <span data-n>3</span></button>
            <button class="gc-react" type="button" aria-pressed="false" data-react="🙏"><span aria-hidden="true">🙏</span><span class="sr-only" data-label>Thank you</span> <span data-n>1</span></button>
          </div>
          <button class="gc-tool" type="button" aria-expanded="false" aria-controls="gc-pick-1" data-add><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="13" r="8"/><path d="M8 15a4 4 0 0 0 6 0M8.5 11h.01M13.5 11h.01M19 2v6M16 5h6"/></svg><span class="sr-only">Add reaction</span></button>
          <button class="gc-tool" type="button" data-reply>Reply<span class="sr-only"> to Ines Duarte</span></button>
        </div>
        <div class="gc-picker" id="gc-pick-1" role="group" aria-label="Pick a reaction" hidden data-picker>
          <button type="button" data-pick="👍" aria-label="Thumbs up">👍</button><button type="button" data-pick="❤️" aria-label="Heart">❤️</button><button type="button" data-pick="😂" aria-label="Laughing">😂</button><button type="button" data-pick="🎉" aria-label="Party popper">🎉</button><button type="button" data-pick="🚀" aria-label="Rocket">🚀</button><button type="button" data-pick="👀" aria-label="Eyes">👀</button>
        </div>
      </div>
    </div>
    <div class="gc-msg" data-msg data-author="Dev Patel">
      <span class="gc-av c2" aria-hidden="true">DP</span>
      <div class="gc-col">
        <div class="gc-b"><p class="gc-name n2">Dev Patel <time datetime="2026-10-14T09:14">09:14</time></p><blockquote class="gc-quote n1"><b><span class="sr-only">Replying to </span>Ines Duarte</b><span>Can we move the demo to Thursday? Staging data won't be ready until Wednesday night.</span></blockquote><p data-body>Thursday works. I'll move the room booking to 14:00.</p></div>
        <div class="gc-acts">
          <div class="gc-acts" role="group" aria-label="Reactions" data-reacts>
            <button class="gc-react" type="button" aria-pressed="true" data-react="✅"><span aria-hidden="true">✅</span><span class="sr-only" data-label>Check mark</span> <span data-n>2</span></button>
          </div>
          <button class="gc-tool" type="button" aria-expanded="false" aria-controls="gc-pick-2" data-add><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="13" r="8"/><path d="M8 15a4 4 0 0 0 6 0M8.5 11h.01M13.5 11h.01M19 2v6M16 5h6"/></svg><span class="sr-only">Add reaction</span></button>
          <button class="gc-tool" type="button" data-reply>Reply<span class="sr-only"> to Dev Patel</span></button>
        </div>
        <div class="gc-picker" id="gc-pick-2" role="group" aria-label="Pick a reaction" hidden data-picker>
          <button type="button" data-pick="👍" aria-label="Thumbs up">👍</button><button type="button" data-pick="❤️" aria-label="Heart">❤️</button><button type="button" data-pick="😂" aria-label="Laughing">😂</button><button type="button" data-pick="🎉" aria-label="Party popper">🎉</button><button type="button" data-pick="🚀" aria-label="Rocket">🚀</button><button type="button" data-pick="👀" aria-label="Eyes">👀</button>
        </div>
      </div>
    </div>
    <div class="gc-msg out" data-msg data-author="You">
      <div class="gc-col">
        <div class="gc-b"><p class="gc-name"><span class="sr-only">You</span> <time datetime="2026-10-14T09:15">09:15</time></p><p data-body>Same here. I'll update the invite and the press list.</p></div>
        <div class="gc-acts">
          <div class="gc-acts" role="group" aria-label="Reactions" data-reacts>
            <button class="gc-react" type="button" aria-pressed="false" data-react="🎉"><span aria-hidden="true">🎉</span><span class="sr-only" data-label>Party popper</span> <span data-n>2</span></button>
          </div>
          <button class="gc-tool" type="button" aria-expanded="false" aria-controls="gc-pick-3" data-add><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="13" r="8"/><path d="M8 15a4 4 0 0 0 6 0M8.5 11h.01M13.5 11h.01M19 2v6M16 5h6"/></svg><span class="sr-only">Add reaction</span></button>
        </div>
        <div class="gc-picker" id="gc-pick-3" role="group" aria-label="Pick a reaction" hidden data-picker>
          <button type="button" data-pick="👍" aria-label="Thumbs up">👍</button><button type="button" data-pick="❤️" aria-label="Heart">❤️</button><button type="button" data-pick="😂" aria-label="Laughing">😂</button><button type="button" data-pick="🎉" aria-label="Party popper">🎉</button><button type="button" data-pick="🚀" aria-label="Rocket">🚀</button><button type="button" data-pick="👀" aria-label="Eyes">👀</button>
        </div>
      </div>
    </div>
    <div class="gc-msg" data-msg data-author="Kofi Mensah">
      <span class="gc-av c3" aria-hidden="true">KM</span>
      <div class="gc-col">
        <div class="gc-b"><p class="gc-name n3">Kofi Mensah <time datetime="2026-10-14T09:21">09:21</time></p><p data-body>App Store review is back. We're approved for a Thursday launch.</p></div>
        <div class="gc-acts">
          <div class="gc-acts" role="group" aria-label="Reactions" data-reacts>
            <button class="gc-react" type="button" aria-pressed="true" data-react="🚀"><span aria-hidden="true">🚀</span><span class="sr-only" data-label>Rocket</span> <span data-n>5</span></button>
            <button class="gc-react" type="button" aria-pressed="false" data-react="❤️"><span aria-hidden="true">❤️</span><span class="sr-only" data-label>Heart</span> <span data-n>2</span></button>
          </div>
          <button class="gc-tool" type="button" aria-expanded="false" aria-controls="gc-pick-4" data-add><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="13" r="8"/><path d="M8 15a4 4 0 0 0 6 0M8.5 11h.01M13.5 11h.01M19 2v6M16 5h6"/></svg><span class="sr-only">Add reaction</span></button>
          <button class="gc-tool" type="button" data-reply>Reply<span class="sr-only"> to Kofi Mensah</span></button>
        </div>
        <div class="gc-picker" id="gc-pick-4" role="group" aria-label="Pick a reaction" hidden data-picker>
          <button type="button" data-pick="👍" aria-label="Thumbs up">👍</button><button type="button" data-pick="❤️" aria-label="Heart">❤️</button><button type="button" data-pick="😂" aria-label="Laughing">😂</button><button type="button" data-pick="🎉" aria-label="Party popper">🎉</button><button type="button" data-pick="🚀" aria-label="Rocket">🚀</button><button type="button" data-pick="👀" aria-label="Eyes">👀</button>
        </div>
      </div>
    </div>
  </div>
  <div class="gc-foot">
    <div class="gc-replying" hidden data-replying>
      <p>Replying to <b data-reply-name></b>: <span data-reply-text></span></p>
      <button type="button" data-cancel-reply><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg><span class="sr-only">Cancel reply</span></button>
    </div>
    <form class="gc-form" data-form>
      <label class="sr-only" for="gc-input">Message to #launch-week</label>
      <textarea id="gc-input" rows="1" placeholder="Message #launch-week" data-input></textarea>
      <button class="gc-send" type="submit"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg><span class="sr-only">Send</span></button>
    </form>
  </div>
  <p class="sr-only" role="status" data-status></p>
  <template data-tpl="msg"><div class="gc-msg out" data-msg data-author="You"><div class="gc-col"><div class="gc-b"><p class="gc-name"><span class="sr-only">You</span> <time data-time></time></p><blockquote class="gc-quote" data-quote hidden><b><span class="sr-only">Replying to </span><span data-quote-name></span></b><span data-quote-text></span></blockquote><p data-body></p></div></div></div></template>
  <template data-tpl="react"><button class="gc-react" type="button" aria-pressed="true" data-react><span aria-hidden="true" data-emoji></span><span class="sr-only" data-label></span> <span data-n>1</span></button></template>
</div></div>
CSS
/* Group chat: initials avatars, reactions as toggle buttons (aria-pressed) with counts, an emoji picker, and replies that quote the original. */
.gc{--gc-accent:#be185d;--gc-accent-dark:#9d174d;--gc-soft:#fce7f3;--gc-ink:#26151d;--gc-muted:#6a5560;--gc-line:#eedde5;--gc-bg:#fff;--gc-ground:#fbf7f9;
  display:flex;flex-direction:column;overflow:hidden;background:var(--gc-bg);border:1px solid var(--gc-line);border-radius:22px;color:var(--gc-ink);
  font-size:.94rem;line-height:1.45;box-shadow:0 22px 48px -30px rgba(80,20,50,.4)}
.gc :where(h2,p,blockquote){margin:0}
:where(.gc) button{font:inherit;color:inherit;cursor:pointer}
.gc .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.gc :focus-visible{outline:2px solid var(--gc-accent);outline-offset:2px}

.gc-head{display:flex;align-items:center;gap:12px;padding:14px 20px;border-bottom:1px solid var(--gc-line)}
.gc-head h2{font-size:1.02rem;font-weight:700}
.gc-head p{font-size:.8rem;color:var(--gc-muted)}
.gc-stack{display:flex;margin-left:auto}
.gc-stack span{width:30px;height:30px;margin-left:-8px;border:2px solid var(--gc-bg);font-size:.66rem}
.gc-av{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;font-size:.72rem;font-weight:700;background:#fbcfe8;color:#831843}
.gc-av.c2{background:#c7d2fe;color:#312e81}
.gc-av.c3{background:#bbf7d0;color:#14532d}
.gc-av.c4{background:#fde68a;color:#713f12}

.gc-log{display:flex;flex-direction:column;gap:16px;max-height:600px;overflow-y:auto;padding:20px 18px;background:var(--gc-ground)}
.gc-log:focus-visible{outline-offset:-3px}
.gc-day{align-self:center;padding:3px 12px;border-radius:999px;background:var(--gc-line);font-size:.74rem;font-weight:600;color:var(--gc-muted)}
.gc-msg{display:flex;align-items:flex-start;gap:10px;max-width:min(86%,560px)}
.gc-msg.out{align-self:flex-end;flex-direction:row-reverse}
.gc-col{display:flex;flex-direction:column;align-items:flex-start;gap:6px;min-width:0}
.gc-msg.out .gc-col{align-items:flex-end}
.gc-b{padding:8px 13px 7px;border:1px solid var(--gc-line);border-radius:4px 18px 18px 18px;background:var(--gc-bg);overflow-wrap:anywhere;white-space:pre-wrap}
.gc-msg.out .gc-b{border-color:transparent;border-radius:18px 4px 18px 18px;background:var(--gc-accent);color:#fff}
.gc-name{display:flex;align-items:baseline;gap:8px;font-size:.8rem;font-weight:700;white-space:normal}
.gc-name time{font-size:.72rem;font-weight:400;color:var(--gc-muted)}
.gc-msg.out .gc-name time{color:#fbd5e7}
.n1{color:#9d174d}.n2{color:#3730a3}.n3{color:#166534}.n4{color:#854d0e}
.gc-quote{margin:4px 0 6px;padding:6px 10px;border-left:3px solid currentColor;border-radius:4px 10px 10px 4px;background:var(--gc-ground);font-size:.84rem;white-space:normal}
.gc-quote b{display:block;font-size:.76rem}
.gc-quote span{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;color:var(--gc-muted)}
.gc-msg.out .gc-quote{background:#a3154f;color:#fff}
.gc-msg.out .gc-quote span{color:#fbd5e7}

/* reactions: each is a toggle button with a visible count */
.gc-acts{display:flex;flex-wrap:wrap;align-items:center;gap:4px}
.gc-msg.out .gc-acts{justify-content:flex-end}
.gc-react{display:flex;align-items:center;gap:5px;height:28px;padding:0 9px;border:1px solid var(--gc-line);border-radius:999px;background:var(--gc-bg);font-size:.8rem;font-weight:600;color:var(--gc-muted);transition:background-color .15s,border-color .15s}
.gc-react:hover{border-color:#d9a6bf}
.gc-react[aria-pressed="true"]{border-color:var(--gc-accent);background:var(--gc-soft);color:var(--gc-accent-dark)}
.gc-react span[aria-hidden]{font-size:.95rem}
.gc-tool{display:grid;place-items:center;height:28px;min-width:28px;padding:0 8px;border:0;border-radius:999px;background:none;font-size:.78rem;font-weight:600;color:var(--gc-muted)}
.gc-tool:hover,.gc-tool[aria-expanded="true"]{background:var(--gc-line);color:var(--gc-ink)}
.gc-picker{display:flex;flex-wrap:wrap;gap:2px;padding:4px;border:1px solid var(--gc-line);border-radius:14px;background:var(--gc-bg);box-shadow:0 10px 24px -14px rgba(80,20,50,.4)}
.gc-picker[hidden]{display:none}
.gc-picker button{display:grid;place-items:center;width:36px;height:36px;border:0;border-radius:10px;background:none;font-size:1.15rem}
.gc-picker button:hover{background:var(--gc-soft)}

/* reply preview and composer */
.gc-foot{padding:10px 14px 14px;border-top:1px solid var(--gc-line)}
.gc-replying{display:flex;align-items:center;gap:10px;margin-bottom:8px;padding:8px 8px 8px 12px;border-left:3px solid var(--gc-accent);border-radius:4px 12px 12px 4px;background:var(--gc-soft);font-size:.84rem}
.gc-replying[hidden]{display:none}
.gc-replying p{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gc-replying button{flex:none;display:grid;place-items:center;width:28px;height:28px;border:0;border-radius:50%;background:none;color:var(--gc-accent-dark)}
.gc-replying button:hover{background:#f9d0e5}
.gc-form{display:flex;align-items:flex-end;gap:8px}
.gc-form textarea{box-sizing:border-box;flex:1;min-width:0;max-height:130px;padding:10px 14px;border:1.5px solid #9d8a94;border-radius:18px;background:var(--gc-bg);color:var(--gc-ink);font:inherit;line-height:1.4;resize:none}
.gc-form textarea::placeholder{color:#6f5d66}
.gc-form textarea:focus-visible{border-color:var(--gc-accent);outline-offset:1px}
.gc-send{flex:none;display:grid;place-items:center;width:44px;height:44px;border:0;border-radius:50%;background:var(--gc-accent);color:#fff}
.gc-send:hover{background:var(--gc-accent-dark)}
.gc-send:disabled{background:#eadde3;color:#5f4d56;cursor:not-allowed}
@media (max-width:520px){.gc-log{padding:16px 10px}.gc-msg{max-width:94%}.gc-msg>.gc-av{width:28px;height:28px;font-size:.62rem}.gc-stack{display:none}}
@media (prefers-reduced-motion:reduce){.gc-react{transition:none}}
JavaScript
/* Group chat: reactions toggle with aria-pressed and update their count, the picker adds a reaction, Reply quotes a message
   in the composer, and Esc closes the picker or cancels the reply. Sending only appends to the page. */
document.querySelectorAll('[data-group]').forEach(function (chat) {
  var log = chat.querySelector('[data-log]'), form = chat.querySelector('[data-form]'), input = chat.querySelector('[data-input]');
  var send = form.querySelector('[type="submit"]'), status = chat.querySelector('[data-status]'), box = chat.querySelector('[data-replying]');
  var replyTo = null;
  var pad = function (n) { return String(n).padStart(2, '0'); };

  function closePicker(p, refocus) {
    p.hidden = true;
    var t = chat.querySelector('[aria-controls="' + p.id + '"]');
    t.setAttribute('aria-expanded', 'false');
    if (refocus) t.focus();
  }
  function toggleReaction(btn) {
    var on = btn.getAttribute('aria-pressed') !== 'true', n = btn.querySelector('[data-n]'), count = +n.textContent + (on ? 1 : -1);
    var label = btn.querySelector('[data-label]').textContent;
    if (!count) { status.textContent = label + ' removed'; return btn.remove(); }
    btn.setAttribute('aria-pressed', String(on)); n.textContent = count;
    status.textContent = label + (on ? ' added, ' : ' removed, ') + count + ' in total';
  }
  function setReply(msg) {
    replyTo = msg;
    box.hidden = !msg;
    if (msg) {
      box.querySelector('[data-reply-name]').textContent = msg.dataset.author;
      box.querySelector('[data-reply-text]').textContent = msg.querySelector('[data-body]').textContent;
      input.focus();
    }
  }

  chat.addEventListener('click', function (e) {
    var b = e.target.closest('button');
    if (!b) return;
    if (b.hasAttribute('data-react')) toggleReaction(b);
    if (b.hasAttribute('data-add')) {
      var p = chat.querySelector('#' + b.getAttribute('aria-controls')), open = p.hidden;
      chat.querySelectorAll('[data-picker]').forEach(function (x) { if (!x.hidden) closePicker(x); });
      if (open) { p.hidden = false; b.setAttribute('aria-expanded', 'true'); p.querySelector('button').focus(); }
    }
    if (b.hasAttribute('data-pick')) {
      var msg = b.closest('[data-msg]'), list = msg.querySelector('[data-reacts]'), emoji = b.dataset.pick;
      var chip = list.querySelector('[data-react="' + emoji + '"]');
      if (chip) { if (chip.getAttribute('aria-pressed') !== 'true') toggleReaction(chip); }
      else {
        chip = chat.querySelector('template[data-tpl="react"]').content.firstElementChild.cloneNode(true);
        chip.dataset.react = emoji;
        chip.querySelector('[data-emoji]').textContent = emoji;
        chip.querySelector('[data-label]').textContent = b.getAttribute('aria-label');
        list.appendChild(chip);
        status.textContent = b.getAttribute('aria-label') + ' added, 1 in total';
      }
      closePicker(b.closest('[data-picker]'));
      chip.focus();
    }
    if (b.hasAttribute('data-reply')) setReply(b.closest('[data-msg]'));
    if (b.hasAttribute('data-cancel-reply')) { setReply(null); input.focus(); }
  });
  chat.addEventListener('keydown', function (e) {
    if (e.key !== 'Escape') return;
    var p = e.target.closest('[data-picker]');
    if (p) { e.preventDefault(); closePicker(p, true); }
    else if (replyTo && e.target === input) { e.preventDefault(); setReply(null); }
  });

  function grow() { input.style.height = 'auto'; input.style.height = input.scrollHeight + 'px'; send.disabled = !input.value.trim(); }
  input.addEventListener('input', grow);
  input.addEventListener('keydown', function (e) {
    if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); form.requestSubmit(); }
  });
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var text = input.value.trim();
    if (!text) return;
    var d = new Date(), msg = chat.querySelector('template[data-tpl="msg"]').content.cloneNode(true), time = msg.querySelector('[data-time]');
    msg.querySelector('[data-body]').textContent = text;
    time.textContent = pad(d.getHours()) + ':' + pad(d.getMinutes());
    time.dateTime = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + time.textContent;
    if (replyTo) {
      var q = msg.querySelector('[data-quote]');
      q.hidden = false;
      q.querySelector('[data-quote-name]').textContent = replyTo.dataset.author;
      q.querySelector('[data-quote-text]').textContent = replyTo.querySelector('[data-body]').textContent;
    }
    log.appendChild(msg);
    log.scrollTop = log.scrollHeight;
    setReply(null); input.value = ''; grow(); input.focus();
  });
  log.scrollTop = log.scrollHeight;
  grow();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Group thread with reactions, an emoji picker and quoted replies</span>
<div class="card gc" data-group>
  <div class="gc-head">
    <div><h2 class="fw-bold" id="gc-title">#launch-week</h2><p>Fernway app launch, 6 members</p></div>
    <div class="gc-stack" aria-hidden="true"><span class="gc-av">ID</span><span class="gc-av c2">DP</span><span class="gc-av c3">KM</span><span class="gc-av c4">AS</span></div>
  </div>
  <div class="gc-log" role="log" aria-live="polite" aria-labelledby="gc-title" tabindex="0" data-log>
    <p class="gc-day"><time datetime="2026-10-14">Today</time></p>
    <div class="gc-msg" data-msg data-author="Ines Duarte">
      <span class="gc-av" aria-hidden="true">ID</span>
      <div class="gc-col">
        <div class="gc-b"><p class="gc-name n1">Ines Duarte <time datetime="2026-10-14T09:12">09:12</time></p><p data-body>Can we move the demo to Thursday? Staging data won't be ready until Wednesday night.</p></div>
        <div class="gc-acts">
          <div class="gc-acts" role="group" aria-label="Reactions" data-reacts>
            <button class="btn gc-react" type="button" aria-pressed="false" data-react="👍"><span aria-hidden="true">👍</span><span class="visually-hidden" data-label>Thumbs up</span> <span data-n>3</span></button>
            <button class="btn gc-react" type="button" aria-pressed="false" data-react="🙏"><span aria-hidden="true">🙏</span><span class="visually-hidden" data-label>Thank you</span> <span data-n>1</span></button>
          </div>
          <button class="btn gc-tool" type="button" aria-expanded="false" aria-controls="gc-pick-1" data-add><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="13" r="8"/><path d="M8 15a4 4 0 0 0 6 0M8.5 11h.01M13.5 11h.01M19 2v6M16 5h6"/></svg><span class="visually-hidden">Add reaction</span></button>
          <button class="btn gc-tool" type="button" data-reply>Reply<span class="visually-hidden"> to Ines Duarte</span></button>
        </div>
        <div class="gc-picker" id="gc-pick-1" role="group" aria-label="Pick a reaction" hidden data-picker>
          <button class="btn gc-pick" type="button" data-pick="👍" aria-label="Thumbs up">👍</button><button class="btn gc-pick" type="button" data-pick="❤️" aria-label="Heart">❤️</button><button class="btn gc-pick" type="button" data-pick="😂" aria-label="Laughing">😂</button><button class="btn gc-pick" type="button" data-pick="🎉" aria-label="Party popper">🎉</button><button class="btn gc-pick" type="button" data-pick="🚀" aria-label="Rocket">🚀</button><button class="btn gc-pick" type="button" data-pick="👀" aria-label="Eyes">👀</button>
        </div>
      </div>
    </div>
    <div class="gc-msg" data-msg data-author="Dev Patel">
      <span class="gc-av c2" aria-hidden="true">DP</span>
      <div class="gc-col">
        <div class="gc-b"><p class="gc-name n2">Dev Patel <time datetime="2026-10-14T09:14">09:14</time></p><blockquote class="gc-quote n1"><b><span class="visually-hidden">Replying to </span>Ines Duarte</b><span>Can we move the demo to Thursday? Staging data won't be ready until Wednesday night.</span></blockquote><p data-body>Thursday works. I'll move the room booking to 14:00.</p></div>
        <div class="gc-acts">
          <div class="gc-acts" role="group" aria-label="Reactions" data-reacts>
            <button class="btn gc-react" type="button" aria-pressed="true" data-react="✅"><span aria-hidden="true">✅</span><span class="visually-hidden" data-label>Check mark</span> <span data-n>2</span></button>
          </div>
          <button class="btn gc-tool" type="button" aria-expanded="false" aria-controls="gc-pick-2" data-add><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="13" r="8"/><path d="M8 15a4 4 0 0 0 6 0M8.5 11h.01M13.5 11h.01M19 2v6M16 5h6"/></svg><span class="visually-hidden">Add reaction</span></button>
          <button class="btn gc-tool" type="button" data-reply>Reply<span class="visually-hidden"> to Dev Patel</span></button>
        </div>
        <div class="gc-picker" id="gc-pick-2" role="group" aria-label="Pick a reaction" hidden data-picker>
          <button class="btn gc-pick" type="button" data-pick="👍" aria-label="Thumbs up">👍</button><button class="btn gc-pick" type="button" data-pick="❤️" aria-label="Heart">❤️</button><button class="btn gc-pick" type="button" data-pick="😂" aria-label="Laughing">😂</button><button class="btn gc-pick" type="button" data-pick="🎉" aria-label="Party popper">🎉</button><button class="btn gc-pick" type="button" data-pick="🚀" aria-label="Rocket">🚀</button><button class="btn gc-pick" type="button" data-pick="👀" aria-label="Eyes">👀</button>
        </div>
      </div>
    </div>
    <div class="gc-msg out" data-msg data-author="You">
      <div class="gc-col">
        <div class="gc-b"><p class="gc-name"><span class="visually-hidden">You</span> <time datetime="2026-10-14T09:15">09:15</time></p><p data-body>Same here. I'll update the invite and the press list.</p></div>
        <div class="gc-acts">
          <div class="gc-acts" role="group" aria-label="Reactions" data-reacts>
            <button class="btn gc-react" type="button" aria-pressed="false" data-react="🎉"><span aria-hidden="true">🎉</span><span class="visually-hidden" data-label>Party popper</span> <span data-n>2</span></button>
          </div>
          <button class="btn gc-tool" type="button" aria-expanded="false" aria-controls="gc-pick-3" data-add><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="13" r="8"/><path d="M8 15a4 4 0 0 0 6 0M8.5 11h.01M13.5 11h.01M19 2v6M16 5h6"/></svg><span class="visually-hidden">Add reaction</span></button>
        </div>
        <div class="gc-picker" id="gc-pick-3" role="group" aria-label="Pick a reaction" hidden data-picker>
          <button class="btn gc-pick" type="button" data-pick="👍" aria-label="Thumbs up">👍</button><button class="btn gc-pick" type="button" data-pick="❤️" aria-label="Heart">❤️</button><button class="btn gc-pick" type="button" data-pick="😂" aria-label="Laughing">😂</button><button class="btn gc-pick" type="button" data-pick="🎉" aria-label="Party popper">🎉</button><button class="btn gc-pick" type="button" data-pick="🚀" aria-label="Rocket">🚀</button><button class="btn gc-pick" type="button" data-pick="👀" aria-label="Eyes">👀</button>
        </div>
      </div>
    </div>
    <div class="gc-msg" data-msg data-author="Kofi Mensah">
      <span class="gc-av c3" aria-hidden="true">KM</span>
      <div class="gc-col">
        <div class="gc-b"><p class="gc-name n3">Kofi Mensah <time datetime="2026-10-14T09:21">09:21</time></p><p data-body>App Store review is back. We're approved for a Thursday launch.</p></div>
        <div class="gc-acts">
          <div class="gc-acts" role="group" aria-label="Reactions" data-reacts>
            <button class="btn gc-react" type="button" aria-pressed="true" data-react="🚀"><span aria-hidden="true">🚀</span><span class="visually-hidden" data-label>Rocket</span> <span data-n>5</span></button>
            <button class="btn gc-react" type="button" aria-pressed="false" data-react="❤️"><span aria-hidden="true">❤️</span><span class="visually-hidden" data-label>Heart</span> <span data-n>2</span></button>
          </div>
          <button class="btn gc-tool" type="button" aria-expanded="false" aria-controls="gc-pick-4" data-add><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="13" r="8"/><path d="M8 15a4 4 0 0 0 6 0M8.5 11h.01M13.5 11h.01M19 2v6M16 5h6"/></svg><span class="visually-hidden">Add reaction</span></button>
          <button class="btn gc-tool" type="button" data-reply>Reply<span class="visually-hidden"> to Kofi Mensah</span></button>
        </div>
        <div class="gc-picker" id="gc-pick-4" role="group" aria-label="Pick a reaction" hidden data-picker>
          <button class="btn gc-pick" type="button" data-pick="👍" aria-label="Thumbs up">👍</button><button class="btn gc-pick" type="button" data-pick="❤️" aria-label="Heart">❤️</button><button class="btn gc-pick" type="button" data-pick="😂" aria-label="Laughing">😂</button><button class="btn gc-pick" type="button" data-pick="🎉" aria-label="Party popper">🎉</button><button class="btn gc-pick" type="button" data-pick="🚀" aria-label="Rocket">🚀</button><button class="btn gc-pick" type="button" data-pick="👀" aria-label="Eyes">👀</button>
        </div>
      </div>
    </div>
  </div>
  <div class="gc-foot">
    <div class="gc-replying" hidden data-replying>
      <p>Replying to <b data-reply-name></b>: <span data-reply-text></span></p>
      <button class="btn-close" type="button" aria-label="Cancel reply" data-cancel-reply></button>
    </div>
    <form class="gc-form" data-form>
      <label class="visually-hidden" for="gc-input">Message to #launch-week</label>
      <textarea class="form-control" id="gc-input" rows="1" placeholder="Message #launch-week" data-input></textarea>
      <button class="btn gc-send" type="submit"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg><span class="visually-hidden">Send</span></button>
    </form>
  </div>
  <p class="visually-hidden" role="status" data-status></p>
  <template data-tpl="msg"><div class="gc-msg out" data-msg data-author="You"><div class="gc-col"><div class="gc-b"><p class="gc-name"><span class="visually-hidden">You</span> <time data-time></time></p><blockquote class="gc-quote" data-quote hidden><b><span class="visually-hidden">Replying to </span><span data-quote-name></span></b><span data-quote-text></span></blockquote><p data-body></p></div></div></div></template>
  <template data-tpl="react"><button class="btn gc-react" type="button" aria-pressed="true" data-react><span aria-hidden="true" data-emoji></span><span class="visually-hidden" data-label></span> <span data-n>1</span></button></template>
</div></div>
CSS
/* Group chat on Bootstrap: a .card with .btn reactions and tools, .form-control composer and .btn-close. Initials avatars, reactions as toggle buttons (aria-pressed) with counts, an emoji picker, and replies that quote the original. */
.gc{--gc-accent:#be185d;--gc-accent-dark:#9d174d;--gc-soft:#fce7f3;--gc-ink:#26151d;--gc-muted:#6a5560;--gc-line:#eedde5;--gc-bg:#fff;--gc-ground:#fbf7f9;
  --bs-card-bg:var(--gc-bg);--bs-card-border-color:var(--gc-line);--bs-card-border-radius:22px;--bs-card-color:var(--gc-ink);overflow:hidden;
  font-size:.94rem;line-height:1.45;box-shadow:0 22px 48px -30px rgba(80,20,50,.4)}
.gc :where(h2,p,blockquote){margin:0}
.gc :focus-visible{outline:2px solid var(--gc-accent);outline-offset:2px;box-shadow:none}
.gc .btn{--bs-btn-focus-box-shadow:none;--bs-btn-padding-y:0;--bs-btn-line-height:1.2}

.gc-head{display:flex;align-items:center;gap:12px;padding:14px 20px;border-bottom:1px solid var(--gc-line)}
.gc-head h2{font-size:1.02rem;font-weight:700}
.gc-head p{font-size:.8rem;color:var(--gc-muted)}
.gc-stack{display:flex;margin-left:auto}
.gc-stack span{width:30px;height:30px;margin-left:-8px;border:2px solid var(--gc-bg);font-size:.66rem}
.gc-av{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;font-size:.72rem;font-weight:700;background:#fbcfe8;color:#831843}
.gc-av.c2{background:#c7d2fe;color:#312e81}
.gc-av.c3{background:#bbf7d0;color:#14532d}
.gc-av.c4{background:#fde68a;color:#713f12}

.gc-log{display:flex;flex-direction:column;gap:16px;max-height:600px;overflow-y:auto;padding:20px 18px;background:var(--gc-ground)}
.gc-log:focus-visible{outline-offset:-3px}
.gc-day{align-self:center;padding:3px 12px;border-radius:999px;background:var(--gc-line);font-size:.74rem;font-weight:600;color:var(--gc-muted)}
.gc-msg{display:flex;align-items:flex-start;gap:10px;max-width:min(86%,560px)}
.gc-msg.out{align-self:flex-end;flex-direction:row-reverse}
.gc-col{display:flex;flex-direction:column;align-items:flex-start;gap:6px;min-width:0}
.gc-msg.out .gc-col{align-items:flex-end}
.gc-b{padding:8px 13px 7px;border:1px solid var(--gc-line);border-radius:4px 18px 18px 18px;background:var(--gc-bg);overflow-wrap:anywhere;white-space:pre-wrap}
.gc-msg.out .gc-b{border-color:transparent;border-radius:18px 4px 18px 18px;background:var(--gc-accent);color:#fff}
.gc-name{display:flex;align-items:baseline;gap:8px;font-size:.8rem;font-weight:700;white-space:normal}
.gc-name time{font-size:.72rem;font-weight:400;color:var(--gc-muted)}
.gc-msg.out .gc-name time{color:#fbd5e7}
.n1{color:#9d174d}.n2{color:#3730a3}.n3{color:#166534}.n4{color:#854d0e}
.gc-quote{margin:4px 0 6px;padding:6px 10px;border-left:3px solid currentColor;border-radius:4px 10px 10px 4px;background:var(--gc-ground);font-size:.84rem;white-space:normal}
.gc-quote b{display:block;font-size:.76rem;font-weight:700}
.gc-quote span{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;color:var(--gc-muted)}
.gc-msg.out .gc-quote{background:#a3154f;color:#fff}
.gc-msg.out .gc-quote span{color:#fbd5e7}

/* reactions: each is a toggle button with a visible count */
.gc-acts{display:flex;flex-wrap:wrap;align-items:center;gap:4px}
.gc-msg.out .gc-acts{justify-content:flex-end}
.gc-react{--bs-btn-padding-x:9px;--bs-btn-font-size:.8rem;--bs-btn-font-weight:600;--bs-btn-color:var(--gc-muted);--bs-btn-bg:var(--gc-bg);--bs-btn-border-color:var(--gc-line);--bs-btn-border-radius:999px;--bs-btn-hover-color:var(--gc-muted);--bs-btn-hover-bg:var(--gc-bg);--bs-btn-hover-border-color:#d9a6bf;--bs-btn-active-color:var(--gc-accent-dark);--bs-btn-active-bg:var(--gc-soft);--bs-btn-active-border-color:var(--gc-accent);
  display:flex;align-items:center;gap:5px;height:28px;transition:background-color .15s,border-color .15s}
.gc-react[aria-pressed="true"]{border-color:var(--gc-accent);background:var(--gc-soft);color:var(--gc-accent-dark)}
.gc-react span[aria-hidden]{font-size:.95rem}
.gc-tool{--bs-btn-padding-x:8px;--bs-btn-font-size:.78rem;--bs-btn-font-weight:600;--bs-btn-color:var(--gc-muted);--bs-btn-border-width:0;--bs-btn-border-radius:999px;--bs-btn-hover-color:var(--gc-ink);--bs-btn-hover-bg:var(--gc-line);--bs-btn-active-color:var(--gc-ink);--bs-btn-active-bg:var(--gc-line);display:grid;place-items:center;height:28px;min-width:28px}
.gc-tool[aria-expanded="true"]{background:var(--gc-line);color:var(--gc-ink)}
.gc-picker{display:flex;flex-wrap:wrap;gap:2px;padding:4px;border:1px solid var(--gc-line);border-radius:14px;background:var(--gc-bg);box-shadow:0 10px 24px -14px rgba(80,20,50,.4)}
.gc-picker[hidden]{display:none}
.gc-pick{--bs-btn-padding-x:0;--bs-btn-font-size:1.15rem;--bs-btn-border-width:0;--bs-btn-border-radius:10px;--bs-btn-hover-bg:var(--gc-soft);--bs-btn-active-bg:var(--gc-soft);display:grid;place-items:center;width:36px;height:36px}

/* reply preview and composer */
.gc-foot{padding:10px 14px 14px;border-top:1px solid var(--gc-line)}
.gc-replying{display:flex;align-items:center;gap:10px;margin-bottom:8px;padding:8px 8px 8px 12px;border-left:3px solid var(--gc-accent);border-radius:4px 12px 12px 4px;background:var(--gc-soft);font-size:.84rem}
.gc-replying[hidden]{display:none}
.gc-replying p{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gc-replying .btn-close{--bs-btn-close-opacity:.8;--bs-btn-close-focus-shadow:none;flex:none;width:28px;height:28px;padding:0;border-radius:50%;background-size:10px}
.gc-replying .btn-close:hover{background-color:#f9d0e5}
.gc-form{display:flex;align-items:flex-end;gap:8px}
.gc-form .form-control{--bs-border-color:#9d8a94;max-height:130px;padding:10px 14px;border-width:1.5px;border-radius:18px;color:var(--gc-ink);font-size:inherit;line-height:1.4;resize:none}
.gc-form .form-control::placeholder{color:#6f5d66}
.gc-form .form-control:focus{border-color:var(--gc-accent);box-shadow:none}
.gc-form .form-control:focus-visible{outline-offset:1px}
.gc-send{--bs-btn-padding-x:0;--bs-btn-border-width:0;--bs-btn-border-radius:50%;--bs-btn-color:#fff;--bs-btn-bg:var(--gc-accent);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--gc-accent-dark);--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--gc-accent-dark);--bs-btn-disabled-color:#5f4d56;--bs-btn-disabled-bg:#eadde3;--bs-btn-disabled-opacity:1;display:grid;place-items:center;flex:none;width:44px;height:44px}
@media (max-width:520px){.gc-log{padding:16px 10px}.gc-msg{max-width:94%}.gc-msg>.gc-av{width:28px;height:28px;font-size:.62rem}.gc-stack{display:none}}
@media (prefers-reduced-motion:reduce){.gc-react{transition:none}}
JavaScript
/* Group chat: reactions toggle with aria-pressed and update their count, the picker adds a reaction, Reply quotes a message
   in the composer, and Esc closes the picker or cancels the reply. Sending only appends to the page. */
document.querySelectorAll('[data-group]').forEach(function (chat) {
  var log = chat.querySelector('[data-log]'), form = chat.querySelector('[data-form]'), input = chat.querySelector('[data-input]');
  var send = form.querySelector('[type="submit"]'), status = chat.querySelector('[data-status]'), box = chat.querySelector('[data-replying]');
  var replyTo = null;
  var pad = function (n) { return String(n).padStart(2, '0'); };

  function closePicker(p, refocus) {
    p.hidden = true;
    var t = chat.querySelector('[aria-controls="' + p.id + '"]');
    t.setAttribute('aria-expanded', 'false');
    if (refocus) t.focus();
  }
  function toggleReaction(btn) {
    var on = btn.getAttribute('aria-pressed') !== 'true', n = btn.querySelector('[data-n]'), count = +n.textContent + (on ? 1 : -1);
    var label = btn.querySelector('[data-label]').textContent;
    if (!count) { status.textContent = label + ' removed'; return btn.remove(); }
    btn.setAttribute('aria-pressed', String(on)); n.textContent = count;
    status.textContent = label + (on ? ' added, ' : ' removed, ') + count + ' in total';
  }
  function setReply(msg) {
    replyTo = msg;
    box.hidden = !msg;
    if (msg) {
      box.querySelector('[data-reply-name]').textContent = msg.dataset.author;
      box.querySelector('[data-reply-text]').textContent = msg.querySelector('[data-body]').textContent;
      input.focus();
    }
  }

  chat.addEventListener('click', function (e) {
    var b = e.target.closest('button');
    if (!b) return;
    if (b.hasAttribute('data-react')) toggleReaction(b);
    if (b.hasAttribute('data-add')) {
      var p = chat.querySelector('#' + b.getAttribute('aria-controls')), open = p.hidden;
      chat.querySelectorAll('[data-picker]').forEach(function (x) { if (!x.hidden) closePicker(x); });
      if (open) { p.hidden = false; b.setAttribute('aria-expanded', 'true'); p.querySelector('button').focus(); }
    }
    if (b.hasAttribute('data-pick')) {
      var msg = b.closest('[data-msg]'), list = msg.querySelector('[data-reacts]'), emoji = b.dataset.pick;
      var chip = list.querySelector('[data-react="' + emoji + '"]');
      if (chip) { if (chip.getAttribute('aria-pressed') !== 'true') toggleReaction(chip); }
      else {
        chip = chat.querySelector('template[data-tpl="react"]').content.firstElementChild.cloneNode(true);
        chip.dataset.react = emoji;
        chip.querySelector('[data-emoji]').textContent = emoji;
        chip.querySelector('[data-label]').textContent = b.getAttribute('aria-label');
        list.appendChild(chip);
        status.textContent = b.getAttribute('aria-label') + ' added, 1 in total';
      }
      closePicker(b.closest('[data-picker]'));
      chip.focus();
    }
    if (b.hasAttribute('data-reply')) setReply(b.closest('[data-msg]'));
    if (b.hasAttribute('data-cancel-reply')) { setReply(null); input.focus(); }
  });
  chat.addEventListener('keydown', function (e) {
    if (e.key !== 'Escape') return;
    var p = e.target.closest('[data-picker]');
    if (p) { e.preventDefault(); closePicker(p, true); }
    else if (replyTo && e.target === input) { e.preventDefault(); setReply(null); }
  });

  function grow() { input.style.height = 'auto'; input.style.height = input.scrollHeight + 'px'; send.disabled = !input.value.trim(); }
  input.addEventListener('input', grow);
  input.addEventListener('keydown', function (e) {
    if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); form.requestSubmit(); }
  });
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var text = input.value.trim();
    if (!text) return;
    var d = new Date(), msg = chat.querySelector('template[data-tpl="msg"]').content.cloneNode(true), time = msg.querySelector('[data-time]');
    msg.querySelector('[data-body]').textContent = text;
    time.textContent = pad(d.getHours()) + ':' + pad(d.getMinutes());
    time.dateTime = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + time.textContent;
    if (replyTo) {
      var q = msg.querySelector('[data-quote]');
      q.hidden = false;
      q.querySelector('[data-quote-name]').textContent = replyTo.dataset.author;
      q.querySelector('[data-quote-text]').textContent = replyTo.querySelector('[data-body]').textContent;
    }
    log.appendChild(msg);
    log.scrollTop = log.scrollHeight;
    setReply(null); input.value = ''; grow(); input.focus();
  });
  log.scrollTop = log.scrollHeight;
  grow();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-gc-muted uppercase">Group thread with reactions, an emoji picker and quoted replies</span>
<div class="flex flex-col overflow-hidden rounded-[22px] border border-gc-line bg-white text-[.94rem] leading-[1.45] text-gc-ink shadow-[0_22px_48px_-30px_rgba(80,20,50,.4)]" data-group>
  <div class="flex items-center gap-3 border-b border-gc-line px-5 py-3.5">
    <div><h2 class="text-[1.02rem] font-bold" id="gc-title">#launch-week</h2><p class="text-[.8rem] text-gc-muted">Fernway app launch, 6 members</p></div>
    <div class="ml-auto flex max-[520px]:hidden" aria-hidden="true"><span class="-ml-2 grid size-[30px] place-items-center rounded-full border-2 border-white text-[.66rem] font-bold bg-[#fbcfe8] text-[#831843]">ID</span><span class="-ml-2 grid size-[30px] place-items-center rounded-full border-2 border-white text-[.66rem] font-bold bg-[#c7d2fe] text-[#312e81]">DP</span><span class="-ml-2 grid size-[30px] place-items-center rounded-full border-2 border-white text-[.66rem] font-bold bg-[#bbf7d0] text-[#14532d]">KM</span><span class="-ml-2 grid size-[30px] place-items-center rounded-full border-2 border-white text-[.66rem] font-bold bg-[#fde68a] text-[#713f12]">AS</span></div>
  </div>
  <div class="flex max-h-[600px] flex-col gap-4 overflow-y-auto bg-gc-ground px-[18px] py-5 focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-gc-accent max-[520px]:px-2.5 max-[520px]:py-4" role="log" aria-live="polite" aria-labelledby="gc-title" tabindex="0" data-log>
    <p class="self-center rounded-full bg-gc-line px-3 py-[3px] text-[.74rem] font-semibold text-gc-muted"><time class="text-[.72rem] font-normal text-gc-muted" datetime="2026-10-14">Today</time></p>
    <div class="flex max-w-[min(86%,560px)] items-start gap-2.5 max-[520px]:max-w-[94%]" data-msg data-author="Ines Duarte">
      <span class="grid size-[34px] flex-none place-items-center rounded-full text-[.72rem] font-bold max-[520px]:size-7 max-[520px]:text-[.62rem] bg-[#fbcfe8] text-[#831843]" aria-hidden="true">ID</span>
      <div class="flex min-w-0 flex-col items-start gap-1.5">
        <div class="rounded-[4px_18px_18px_18px] border border-gc-line bg-white px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><p class="flex items-baseline gap-2 text-[.8rem] font-bold whitespace-normal text-[#9d174d]">Ines Duarte <time class="text-[.72rem] font-normal text-gc-muted" datetime="2026-10-14T09:12">09:12</time></p><p data-body>Can we move the demo to Thursday? Staging data won't be ready until Wednesday night.</p></div>
        <div class="flex flex-wrap items-center gap-1">
          <div class="flex flex-wrap items-center gap-1" role="group" aria-label="Reactions" data-reacts>
            <button class="flex h-7 cursor-pointer items-center gap-[5px] rounded-full border border-gc-line bg-white px-[9px] text-[.8rem] font-semibold text-gc-muted transition-[background-color,border-color] duration-150 hover:border-[#d9a6bf] aria-pressed:border-gc-accent aria-pressed:bg-gc-soft aria-pressed:text-gc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" aria-pressed="false" data-react="👍"><span class="text-[.95rem]" aria-hidden="true">👍</span><span class="sr-only" data-label>Thumbs up</span> <span data-n>3</span></button>
            <button class="flex h-7 cursor-pointer items-center gap-[5px] rounded-full border border-gc-line bg-white px-[9px] text-[.8rem] font-semibold text-gc-muted transition-[background-color,border-color] duration-150 hover:border-[#d9a6bf] aria-pressed:border-gc-accent aria-pressed:bg-gc-soft aria-pressed:text-gc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" aria-pressed="false" data-react="🙏"><span class="text-[.95rem]" aria-hidden="true">🙏</span><span class="sr-only" data-label>Thank you</span> <span data-n>1</span></button>
          </div>
          <button class="grid h-7 min-w-7 cursor-pointer place-items-center rounded-full px-2 text-[.78rem] font-semibold text-gc-muted hover:bg-gc-line hover:text-gc-ink aria-expanded:bg-gc-line aria-expanded:text-gc-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" aria-expanded="false" aria-controls="gc-pick-1" data-add><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="13" r="8"/><path d="M8 15a4 4 0 0 0 6 0M8.5 11h.01M13.5 11h.01M19 2v6M16 5h6"/></svg><span class="sr-only">Add reaction</span></button>
          <button class="grid h-7 min-w-7 cursor-pointer place-items-center rounded-full px-2 text-[.78rem] font-semibold text-gc-muted hover:bg-gc-line hover:text-gc-ink aria-expanded:bg-gc-line aria-expanded:text-gc-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-reply>Reply<span class="sr-only"> to Ines Duarte</span></button>
        </div>
        <div class="flex flex-wrap gap-0.5 rounded-[14px] border border-gc-line bg-white p-1 shadow-[0_10px_24px_-14px_rgba(80,20,50,.4)]" id="gc-pick-1" role="group" aria-label="Pick a reaction" hidden data-picker>
          <button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="👍" aria-label="Thumbs up">👍</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="❤️" aria-label="Heart">❤️</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="😂" aria-label="Laughing">😂</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="🎉" aria-label="Party popper">🎉</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="🚀" aria-label="Rocket">🚀</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="👀" aria-label="Eyes">👀</button>
        </div>
      </div>
    </div>
    <div class="flex max-w-[min(86%,560px)] items-start gap-2.5 max-[520px]:max-w-[94%]" data-msg data-author="Dev Patel">
      <span class="grid size-[34px] flex-none place-items-center rounded-full text-[.72rem] font-bold max-[520px]:size-7 max-[520px]:text-[.62rem] bg-[#c7d2fe] text-[#312e81]" aria-hidden="true">DP</span>
      <div class="flex min-w-0 flex-col items-start gap-1.5">
        <div class="rounded-[4px_18px_18px_18px] border border-gc-line bg-white px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><p class="flex items-baseline gap-2 text-[.8rem] font-bold whitespace-normal text-[#3730a3]">Dev Patel <time class="text-[.72rem] font-normal text-gc-muted" datetime="2026-10-14T09:14">09:14</time></p><blockquote class="mt-1 mb-1.5 rounded-[4px_10px_10px_4px] border-l-[3px] border-current bg-gc-ground px-2.5 py-1.5 text-[.84rem] whitespace-normal text-[#9d174d]"><b class="block text-[.76rem]"><span class="sr-only">Replying to </span>Ines Duarte</b><span class="line-clamp-2 text-gc-muted">Can we move the demo to Thursday? Staging data won't be ready until Wednesday night.</span></blockquote><p data-body>Thursday works. I'll move the room booking to 14:00.</p></div>
        <div class="flex flex-wrap items-center gap-1">
          <div class="flex flex-wrap items-center gap-1" role="group" aria-label="Reactions" data-reacts>
            <button class="flex h-7 cursor-pointer items-center gap-[5px] rounded-full border border-gc-line bg-white px-[9px] text-[.8rem] font-semibold text-gc-muted transition-[background-color,border-color] duration-150 hover:border-[#d9a6bf] aria-pressed:border-gc-accent aria-pressed:bg-gc-soft aria-pressed:text-gc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" aria-pressed="true" data-react="✅"><span class="text-[.95rem]" aria-hidden="true">✅</span><span class="sr-only" data-label>Check mark</span> <span data-n>2</span></button>
          </div>
          <button class="grid h-7 min-w-7 cursor-pointer place-items-center rounded-full px-2 text-[.78rem] font-semibold text-gc-muted hover:bg-gc-line hover:text-gc-ink aria-expanded:bg-gc-line aria-expanded:text-gc-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" aria-expanded="false" aria-controls="gc-pick-2" data-add><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="13" r="8"/><path d="M8 15a4 4 0 0 0 6 0M8.5 11h.01M13.5 11h.01M19 2v6M16 5h6"/></svg><span class="sr-only">Add reaction</span></button>
          <button class="grid h-7 min-w-7 cursor-pointer place-items-center rounded-full px-2 text-[.78rem] font-semibold text-gc-muted hover:bg-gc-line hover:text-gc-ink aria-expanded:bg-gc-line aria-expanded:text-gc-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-reply>Reply<span class="sr-only"> to Dev Patel</span></button>
        </div>
        <div class="flex flex-wrap gap-0.5 rounded-[14px] border border-gc-line bg-white p-1 shadow-[0_10px_24px_-14px_rgba(80,20,50,.4)]" id="gc-pick-2" role="group" aria-label="Pick a reaction" hidden data-picker>
          <button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="👍" aria-label="Thumbs up">👍</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="❤️" aria-label="Heart">❤️</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="😂" aria-label="Laughing">😂</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="🎉" aria-label="Party popper">🎉</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="🚀" aria-label="Rocket">🚀</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="👀" aria-label="Eyes">👀</button>
        </div>
      </div>
    </div>
    <div class="flex max-w-[min(86%,560px)] flex-row-reverse items-start gap-2.5 self-end max-[520px]:max-w-[94%]" data-msg data-author="You">
      <div class="flex min-w-0 flex-col items-end gap-1.5">
        <div class="rounded-[18px_4px_18px_18px] bg-gc-accent px-[13px] pt-2 pb-[7px] whitespace-pre-wrap text-white [overflow-wrap:anywhere]"><p class="flex items-baseline gap-2 text-[.8rem] font-bold whitespace-normal"><span class="sr-only">You</span> <time class="text-[.72rem] font-normal text-[#fbd5e7]" datetime="2026-10-14T09:15">09:15</time></p><p data-body>Same here. I'll update the invite and the press list.</p></div>
        <div class="flex flex-wrap items-center justify-end gap-1">
          <div class="flex flex-wrap items-center justify-end gap-1" role="group" aria-label="Reactions" data-reacts>
            <button class="flex h-7 cursor-pointer items-center gap-[5px] rounded-full border border-gc-line bg-white px-[9px] text-[.8rem] font-semibold text-gc-muted transition-[background-color,border-color] duration-150 hover:border-[#d9a6bf] aria-pressed:border-gc-accent aria-pressed:bg-gc-soft aria-pressed:text-gc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" aria-pressed="false" data-react="🎉"><span class="text-[.95rem]" aria-hidden="true">🎉</span><span class="sr-only" data-label>Party popper</span> <span data-n>2</span></button>
          </div>
          <button class="grid h-7 min-w-7 cursor-pointer place-items-center rounded-full px-2 text-[.78rem] font-semibold text-gc-muted hover:bg-gc-line hover:text-gc-ink aria-expanded:bg-gc-line aria-expanded:text-gc-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" aria-expanded="false" aria-controls="gc-pick-3" data-add><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="13" r="8"/><path d="M8 15a4 4 0 0 0 6 0M8.5 11h.01M13.5 11h.01M19 2v6M16 5h6"/></svg><span class="sr-only">Add reaction</span></button>
        </div>
        <div class="flex flex-wrap gap-0.5 rounded-[14px] border border-gc-line bg-white p-1 shadow-[0_10px_24px_-14px_rgba(80,20,50,.4)]" id="gc-pick-3" role="group" aria-label="Pick a reaction" hidden data-picker>
          <button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="👍" aria-label="Thumbs up">👍</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="❤️" aria-label="Heart">❤️</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="😂" aria-label="Laughing">😂</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="🎉" aria-label="Party popper">🎉</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="🚀" aria-label="Rocket">🚀</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="👀" aria-label="Eyes">👀</button>
        </div>
      </div>
    </div>
    <div class="flex max-w-[min(86%,560px)] items-start gap-2.5 max-[520px]:max-w-[94%]" data-msg data-author="Kofi Mensah">
      <span class="grid size-[34px] flex-none place-items-center rounded-full text-[.72rem] font-bold max-[520px]:size-7 max-[520px]:text-[.62rem] bg-[#bbf7d0] text-[#14532d]" aria-hidden="true">KM</span>
      <div class="flex min-w-0 flex-col items-start gap-1.5">
        <div class="rounded-[4px_18px_18px_18px] border border-gc-line bg-white px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><p class="flex items-baseline gap-2 text-[.8rem] font-bold whitespace-normal text-[#166534]">Kofi Mensah <time class="text-[.72rem] font-normal text-gc-muted" datetime="2026-10-14T09:21">09:21</time></p><p data-body>App Store review is back. We're approved for a Thursday launch.</p></div>
        <div class="flex flex-wrap items-center gap-1">
          <div class="flex flex-wrap items-center gap-1" role="group" aria-label="Reactions" data-reacts>
            <button class="flex h-7 cursor-pointer items-center gap-[5px] rounded-full border border-gc-line bg-white px-[9px] text-[.8rem] font-semibold text-gc-muted transition-[background-color,border-color] duration-150 hover:border-[#d9a6bf] aria-pressed:border-gc-accent aria-pressed:bg-gc-soft aria-pressed:text-gc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" aria-pressed="true" data-react="🚀"><span class="text-[.95rem]" aria-hidden="true">🚀</span><span class="sr-only" data-label>Rocket</span> <span data-n>5</span></button>
            <button class="flex h-7 cursor-pointer items-center gap-[5px] rounded-full border border-gc-line bg-white px-[9px] text-[.8rem] font-semibold text-gc-muted transition-[background-color,border-color] duration-150 hover:border-[#d9a6bf] aria-pressed:border-gc-accent aria-pressed:bg-gc-soft aria-pressed:text-gc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" aria-pressed="false" data-react="❤️"><span class="text-[.95rem]" aria-hidden="true">❤️</span><span class="sr-only" data-label>Heart</span> <span data-n>2</span></button>
          </div>
          <button class="grid h-7 min-w-7 cursor-pointer place-items-center rounded-full px-2 text-[.78rem] font-semibold text-gc-muted hover:bg-gc-line hover:text-gc-ink aria-expanded:bg-gc-line aria-expanded:text-gc-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" aria-expanded="false" aria-controls="gc-pick-4" data-add><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="13" r="8"/><path d="M8 15a4 4 0 0 0 6 0M8.5 11h.01M13.5 11h.01M19 2v6M16 5h6"/></svg><span class="sr-only">Add reaction</span></button>
          <button class="grid h-7 min-w-7 cursor-pointer place-items-center rounded-full px-2 text-[.78rem] font-semibold text-gc-muted hover:bg-gc-line hover:text-gc-ink aria-expanded:bg-gc-line aria-expanded:text-gc-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-reply>Reply<span class="sr-only"> to Kofi Mensah</span></button>
        </div>
        <div class="flex flex-wrap gap-0.5 rounded-[14px] border border-gc-line bg-white p-1 shadow-[0_10px_24px_-14px_rgba(80,20,50,.4)]" id="gc-pick-4" role="group" aria-label="Pick a reaction" hidden data-picker>
          <button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="👍" aria-label="Thumbs up">👍</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="❤️" aria-label="Heart">❤️</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="😂" aria-label="Laughing">😂</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="🎉" aria-label="Party popper">🎉</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="🚀" aria-label="Rocket">🚀</button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[1.15rem] hover:bg-gc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-pick="👀" aria-label="Eyes">👀</button>
        </div>
      </div>
    </div>
  </div>
  <div class="border-t border-gc-line px-3.5 pt-2.5 pb-3.5">
    <div class="mb-2 flex items-center gap-2.5 rounded-[4px_12px_12px_4px] border-l-[3px] border-gc-accent bg-gc-soft py-2 pr-2 pl-3 text-[.84rem]" hidden data-replying>
      <p class="min-w-0 flex-1 truncate">Replying to <b data-reply-name></b>: <span data-reply-text></span></p>
      <button class="grid size-7 flex-none cursor-pointer place-items-center rounded-full text-gc-accent-dark hover:bg-[#f9d0e5] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" data-cancel-reply><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg><span class="sr-only">Cancel reply</span></button>
    </div>
    <form class="flex items-end gap-2" data-form>
      <label class="sr-only" for="gc-input">Message to #launch-week</label>
      <textarea class="box-border max-h-[130px] min-w-0 flex-1 resize-none rounded-[18px] border-[1.5px] border-[#9d8a94] bg-white px-3.5 py-2.5 leading-[1.4] text-gc-ink placeholder:text-[#6f5d66] focus-visible:border-gc-accent focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gc-accent" id="gc-input" rows="1" placeholder="Message #launch-week" data-input></textarea>
      <button class="grid size-11 flex-none cursor-pointer place-items-center rounded-full bg-gc-accent text-white hover:bg-gc-accent-dark disabled:cursor-not-allowed disabled:bg-[#eadde3] disabled:text-[#5f4d56] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="submit"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg><span class="sr-only">Send</span></button>
    </form>
  </div>
  <p class="sr-only" role="status" data-status></p>
  <template data-tpl="msg"><div class="flex max-w-[min(86%,560px)] flex-row-reverse items-start gap-2.5 self-end max-[520px]:max-w-[94%]" data-msg data-author="You"><div class="flex min-w-0 flex-col items-end gap-1.5"><div class="rounded-[18px_4px_18px_18px] bg-gc-accent px-[13px] pt-2 pb-[7px] whitespace-pre-wrap text-white [overflow-wrap:anywhere]"><p class="flex items-baseline gap-2 text-[.8rem] font-bold whitespace-normal"><span class="sr-only">You</span> <time class="text-[.72rem] font-normal text-[#fbd5e7]" data-time></time></p><blockquote class="mt-1 mb-1.5 rounded-[4px_10px_10px_4px] border-l-[3px] border-current bg-[#a3154f] px-2.5 py-1.5 text-[.84rem] whitespace-normal text-white" data-quote hidden><b class="block text-[.76rem]"><span class="sr-only">Replying to </span><span data-quote-name></span></b><span class="line-clamp-2 text-[#fbd5e7]" data-quote-text></span></blockquote><p data-body></p></div></div></div></template>
  <template data-tpl="react"><button class="flex h-7 cursor-pointer items-center gap-[5px] rounded-full border border-gc-line bg-white px-[9px] text-[.8rem] font-semibold text-gc-muted transition-[background-color,border-color] duration-150 hover:border-[#d9a6bf] aria-pressed:border-gc-accent aria-pressed:bg-gc-soft aria-pressed:text-gc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gc-accent" type="button" aria-pressed="true" data-react><span class="text-[.95rem]" aria-hidden="true" data-emoji></span><span class="sr-only" data-label></span> <span data-n>1</span></button></template>
</div></div>
CSS
/* Group chat palette as theme colours. */
@theme {
  --color-gc-accent:#be185d; --color-gc-accent-dark:#9d174d; --color-gc-soft:#fce7f3; --color-gc-ink:#26151d;
  --color-gc-muted:#6a5560; --color-gc-line:#eedde5; --color-gc-ground:#fbf7f9;
}
JavaScript
/* Group chat: reactions toggle with aria-pressed and update their count, the picker adds a reaction, Reply quotes a message
   in the composer, and Esc closes the picker or cancels the reply. Sending only appends to the page. */
document.querySelectorAll('[data-group]').forEach(function (chat) {
  var log = chat.querySelector('[data-log]'), form = chat.querySelector('[data-form]'), input = chat.querySelector('[data-input]');
  var send = form.querySelector('[type="submit"]'), status = chat.querySelector('[data-status]'), box = chat.querySelector('[data-replying]');
  var replyTo = null;
  var pad = function (n) { return String(n).padStart(2, '0'); };

  function closePicker(p, refocus) {
    p.hidden = true;
    var t = chat.querySelector('[aria-controls="' + p.id + '"]');
    t.setAttribute('aria-expanded', 'false');
    if (refocus) t.focus();
  }
  function toggleReaction(btn) {
    var on = btn.getAttribute('aria-pressed') !== 'true', n = btn.querySelector('[data-n]'), count = +n.textContent + (on ? 1 : -1);
    var label = btn.querySelector('[data-label]').textContent;
    if (!count) { status.textContent = label + ' removed'; return btn.remove(); }
    btn.setAttribute('aria-pressed', String(on)); n.textContent = count;
    status.textContent = label + (on ? ' added, ' : ' removed, ') + count + ' in total';
  }
  function setReply(msg) {
    replyTo = msg;
    box.hidden = !msg;
    if (msg) {
      box.querySelector('[data-reply-name]').textContent = msg.dataset.author;
      box.querySelector('[data-reply-text]').textContent = msg.querySelector('[data-body]').textContent;
      input.focus();
    }
  }

  chat.addEventListener('click', function (e) {
    var b = e.target.closest('button');
    if (!b) return;
    if (b.hasAttribute('data-react')) toggleReaction(b);
    if (b.hasAttribute('data-add')) {
      var p = chat.querySelector('#' + b.getAttribute('aria-controls')), open = p.hidden;
      chat.querySelectorAll('[data-picker]').forEach(function (x) { if (!x.hidden) closePicker(x); });
      if (open) { p.hidden = false; b.setAttribute('aria-expanded', 'true'); p.querySelector('button').focus(); }
    }
    if (b.hasAttribute('data-pick')) {
      var msg = b.closest('[data-msg]'), list = msg.querySelector('[data-reacts]'), emoji = b.dataset.pick;
      var chip = list.querySelector('[data-react="' + emoji + '"]');
      if (chip) { if (chip.getAttribute('aria-pressed') !== 'true') toggleReaction(chip); }
      else {
        chip = chat.querySelector('template[data-tpl="react"]').content.firstElementChild.cloneNode(true);
        chip.dataset.react = emoji;
        chip.querySelector('[data-emoji]').textContent = emoji;
        chip.querySelector('[data-label]').textContent = b.getAttribute('aria-label');
        list.appendChild(chip);
        status.textContent = b.getAttribute('aria-label') + ' added, 1 in total';
      }
      closePicker(b.closest('[data-picker]'));
      chip.focus();
    }
    if (b.hasAttribute('data-reply')) setReply(b.closest('[data-msg]'));
    if (b.hasAttribute('data-cancel-reply')) { setReply(null); input.focus(); }
  });
  chat.addEventListener('keydown', function (e) {
    if (e.key !== 'Escape') return;
    var p = e.target.closest('[data-picker]');
    if (p) { e.preventDefault(); closePicker(p, true); }
    else if (replyTo && e.target === input) { e.preventDefault(); setReply(null); }
  });

  function grow() { input.style.height = 'auto'; input.style.height = input.scrollHeight + 'px'; send.disabled = !input.value.trim(); }
  input.addEventListener('input', grow);
  input.addEventListener('keydown', function (e) {
    if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); form.requestSubmit(); }
  });
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var text = input.value.trim();
    if (!text) return;
    var d = new Date(), msg = chat.querySelector('template[data-tpl="msg"]').content.cloneNode(true), time = msg.querySelector('[data-time]');
    msg.querySelector('[data-body]').textContent = text;
    time.textContent = pad(d.getHours()) + ':' + pad(d.getMinutes());
    time.dateTime = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + time.textContent;
    if (replyTo) {
      var q = msg.querySelector('[data-quote]');
      q.hidden = false;
      q.querySelector('[data-quote-name]').textContent = replyTo.dataset.author;
      q.querySelector('[data-quote-text]').textContent = replyTo.querySelector('[data-body]').textContent;
    }
    log.appendChild(msg);
    log.scrollTop = log.scrollHeight;
    setReply(null); input.value = ''; grow(); input.focus();
  });
  log.scrollTop = log.scrollHeight;
  grow();
});

About this set

A group conversation for team and community chats. Each message from someone else has an initials avatar and a bubble with the sender’s name in their own colour, and your messages sit on the right in pink. Reactions are toggle buttons with an emoji and a count: pressing one adds you and raises the count, pressing it again takes you off, and a reaction that reaches zero disappears. The smiley button opens a row of six emoji that adds a new reaction or joins an existing one. Reply puts a quoted preview of the message above the composer, and the message you send carries that quote with the original sender’s name, like Dev Patel’s reply to Ines in the demo. Esc closes the picker or cancels the reply, and header avatars show who is in the channel. The script is about 90 lines of vanilla JavaScript and uses event delegation, so reactions added from the picker behave exactly like the original ones. In Bootstrap the reactions and tools are .btn elements and Cancel reply is a .btn-close.

What’s included

  • Reactions as aria-pressed toggle buttons with live counts
  • Emoji picker that adds a reaction or joins one
  • Reply with a quoted preview, carried into the sent message
  • Initials avatars and per-person name colours with text names
  • Esc closes the picker or cancels the reply

Accessibility

Each reaction is a button with aria-pressed, a hidden emoji name and its visible count, so it reads as Thumbs up 3, toggle button, not pressed. Count changes are confirmed in a visually hidden role="status" line. The picker toggle uses aria-expanded and aria-controls and gets focus back on Esc, quotes start with hidden Replying to text, and the thread is a focusable role="log" region with aria-live="polite". Reaction colour transitions are removed under prefers-reduced-motion.

Customise it

Change --gc-accent, --gc-soft and --gc-ground on .gc and the .n1 to .n4 name colours. In Tailwind edit the --color-gc-* theme colours; Bootstrap keeps the same variables and adds --bs-btn-* overrides on .gc-react and .gc-tool.