Open demo

CSS code

HTML
<div><span class="lbl">Conversation list, thread and composer (one pane at a time under 768px)</span>
<div class="mg" data-messenger data-view="list">
  <section class="mg-side" aria-labelledby="mg-inbox">
    <div class="mg-side-head"><h2 id="mg-inbox">Messages</h2><p class="mg-count" data-unread-total>3 unread</p></div>
    <ul class="mg-convos">
      <li><button class="mg-convo" type="button" data-open="maya" aria-current="true">
        <span class="mg-av" aria-hidden="true">MC<i class="mg-dot"></i></span>
        <span class="mg-convo-main">
          <span class="mg-convo-top"><b>Maya Chen<span class="sr-only">, online</span></b><time datetime="2026-10-14T09:12">09:12</time></span>
          <span class="mg-convo-bot"><span class="mg-prev" data-preview>You: See you at 16:00 at the loading bay.</span></span>
        </span></button></li>
      <li><button class="mg-convo" type="button" data-open="club">
        <span class="mg-av c2" aria-hidden="true">NC</span>
        <span class="mg-convo-main">
          <span class="mg-convo-top"><b>Northside Climbing Club</b><time datetime="2026-10-14T08:40">08:40</time></span>
          <span class="mg-convo-bot"><span class="mg-prev" data-preview>Jonas: Wall is booked for Saturday at 10:00</span><span class="mg-unread" data-unread="2">2<span class="sr-only"> unread</span></span></span>
        </span></button></li>
      <li><button class="mg-convo" type="button" data-open="dev">
        <span class="mg-av c3" aria-hidden="true">DP</span>
        <span class="mg-convo-main">
          <span class="mg-convo-top"><b>Dev Patel</b><time datetime="2026-10-13T18:05">Yesterday</time></span>
          <span class="mg-convo-bot"><span class="mg-prev" data-preview>Could you send the September invoice?</span><span class="mg-unread" data-unread="1">1<span class="sr-only"> unread</span></span></span>
        </span></button></li>
      <li><button class="mg-convo" type="button" data-open="rosa">
        <span class="mg-av c4" aria-hidden="true">RA<i class="mg-dot"></i></span>
        <span class="mg-convo-main">
          <span class="mg-convo-top"><b>Rosa Almeida<span class="sr-only">, online</span></b><time datetime="2026-10-11T12:30">Sat</time></span>
          <span class="mg-convo-bot"><span class="mg-prev" data-preview>You: Count me in for Sunday lunch.</span></span>
        </span></button></li>
    </ul>
  </section>

  <section class="mg-pane" aria-label="Conversation">
    <div class="mg-thread" data-thread="maya">
      <div class="mg-head">
        <button class="mg-back" type="button" data-back><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="sr-only">Back to conversations</span></button>
        <span class="mg-av" aria-hidden="true">MC</span>
        <div><h3 id="mg-t-maya" tabindex="-1">Maya Chen</h3><p class="mg-status"><i aria-hidden="true"></i>Online</p></div>
      </div>
      <div class="mg-log" role="log" aria-live="polite" aria-labelledby="mg-t-maya" tabindex="0">
        <p class="mg-day"><time datetime="2026-10-13">Yesterday</time></p>
        <div class="mg-msg"><p class="mg-who">Maya Chen</p><p class="mg-b"><span>Venue confirmed the rooftop for Thursday.</span> <time datetime="2026-10-13T17:42">17:42</time></p><p class="mg-b"><span>We only have it until 19:30, so golden hour is tight.</span> <time datetime="2026-10-13T17:43">17:43</time></p></div>
        <div class="mg-msg out"><p class="mg-who sr-only">You</p><p class="mg-b"><span>Then portraits first, product shots inside after.</span> <time datetime="2026-10-13T17:51">17:51</time></p></div>
        <p class="mg-day"><time datetime="2026-10-14">Today</time></p>
        <div class="mg-msg"><p class="mg-who">Maya Chen</p><p class="mg-b"><span>Morning. Can you bring the second reflector?</span> <time datetime="2026-10-14T08:58">08:58</time></p></div>
        <div class="mg-msg out"><p class="mg-who sr-only">You</p><p class="mg-b"><span>Already packed, along with the spare batteries.</span> <time datetime="2026-10-14T09:12">09:12</time></p><p class="mg-b"><span>See you at 16:00 at the loading bay.</span> <time datetime="2026-10-14T09:12">09:12</time></p><p class="mg-rcpt">Read</p></div>
      </div>
    </div>
    <div class="mg-thread" data-thread="club" hidden>
      <div class="mg-head">
        <button class="mg-back" type="button" data-back><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="sr-only">Back to conversations</span></button>
        <span class="mg-av c2" aria-hidden="true">NC</span>
        <div><h3 id="mg-t-club" tabindex="-1">Northside Climbing Club</h3><p class="mg-status">14 members</p></div>
      </div>
      <div class="mg-log" role="log" aria-live="polite" aria-labelledby="mg-t-club" tabindex="0">
        <p class="mg-day"><time datetime="2026-10-14">Today</time></p>
        <div class="mg-msg"><p class="mg-who">Jonas Weber</p><p class="mg-b"><span>Wall is booked for Saturday at 10:00.</span> <time datetime="2026-10-14T08:39">08:39</time></p><p class="mg-b"><span>Bring your own chalk, the shop is closed for stocktake.</span> <time datetime="2026-10-14T08:40">08:40</time></p></div>
      </div>
    </div>
    <div class="mg-thread" data-thread="dev" hidden>
      <div class="mg-head">
        <button class="mg-back" type="button" data-back><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="sr-only">Back to conversations</span></button>
        <span class="mg-av c3" aria-hidden="true">DP</span>
        <div><h3 id="mg-t-dev" tabindex="-1">Dev Patel</h3><p class="mg-status">Last seen yesterday at 18:20</p></div>
      </div>
      <div class="mg-log" role="log" aria-live="polite" aria-labelledby="mg-t-dev" tabindex="0">
        <p class="mg-day"><time datetime="2026-10-13">Yesterday</time></p>
        <div class="mg-msg"><p class="mg-who">Dev Patel</p><p class="mg-b"><span>Thanks for the retouched set, the client loved it.</span> <time datetime="2026-10-13T18:04">18:04</time></p><p class="mg-b"><span>Could you send the September invoice?</span> <time datetime="2026-10-13T18:05">18:05</time></p></div>
      </div>
    </div>
    <div class="mg-thread" data-thread="rosa" hidden>
      <div class="mg-head">
        <button class="mg-back" type="button" data-back><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="sr-only">Back to conversations</span></button>
        <span class="mg-av c4" aria-hidden="true">RA</span>
        <div><h3 id="mg-t-rosa" tabindex="-1">Rosa Almeida</h3><p class="mg-status"><i aria-hidden="true"></i>Online</p></div>
      </div>
      <div class="mg-log" role="log" aria-live="polite" aria-labelledby="mg-t-rosa" tabindex="0">
        <p class="mg-day"><time datetime="2026-10-11">Saturday</time></p>
        <div class="mg-msg"><p class="mg-who">Rosa Almeida</p><p class="mg-b"><span>Lunch on Sunday? I'm making bacalhau.</span> <time datetime="2026-10-11T12:21">12:21</time></p></div>
        <div class="mg-msg out"><p class="mg-who sr-only">You</p><p class="mg-b"><span>Count me in for Sunday lunch.</span> <time datetime="2026-10-11T12:30">12:30</time></p><p class="mg-rcpt">Read</p></div>
      </div>
    </div>

    <form class="mg-composer" data-composer>
      <label class="sr-only" for="mg-input">Message</label>
      <textarea id="mg-input" rows="1" placeholder="Write a message" data-input></textarea>
      <button class="mg-send" type="submit"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12 20 4l-6 16-3-7z"/><path d="M11 13 20 4"/></svg><span class="sr-only">Send</span></button>
    </form>
    <template data-tpl><div class="mg-msg out"><p class="mg-who sr-only">You</p><p class="mg-b"><span data-text></span> <time data-time></time></p><p class="mg-rcpt">Sent</p></div></template>
  </section>
</div></div>
CSS
/* Two-pane messenger: conversation list + thread + composer. Under 768px it shows one pane at a time (data-view). */
.mg{--mg-accent:#0f766e;--mg-accent-dark:#0b5d56;--mg-on-accent:#fff;--mg-soft:#d5f5ee;--mg-in:#eef3f2;--mg-ink:#14201f;--mg-muted:#55625f;
  --mg-line:#dde5e3;--mg-side:#f7faf9;--mg-bg:#fff;--mg-online:#15803d;
  display:grid;grid-template-columns:320px minmax(0,1fr);height:620px;overflow:hidden;background:var(--mg-bg);
  border:1px solid var(--mg-line);border-radius:20px;color:var(--mg-ink);font-size:.95rem;line-height:1.45;
  box-shadow:0 24px 48px -28px rgba(20,32,31,.35)}
.mg :where(h2,h3,p,ul){margin:0;padding:0}
.mg ul{list-style:none}
.mg .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}
.mg button{font:inherit;color:inherit;cursor:pointer}
.mg :focus-visible{outline:2px solid var(--mg-accent);outline-offset:2px}

/* conversation list */
.mg-side{display:flex;flex-direction:column;min-height:0;background:var(--mg-side);border-right:1px solid var(--mg-line)}
.mg-side-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:22px 20px 14px}
.mg-side-head h2{font-size:1.25rem;font-weight:800;letter-spacing:-.02em}
.mg-count{font-size:.8rem;font-weight:600;color:var(--mg-accent-dark)}
.mg-convos{flex:1;overflow-y:auto;padding:0 10px 12px}
.mg-convo{display:flex;align-items:center;gap:12px;width:100%;padding:11px 10px;border:0;border-radius:14px;background:none;text-align:left;transition:background-color .15s}
.mg-convo:hover{background:#edf3f1}
.mg-convo[aria-current="true"]{background:var(--mg-soft)}
.mg-convo:focus-visible{outline-offset:-2px}
.mg-av{position:relative;flex:none;display:grid;place-items:center;width:44px;height:44px;border-radius:50%;font-size:.8rem;font-weight:700;background:#cfe3ff;color:#1d3f8f}
.mg-av.c2{background:#fde2cf;color:#8a3b0a}
.mg-av.c3{background:#e6dcff;color:#4c2a97}
.mg-av.c4{background:#d5f5ee;color:#0b5d56}
.mg-av .mg-dot{position:absolute;right:0;bottom:1px;width:12px;height:12px;border:2px solid var(--mg-side);border-radius:50%;background:var(--mg-online)}
.mg-convo-main{display:flex;flex-direction:column;flex:1;min-width:0}
.mg-convo-top{display:flex;align-items:baseline;justify-content:space-between;gap:8px}
.mg-convo-top b{font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mg-convo-top time{flex:none;font-size:.74rem;color:var(--mg-muted)}
.mg-convo-bot{display:flex;align-items:center;gap:8px;margin-top:2px}
.mg-prev{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.86rem;color:var(--mg-muted)}
.mg-unread{flex:none;min-width:22px;padding:2px 7px;border-radius:999px;background:var(--mg-accent);color:var(--mg-on-accent);font-size:.72rem;font-weight:700;text-align:center}
.mg-convo:has(.mg-unread) .mg-prev{color:var(--mg-ink);font-weight:600}

/* thread pane */
.mg-pane{display:flex;flex-direction:column;min-width:0;min-height:0}
.mg-thread{display:flex;flex-direction:column;flex:1;min-height:0}
.mg-thread[hidden]{display:none}
.mg-head{display:flex;align-items:center;gap:12px;padding:14px 20px;border-bottom:1px solid var(--mg-line)}
.mg-head .mg-av{width:40px;height:40px}
.mg-head h3{font-size:1rem;font-weight:700}
.mg-head h3:focus{outline:none}
.mg-head h3:focus-visible{outline:2px solid var(--mg-accent)}
.mg-status{display:flex;align-items:center;gap:6px;font-size:.8rem;color:var(--mg-muted)}
.mg-status i{width:8px;height:8px;border-radius:50%;background:var(--mg-online)}
.mg-back{display:none;place-items:center;width:38px;height:38px;margin-left:-8px;border:0;border-radius:50%;background:none}
.mg-back:hover{background:var(--mg-in)}

/* message log */
.mg-log{flex:1;display:flex;flex-direction:column;gap:12px;overflow-y:auto;padding:20px;scroll-behavior:smooth}
.mg-log:focus-visible{outline-offset:-3px}
.mg-day{align-self:center;padding:4px 12px;border-radius:999px;background:var(--mg-in);font-size:.74rem;font-weight:600;color:var(--mg-muted)}
.mg-msg{display:flex;flex-direction:column;align-items:flex-start;gap:3px;max-width:min(78%,480px)}
.mg-msg.out{align-self:flex-end;align-items:flex-end}
.mg-who{margin-left:12px;font-size:.76rem;font-weight:600;color:var(--mg-muted)}
.mg-b{display:flex;flex-wrap:wrap;align-items:flex-end;column-gap:10px;padding:8px 13px 7px;border-radius:6px 18px 18px 6px;background:var(--mg-in);overflow-wrap:anywhere;white-space:pre-wrap}
.mg-who+.mg-b{border-top-left-radius:18px}
.mg-msg.out .mg-b{border-radius:18px 6px 6px 18px;background:var(--mg-accent);color:var(--mg-on-accent)}
.mg-msg.out .mg-who+.mg-b{border-top-right-radius:18px}
.mg-b time{margin-left:auto;font-size:.7rem;color:var(--mg-muted);white-space:nowrap}
.mg-msg.out .mg-b time{color:#d5f5ee}
.mg-rcpt{font-size:.72rem;color:var(--mg-muted)}

/* composer */
.mg-composer{display:flex;align-items:flex-end;gap:10px;padding:12px 16px 16px;border-top:1px solid var(--mg-line)}
.mg-composer textarea{box-sizing:border-box;flex:1;min-width:0;max-height:140px;padding:11px 16px;border:1.5px solid #8a9895;border-radius:22px;background:var(--mg-bg);color:var(--mg-ink);font:inherit;line-height:1.4;resize:none}
.mg-composer textarea::placeholder{color:#5f6b68}
.mg-composer textarea:focus-visible{outline:2px solid var(--mg-accent);outline-offset:1px;border-color:var(--mg-accent)}
.mg-send{flex:none;display:grid;place-items:center;width:46px;height:46px;border:0;border-radius:50%;background:var(--mg-accent);color:var(--mg-on-accent);transition:background-color .15s}
.mg-send:hover{background:var(--mg-accent-dark)}
.mg-send:disabled{background:#c8d3d0;color:#4d5a57;cursor:not-allowed}

/* one pane at a time on small screens */
@media (max-width:767.98px){
  .mg{grid-template-columns:minmax(0,1fr);height:600px}
  .mg[data-view="list"] .mg-pane,.mg[data-view="thread"] .mg-side{display:none}
  .mg-side{border-right:0}
  .mg-back{display:grid}
  .mg-head{padding:12px 14px}
  .mg-log{padding:16px 12px}
  .mg-msg{max-width:88%}
  .mg-composer{padding:10px 12px 12px}
}
@media (prefers-reduced-motion:reduce){.mg *{transition:none;scroll-behavior:auto}}
JavaScript
/* Messenger: open a conversation, go back on small screens, grow the textarea, send with Enter (Shift+Enter = new line).
   Sending only appends the message to the page. */
document.querySelectorAll('[data-messenger]').forEach(function (app) {
  var convos = app.querySelectorAll('[data-open]');
  var form = app.querySelector('[data-composer]');
  var input = app.querySelector('[data-input]');
  var send = form.querySelector('[type="submit"]');
  var tpl = app.querySelector('template[data-tpl]');
  var narrow = window.matchMedia('(max-width: 767.98px)');
  var pad = function (n) { return String(n).padStart(2, '0'); };
  var thread = function () { return app.querySelector('[data-thread]:not([hidden])'); };

  function updateTotal() {
    var n = 0;
    app.querySelectorAll('[data-unread]').forEach(function (b) { n += +b.dataset.unread; });
    app.querySelector('[data-unread-total]').textContent = n ? n + ' unread' : 'All read';
  }
  convos.forEach(function (btn) {
    btn.addEventListener('click', function () {
      convos.forEach(function (b) { b.removeAttribute('aria-current'); });
      btn.setAttribute('aria-current', 'true');
      app.querySelectorAll('[data-thread]').forEach(function (t) { t.hidden = t.dataset.thread !== btn.dataset.open; });
      var badge = btn.querySelector('[data-unread]');
      if (badge) { badge.remove(); updateTotal(); }
      var log = thread().querySelector('[role="log"]');
      log.scrollTop = log.scrollHeight;
      app.dataset.view = 'thread';
      if (narrow.matches) thread().querySelector('h3').focus();
    });
  });
  app.querySelectorAll('[data-back]').forEach(function (b) {
    b.addEventListener('click', function () {
      app.dataset.view = 'list';
      app.querySelector('[aria-current="true"]').focus();
    });
  });

  function grow() { input.style.height = 'auto'; input.style.height = input.scrollHeight + 2 + '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 = tpl.content.cloneNode(true), time = msg.querySelector('[data-time]');
    msg.querySelector('[data-text]').textContent = text;
    time.textContent = pad(d.getHours()) + ':' + pad(d.getMinutes());
    time.dateTime = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + time.textContent;
    var log = thread().querySelector('[role="log"]');
    log.appendChild(msg);
    log.scrollTop = log.scrollHeight;
    var prev = app.querySelector('[aria-current="true"] [data-preview]');
    if (prev) prev.textContent = 'You: ' + text;
    input.value = ''; grow(); input.focus();
  });
  grow();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Conversation list, thread and composer (one pane at a time under 768px)</span>
<div class="mg" data-messenger data-view="list">
  <section class="mg-side d-flex flex-column" aria-labelledby="mg-inbox">
    <div class="d-flex align-items-baseline justify-content-between gap-3 px-4 pt-4 pb-3"><h2 class="fw-bolder" id="mg-inbox">Messages</h2><p class="mg-count fw-semibold" data-unread-total>3 unread</p></div>
    <div class="list-group mg-convos flex-grow-1 px-2 pb-3">
      <button class="list-group-item list-group-item-action d-flex align-items-center gap-3" type="button" data-open="maya" aria-current="true">
        <span class="mg-av" aria-hidden="true">MC<i class="mg-dot"></i></span>
        <span class="d-flex flex-column flex-grow-1" style="min-width:0">
          <span class="d-flex align-items-baseline justify-content-between gap-2"><b class="text-truncate">Maya Chen<span class="visually-hidden">, online</span></b><time class="mg-time flex-shrink-0" datetime="2026-10-14T09:12">09:12</time></span>
          <span class="d-flex align-items-center gap-2" style="margin-top:2px"><span class="mg-prev text-truncate flex-grow-1" data-preview>You: See you at 16:00 at the loading bay.</span></span>
        </span></button>
      <button class="list-group-item list-group-item-action d-flex align-items-center gap-3" type="button" data-open="club">
        <span class="mg-av c2" aria-hidden="true">NC</span>
        <span class="d-flex flex-column flex-grow-1" style="min-width:0">
          <span class="d-flex align-items-baseline justify-content-between gap-2"><b class="text-truncate">Northside Climbing Club</b><time class="mg-time flex-shrink-0" datetime="2026-10-14T08:40">08:40</time></span>
          <span class="d-flex align-items-center gap-2" style="margin-top:2px"><span class="mg-prev text-truncate flex-grow-1" data-preview>Jonas: Wall is booked for Saturday at 10:00</span><span class="badge rounded-pill mg-unread" data-unread="2">2<span class="visually-hidden"> unread</span></span></span>
        </span></button>
      <button class="list-group-item list-group-item-action d-flex align-items-center gap-3" type="button" data-open="dev">
        <span class="mg-av c3" aria-hidden="true">DP</span>
        <span class="d-flex flex-column flex-grow-1" style="min-width:0">
          <span class="d-flex align-items-baseline justify-content-between gap-2"><b class="text-truncate">Dev Patel</b><time class="mg-time flex-shrink-0" datetime="2026-10-13T18:05">Yesterday</time></span>
          <span class="d-flex align-items-center gap-2" style="margin-top:2px"><span class="mg-prev text-truncate flex-grow-1" data-preview>Could you send the September invoice?</span><span class="badge rounded-pill mg-unread" data-unread="1">1<span class="visually-hidden"> unread</span></span></span>
        </span></button>
      <button class="list-group-item list-group-item-action d-flex align-items-center gap-3" type="button" data-open="rosa">
        <span class="mg-av c4" aria-hidden="true">RA<i class="mg-dot"></i></span>
        <span class="d-flex flex-column flex-grow-1" style="min-width:0">
          <span class="d-flex align-items-baseline justify-content-between gap-2"><b class="text-truncate">Rosa Almeida<span class="visually-hidden">, online</span></b><time class="mg-time flex-shrink-0" datetime="2026-10-11T12:30">Sat</time></span>
          <span class="d-flex align-items-center gap-2" style="margin-top:2px"><span class="mg-prev text-truncate flex-grow-1" data-preview>You: Count me in for Sunday lunch.</span></span>
        </span></button>
    </div>
  </section>

  <section class="mg-pane d-flex flex-column" aria-label="Conversation">
    <div class="mg-thread d-flex flex-column flex-grow-1" style="min-height:0" data-thread="maya">
      <div class="mg-head d-flex align-items-center gap-3 px-4 py-3">
        <button class="btn mg-back" type="button" data-back><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="visually-hidden">Back to conversations</span></button>
        <span class="mg-av" aria-hidden="true">MC</span>
        <div><h3 class="fw-bold" id="mg-t-maya" tabindex="-1">Maya Chen</h3><p class="mg-status d-flex align-items-center gap-2"><i aria-hidden="true"></i>Online</p></div>
      </div>
      <div class="mg-log flex-grow-1" role="log" aria-live="polite" aria-labelledby="mg-t-maya" tabindex="0">
        <p class="badge rounded-pill mg-day align-self-center"><time datetime="2026-10-13">Yesterday</time></p>
        <div class="mg-msg"><p class="mg-who">Maya Chen</p><p class="mg-b"><span>Venue confirmed the rooftop for Thursday.</span> <time datetime="2026-10-13T17:42">17:42</time></p><p class="mg-b"><span>We only have it until 19:30, so golden hour is tight.</span> <time datetime="2026-10-13T17:43">17:43</time></p></div>
        <div class="mg-msg out"><p class="mg-who visually-hidden">You</p><p class="mg-b"><span>Then portraits first, product shots inside after.</span> <time datetime="2026-10-13T17:51">17:51</time></p></div>
        <p class="badge rounded-pill mg-day align-self-center"><time datetime="2026-10-14">Today</time></p>
        <div class="mg-msg"><p class="mg-who">Maya Chen</p><p class="mg-b"><span>Morning. Can you bring the second reflector?</span> <time datetime="2026-10-14T08:58">08:58</time></p></div>
        <div class="mg-msg out"><p class="mg-who visually-hidden">You</p><p class="mg-b"><span>Already packed, along with the spare batteries.</span> <time datetime="2026-10-14T09:12">09:12</time></p><p class="mg-b"><span>See you at 16:00 at the loading bay.</span> <time datetime="2026-10-14T09:12">09:12</time></p><p class="mg-rcpt">Read</p></div>
      </div>
    </div>
    <div class="mg-thread d-flex flex-column flex-grow-1" style="min-height:0" data-thread="club" hidden>
      <div class="mg-head d-flex align-items-center gap-3 px-4 py-3">
        <button class="btn mg-back" type="button" data-back><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="visually-hidden">Back to conversations</span></button>
        <span class="mg-av c2" aria-hidden="true">NC</span>
        <div><h3 class="fw-bold" id="mg-t-club" tabindex="-1">Northside Climbing Club</h3><p class="mg-status">14 members</p></div>
      </div>
      <div class="mg-log flex-grow-1" role="log" aria-live="polite" aria-labelledby="mg-t-club" tabindex="0">
        <p class="badge rounded-pill mg-day align-self-center"><time datetime="2026-10-14">Today</time></p>
        <div class="mg-msg"><p class="mg-who">Jonas Weber</p><p class="mg-b"><span>Wall is booked for Saturday at 10:00.</span> <time datetime="2026-10-14T08:39">08:39</time></p><p class="mg-b"><span>Bring your own chalk, the shop is closed for stocktake.</span> <time datetime="2026-10-14T08:40">08:40</time></p></div>
      </div>
    </div>
    <div class="mg-thread d-flex flex-column flex-grow-1" style="min-height:0" data-thread="dev" hidden>
      <div class="mg-head d-flex align-items-center gap-3 px-4 py-3">
        <button class="btn mg-back" type="button" data-back><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="visually-hidden">Back to conversations</span></button>
        <span class="mg-av c3" aria-hidden="true">DP</span>
        <div><h3 class="fw-bold" id="mg-t-dev" tabindex="-1">Dev Patel</h3><p class="mg-status">Last seen yesterday at 18:20</p></div>
      </div>
      <div class="mg-log flex-grow-1" role="log" aria-live="polite" aria-labelledby="mg-t-dev" tabindex="0">
        <p class="badge rounded-pill mg-day align-self-center"><time datetime="2026-10-13">Yesterday</time></p>
        <div class="mg-msg"><p class="mg-who">Dev Patel</p><p class="mg-b"><span>Thanks for the retouched set, the client loved it.</span> <time datetime="2026-10-13T18:04">18:04</time></p><p class="mg-b"><span>Could you send the September invoice?</span> <time datetime="2026-10-13T18:05">18:05</time></p></div>
      </div>
    </div>
    <div class="mg-thread d-flex flex-column flex-grow-1" style="min-height:0" data-thread="rosa" hidden>
      <div class="mg-head d-flex align-items-center gap-3 px-4 py-3">
        <button class="btn mg-back" type="button" data-back><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="visually-hidden">Back to conversations</span></button>
        <span class="mg-av c4" aria-hidden="true">RA</span>
        <div><h3 class="fw-bold" id="mg-t-rosa" tabindex="-1">Rosa Almeida</h3><p class="mg-status d-flex align-items-center gap-2"><i aria-hidden="true"></i>Online</p></div>
      </div>
      <div class="mg-log flex-grow-1" role="log" aria-live="polite" aria-labelledby="mg-t-rosa" tabindex="0">
        <p class="badge rounded-pill mg-day align-self-center"><time datetime="2026-10-11">Saturday</time></p>
        <div class="mg-msg"><p class="mg-who">Rosa Almeida</p><p class="mg-b"><span>Lunch on Sunday? I'm making bacalhau.</span> <time datetime="2026-10-11T12:21">12:21</time></p></div>
        <div class="mg-msg out"><p class="mg-who visually-hidden">You</p><p class="mg-b"><span>Count me in for Sunday lunch.</span> <time datetime="2026-10-11T12:30">12:30</time></p><p class="mg-rcpt">Read</p></div>
      </div>
    </div>

    <form class="mg-composer d-flex align-items-end gap-2 px-3 pt-2 pb-3" data-composer>
      <label class="visually-hidden" for="mg-input">Message</label>
      <textarea class="form-control mg-input" id="mg-input" rows="1" placeholder="Write a message" data-input></textarea>
      <button class="btn mg-send" type="submit"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12 20 4l-6 16-3-7z"/><path d="M11 13 20 4"/></svg><span class="visually-hidden">Send</span></button>
    </form>
    <template data-tpl><div class="mg-msg out"><p class="mg-who visually-hidden">You</p><p class="mg-b"><span data-text></span> <time data-time></time></p><p class="mg-rcpt">Sent</p></div></template>
  </section>
</div></div>
CSS
/* Messenger on Bootstrap: .list-group for conversations, .badge for unread counts, .form-control and .btn in the composer.
   Under 768px it shows one pane at a time (data-view). */
.mg{--mg-accent:#0f766e;--mg-accent-dark:#0b5d56;--mg-soft:#d5f5ee;--mg-in:#eef3f2;--mg-ink:#14201f;--mg-muted:#55625f;--mg-line:#dde5e3;--mg-side:#f7faf9;--mg-online:#15803d;
  display:grid;grid-template-columns:320px minmax(0,1fr);height:620px;overflow:hidden;background:#fff;border:1px solid var(--mg-line);border-radius:20px;
  color:var(--mg-ink);font-size:.95rem;line-height:1.45;box-shadow:0 24px 48px -28px rgba(20,32,31,.35)}
.mg :where(h2,h3,p){margin:0}
.mg :focus-visible{outline:2px solid var(--mg-accent);outline-offset:2px;box-shadow:none}
.mg-side{background:var(--mg-side);border-right:1px solid var(--mg-line);min-height:0}
.mg-side h2{font-size:1.25rem;letter-spacing:-.02em}
.mg-count{font-size:.8rem;color:var(--mg-accent-dark)}
.mg-convos{--bs-list-group-bg:transparent;--bs-list-group-border-width:0;--bs-list-group-item-padding-x:10px;--bs-list-group-item-padding-y:11px;--bs-list-group-color:var(--mg-ink);
  --bs-list-group-action-color:var(--mg-ink);--bs-list-group-action-hover-color:var(--mg-ink);--bs-list-group-action-hover-bg:#edf3f1;--bs-list-group-action-active-bg:var(--mg-soft);--bs-list-group-action-active-color:var(--mg-ink);overflow-y:auto}
.mg-convos .list-group-item{border-radius:14px;transition:background-color .15s}
.mg-convos .list-group-item:focus-visible{outline-offset:-2px;z-index:2}
.mg-convos .list-group-item[aria-current="true"]{background:var(--mg-soft)}
.mg-av{position:relative;display:grid;place-items:center;flex:none;width:44px;height:44px;border-radius:50%;font-size:.8rem;font-weight:700;background:#cfe3ff;color:#1d3f8f}
.mg-av.c2{background:#fde2cf;color:#8a3b0a}
.mg-av.c3{background:#e6dcff;color:#4c2a97}
.mg-av.c4{background:#d5f5ee;color:#0b5d56}
.mg-dot{position:absolute;right:0;bottom:1px;width:12px;height:12px;border:2px solid var(--mg-side);border-radius:50%;background:var(--mg-online)}
.mg-time{font-size:.74rem;color:var(--mg-muted)}
.mg-prev{font-size:.86rem;color:var(--mg-muted)}
.list-group-item:has(.badge) .mg-prev{color:var(--mg-ink);font-weight:600}
.mg-unread{--bs-badge-font-size:.72rem;--bs-badge-padding-x:7px;--bs-badge-padding-y:3px;min-width:22px;background:var(--mg-accent)}
.mg-pane{min-width:0;min-height:0}
.mg-thread[hidden]{display:none!important}
.mg-head{border-bottom:1px solid var(--mg-line)}
.mg-head .mg-av{width:40px;height:40px}
.mg-head h3{font-size:1rem}
.mg-head h3:focus{outline:none}
.mg-head h3:focus-visible{outline:2px solid var(--mg-accent)}
.mg-status{font-size:.8rem;color:var(--mg-muted)}
.mg-status i{width:8px;height:8px;border-radius:50%;background:var(--mg-online)}
.mg-back{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-hover-bg:var(--mg-in);--bs-btn-active-bg:var(--mg-in);--bs-btn-color:var(--mg-ink);--bs-btn-hover-color:var(--mg-ink);display:none;place-items:center;width:38px;height:38px;margin-left:-8px;border-radius:50%}
.mg-log{display:flex;flex-direction:column;gap:12px;overflow-y:auto;padding:20px;scroll-behavior:smooth}
.mg-log:focus-visible{outline-offset:-3px}
.mg-day{--bs-badge-font-size:.74rem;--bs-badge-font-weight:600;--bs-badge-color:var(--mg-muted);--bs-badge-padding-x:12px;--bs-badge-padding-y:5px;background:var(--mg-in)}
.mg-msg{display:flex;flex-direction:column;gap:3px;max-width:min(78%,480px)}
.mg-msg.out{align-self:flex-end;align-items:flex-end}
.mg-msg:not(.out){align-items:flex-start}
.mg-who{margin-left:12px;font-size:.76rem;font-weight:600;color:var(--mg-muted)}
.mg-b{display:flex;flex-wrap:wrap;align-items:flex-end;column-gap:10px;padding:8px 13px 7px;border-radius:6px 18px 18px 6px;background:var(--mg-in);overflow-wrap:anywhere;white-space:pre-wrap}
.mg-who+.mg-b{border-top-left-radius:18px}
.mg-msg.out .mg-b{border-radius:18px 6px 6px 18px;background:var(--mg-accent);color:#fff}
.mg-msg.out .mg-who+.mg-b{border-top-right-radius:18px}
.mg-b time{margin-left:auto;font-size:.7rem;color:var(--mg-muted);white-space:nowrap}
.mg-msg.out .mg-b time{color:#d5f5ee}
.mg-rcpt{font-size:.72rem;color:var(--mg-muted)}
.mg-composer{border-top:1px solid var(--mg-line)}
.mg-input{--bs-border-color:#8a9895;max-height:140px;padding:11px 16px;border-width:1.5px;border-radius:22px;color:var(--mg-ink);font-size:inherit;line-height:1.4;resize:none}
.mg-input::placeholder{color:#5f6b68}
.mg-input:focus{border-color:var(--mg-accent);box-shadow:none}
.mg-input:focus-visible{outline:2px solid var(--mg-accent);outline-offset:1px}
.mg-send{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-color:#fff;--bs-btn-bg:var(--mg-accent);--bs-btn-border-color:var(--mg-accent);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--mg-accent-dark);--bs-btn-hover-border-color:var(--mg-accent-dark);
  --bs-btn-active-bg:var(--mg-accent-dark);--bs-btn-active-color:#fff;--bs-btn-disabled-color:#4d5a57;--bs-btn-disabled-bg:#c8d3d0;--bs-btn-disabled-border-color:#c8d3d0;--bs-btn-disabled-opacity:1;
  display:grid;place-items:center;flex:none;width:46px;height:46px;border-radius:50%}
@media (max-width:767.98px){
  .mg{grid-template-columns:minmax(0,1fr);height:600px}
  .mg[data-view="list"] .mg-pane,.mg[data-view="thread"] .mg-side{display:none!important}
  .mg-side{border-right:0}
  .mg-back{display:grid}
  .mg-head{padding:12px 14px!important}
  .mg-log{padding:16px 12px}
  .mg-msg{max-width:88%}
}
@media (prefers-reduced-motion:reduce){.mg *{transition:none!important;scroll-behavior:auto}}
JavaScript
/* Messenger: open a conversation, go back on small screens, grow the textarea, send with Enter (Shift+Enter = new line).
   Sending only appends the message to the page. */
document.querySelectorAll('[data-messenger]').forEach(function (app) {
  var convos = app.querySelectorAll('[data-open]');
  var form = app.querySelector('[data-composer]');
  var input = app.querySelector('[data-input]');
  var send = form.querySelector('[type="submit"]');
  var tpl = app.querySelector('template[data-tpl]');
  var narrow = window.matchMedia('(max-width: 767.98px)');
  var pad = function (n) { return String(n).padStart(2, '0'); };
  var thread = function () { return app.querySelector('[data-thread]:not([hidden])'); };

  function updateTotal() {
    var n = 0;
    app.querySelectorAll('[data-unread]').forEach(function (b) { n += +b.dataset.unread; });
    app.querySelector('[data-unread-total]').textContent = n ? n + ' unread' : 'All read';
  }
  convos.forEach(function (btn) {
    btn.addEventListener('click', function () {
      convos.forEach(function (b) { b.removeAttribute('aria-current'); });
      btn.setAttribute('aria-current', 'true');
      app.querySelectorAll('[data-thread]').forEach(function (t) { t.hidden = t.dataset.thread !== btn.dataset.open; });
      var badge = btn.querySelector('[data-unread]');
      if (badge) { badge.remove(); updateTotal(); }
      var log = thread().querySelector('[role="log"]');
      log.scrollTop = log.scrollHeight;
      app.dataset.view = 'thread';
      if (narrow.matches) thread().querySelector('h3').focus();
    });
  });
  app.querySelectorAll('[data-back]').forEach(function (b) {
    b.addEventListener('click', function () {
      app.dataset.view = 'list';
      app.querySelector('[aria-current="true"]').focus();
    });
  });

  function grow() { input.style.height = 'auto'; input.style.height = input.scrollHeight + 2 + '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 = tpl.content.cloneNode(true), time = msg.querySelector('[data-time]');
    msg.querySelector('[data-text]').textContent = text;
    time.textContent = pad(d.getHours()) + ':' + pad(d.getMinutes());
    time.dateTime = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + time.textContent;
    var log = thread().querySelector('[role="log"]');
    log.appendChild(msg);
    log.scrollTop = log.scrollHeight;
    var prev = app.querySelector('[aria-current="true"] [data-preview]');
    if (prev) prev.textContent = 'You: ' + text;
    input.value = ''; grow(); input.focus();
  });
  grow();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4d5a57] uppercase">Conversation list, thread and composer (one pane at a time under 768px)</span>
<div class="group grid h-[620px] grid-cols-[320px_minmax(0,1fr)] overflow-hidden rounded-[20px] border border-mg-line bg-white text-[.95rem] leading-[1.45] text-mg-ink shadow-[0_24px_48px_-28px_rgba(20,32,31,.35)] max-md:h-[600px] max-md:grid-cols-[minmax(0,1fr)]" data-messenger data-view="list">
  <section class="flex min-h-0 flex-col border-r border-mg-line bg-mg-side max-md:border-r-0 max-md:group-data-[view=thread]:hidden" aria-labelledby="mg-inbox">
    <div class="flex items-baseline justify-between gap-3 px-5 pt-[22px] pb-3.5"><h2 class="text-[1.25rem] font-extrabold tracking-[-.02em]" id="mg-inbox">Messages</h2><p class="text-[.8rem] font-semibold text-mg-accent-dark" data-unread-total>3 unread</p></div>
    <ul class="flex-1 overflow-y-auto px-2.5 pb-3">
      <li><button class="group/c flex w-full cursor-pointer items-center gap-3 rounded-[14px] px-2.5 py-[11px] text-left transition-[background-color] duration-150 hover:bg-[#edf3f1] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-mg-accent aria-[current=true]:bg-mg-soft motion-reduce:transition-none" type="button" data-open="maya" aria-current="true">
        <span class="relative grid size-11 flex-none place-items-center rounded-full bg-[#cfe3ff] text-[.8rem] font-bold text-[#1d3f8f]" aria-hidden="true">MC<i class="absolute right-0 bottom-px size-3 rounded-full border-2 border-mg-side bg-mg-online"></i></span>
        <span class="flex min-w-0 flex-1 flex-col">
          <span class="flex items-baseline justify-between gap-2"><b class="truncate font-bold">Maya Chen<span class="sr-only">, online</span></b><time class="flex-none text-[.74rem] text-mg-muted" datetime="2026-10-14T09:12">09:12</time></span>
          <span class="mt-0.5 flex items-center gap-2"><span class="min-w-0 flex-1 truncate text-[.86rem] text-mg-muted group-has-[[data-unread]]/c:font-semibold group-has-[[data-unread]]/c:text-mg-ink" data-preview>You: See you at 16:00 at the loading bay.</span></span>
        </span></button></li>
      <li><button class="group/c flex w-full cursor-pointer items-center gap-3 rounded-[14px] px-2.5 py-[11px] text-left transition-[background-color] duration-150 hover:bg-[#edf3f1] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-mg-accent aria-[current=true]:bg-mg-soft motion-reduce:transition-none" type="button" data-open="club">
        <span class="relative grid size-11 flex-none place-items-center rounded-full bg-[#fde2cf] text-[.8rem] font-bold text-[#8a3b0a]" aria-hidden="true">NC</span>
        <span class="flex min-w-0 flex-1 flex-col">
          <span class="flex items-baseline justify-between gap-2"><b class="truncate font-bold">Northside Climbing Club</b><time class="flex-none text-[.74rem] text-mg-muted" datetime="2026-10-14T08:40">08:40</time></span>
          <span class="mt-0.5 flex items-center gap-2"><span class="min-w-0 flex-1 truncate text-[.86rem] text-mg-muted group-has-[[data-unread]]/c:font-semibold group-has-[[data-unread]]/c:text-mg-ink" data-preview>Jonas: Wall is booked for Saturday at 10:00</span><span class="min-w-[22px] flex-none rounded-full bg-mg-accent px-[7px] py-0.5 text-center text-[.72rem] font-bold text-white" data-unread="2">2<span class="sr-only"> unread</span></span></span>
        </span></button></li>
      <li><button class="group/c flex w-full cursor-pointer items-center gap-3 rounded-[14px] px-2.5 py-[11px] text-left transition-[background-color] duration-150 hover:bg-[#edf3f1] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-mg-accent aria-[current=true]:bg-mg-soft motion-reduce:transition-none" type="button" data-open="dev">
        <span class="relative grid size-11 flex-none place-items-center rounded-full bg-[#e6dcff] text-[.8rem] font-bold text-[#4c2a97]" aria-hidden="true">DP</span>
        <span class="flex min-w-0 flex-1 flex-col">
          <span class="flex items-baseline justify-between gap-2"><b class="truncate font-bold">Dev Patel</b><time class="flex-none text-[.74rem] text-mg-muted" datetime="2026-10-13T18:05">Yesterday</time></span>
          <span class="mt-0.5 flex items-center gap-2"><span class="min-w-0 flex-1 truncate text-[.86rem] text-mg-muted group-has-[[data-unread]]/c:font-semibold group-has-[[data-unread]]/c:text-mg-ink" data-preview>Could you send the September invoice?</span><span class="min-w-[22px] flex-none rounded-full bg-mg-accent px-[7px] py-0.5 text-center text-[.72rem] font-bold text-white" data-unread="1">1<span class="sr-only"> unread</span></span></span>
        </span></button></li>
      <li><button class="group/c flex w-full cursor-pointer items-center gap-3 rounded-[14px] px-2.5 py-[11px] text-left transition-[background-color] duration-150 hover:bg-[#edf3f1] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-mg-accent aria-[current=true]:bg-mg-soft motion-reduce:transition-none" type="button" data-open="rosa">
        <span class="relative grid size-11 flex-none place-items-center rounded-full bg-[#d5f5ee] text-[.8rem] font-bold text-[#0b5d56]" aria-hidden="true">RA<i class="absolute right-0 bottom-px size-3 rounded-full border-2 border-mg-side bg-mg-online"></i></span>
        <span class="flex min-w-0 flex-1 flex-col">
          <span class="flex items-baseline justify-between gap-2"><b class="truncate font-bold">Rosa Almeida<span class="sr-only">, online</span></b><time class="flex-none text-[.74rem] text-mg-muted" datetime="2026-10-11T12:30">Sat</time></span>
          <span class="mt-0.5 flex items-center gap-2"><span class="min-w-0 flex-1 truncate text-[.86rem] text-mg-muted group-has-[[data-unread]]/c:font-semibold group-has-[[data-unread]]/c:text-mg-ink" data-preview>You: Count me in for Sunday lunch.</span></span>
        </span></button></li>
    </ul>
  </section>

  <section class="flex min-h-0 min-w-0 flex-col max-md:group-data-[view=list]:hidden" aria-label="Conversation">
    <div class="flex min-h-0 flex-1 flex-col" data-thread="maya">
      <div class="flex items-center gap-3 border-b border-mg-line px-5 py-3.5 max-md:px-3.5 max-md:py-3">
        <button class="-ml-2 hidden size-[38px] cursor-pointer place-items-center rounded-full hover:bg-mg-in focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mg-accent max-md:grid" type="button" data-back><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="sr-only">Back to conversations</span></button>
        <span class="grid size-10 flex-none place-items-center rounded-full bg-[#cfe3ff] text-[.8rem] font-bold text-[#1d3f8f]" aria-hidden="true">MC</span>
        <div><h3 class="text-base font-bold focus:outline-none focus-visible:outline-2 focus-visible:outline-mg-accent" id="mg-t-maya" tabindex="-1">Maya Chen</h3><p class="flex items-center gap-1.5 text-[.8rem] text-mg-muted"><i class="size-2 rounded-full bg-mg-online" aria-hidden="true"></i>Online</p></div>
      </div>
      <div class="flex flex-1 flex-col gap-3 overflow-y-auto scroll-smooth p-5 focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-mg-accent max-md:px-3 max-md:py-4 motion-reduce:scroll-auto" role="log" aria-live="polite" aria-labelledby="mg-t-maya" tabindex="0">
        <p class="self-center rounded-full bg-mg-in px-3 py-1 text-[.74rem] font-semibold text-mg-muted"><time datetime="2026-10-13">Yesterday</time></p>
        <div class="flex max-w-[min(78%,480px)] flex-col items-start gap-[3px] max-md:max-w-[88%]"><p class="ml-3 text-[.76rem] font-semibold text-mg-muted">Maya Chen</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_18px_6px] bg-mg-in px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>Venue confirmed the rooftop for Thursday.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-muted" datetime="2026-10-13T17:42">17:42</time></p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[6px_18px_18px_6px] bg-mg-in px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>We only have it until 19:30, so golden hour is tight.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-muted" datetime="2026-10-13T17:43">17:43</time></p></div>
        <div class="flex max-w-[min(78%,480px)] flex-col items-end gap-[3px] self-end max-md:max-w-[88%]"><p class="sr-only">You</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_6px_18px] bg-mg-accent px-[13px] pt-2 pb-[7px] whitespace-pre-wrap text-white [overflow-wrap:anywhere]"><span>Then portraits first, product shots inside after.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-soft" datetime="2026-10-13T17:51">17:51</time></p></div>
        <p class="self-center rounded-full bg-mg-in px-3 py-1 text-[.74rem] font-semibold text-mg-muted"><time datetime="2026-10-14">Today</time></p>
        <div class="flex max-w-[min(78%,480px)] flex-col items-start gap-[3px] max-md:max-w-[88%]"><p class="ml-3 text-[.76rem] font-semibold text-mg-muted">Maya Chen</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_18px_6px] bg-mg-in px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>Morning. Can you bring the second reflector?</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-muted" datetime="2026-10-14T08:58">08:58</time></p></div>
        <div class="flex max-w-[min(78%,480px)] flex-col items-end gap-[3px] self-end max-md:max-w-[88%]"><p class="sr-only">You</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_6px_18px] bg-mg-accent px-[13px] pt-2 pb-[7px] whitespace-pre-wrap text-white [overflow-wrap:anywhere]"><span>Already packed, along with the spare batteries.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-soft" datetime="2026-10-14T09:12">09:12</time></p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_6px_6px_18px] bg-mg-accent px-[13px] pt-2 pb-[7px] whitespace-pre-wrap text-white [overflow-wrap:anywhere]"><span>See you at 16:00 at the loading bay.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-soft" datetime="2026-10-14T09:12">09:12</time></p><p class="text-[.72rem] text-mg-muted">Read</p></div>
      </div>
    </div>
    <div class="flex min-h-0 flex-1 flex-col" data-thread="club" hidden>
      <div class="flex items-center gap-3 border-b border-mg-line px-5 py-3.5 max-md:px-3.5 max-md:py-3">
        <button class="-ml-2 hidden size-[38px] cursor-pointer place-items-center rounded-full hover:bg-mg-in focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mg-accent max-md:grid" type="button" data-back><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="sr-only">Back to conversations</span></button>
        <span class="grid size-10 flex-none place-items-center rounded-full bg-[#fde2cf] text-[.8rem] font-bold text-[#8a3b0a]" aria-hidden="true">NC</span>
        <div><h3 class="text-base font-bold focus:outline-none focus-visible:outline-2 focus-visible:outline-mg-accent" id="mg-t-club" tabindex="-1">Northside Climbing Club</h3><p class="text-[.8rem] text-mg-muted">14 members</p></div>
      </div>
      <div class="flex flex-1 flex-col gap-3 overflow-y-auto scroll-smooth p-5 focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-mg-accent max-md:px-3 max-md:py-4 motion-reduce:scroll-auto" role="log" aria-live="polite" aria-labelledby="mg-t-club" tabindex="0">
        <p class="self-center rounded-full bg-mg-in px-3 py-1 text-[.74rem] font-semibold text-mg-muted"><time datetime="2026-10-14">Today</time></p>
        <div class="flex max-w-[min(78%,480px)] flex-col items-start gap-[3px] max-md:max-w-[88%]"><p class="ml-3 text-[.76rem] font-semibold text-mg-muted">Jonas Weber</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_18px_6px] bg-mg-in px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>Wall is booked for Saturday at 10:00.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-muted" datetime="2026-10-14T08:39">08:39</time></p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[6px_18px_18px_6px] bg-mg-in px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>Bring your own chalk, the shop is closed for stocktake.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-muted" datetime="2026-10-14T08:40">08:40</time></p></div>
      </div>
    </div>
    <div class="flex min-h-0 flex-1 flex-col" data-thread="dev" hidden>
      <div class="flex items-center gap-3 border-b border-mg-line px-5 py-3.5 max-md:px-3.5 max-md:py-3">
        <button class="-ml-2 hidden size-[38px] cursor-pointer place-items-center rounded-full hover:bg-mg-in focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mg-accent max-md:grid" type="button" data-back><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="sr-only">Back to conversations</span></button>
        <span class="grid size-10 flex-none place-items-center rounded-full bg-[#e6dcff] text-[.8rem] font-bold text-[#4c2a97]" aria-hidden="true">DP</span>
        <div><h3 class="text-base font-bold focus:outline-none focus-visible:outline-2 focus-visible:outline-mg-accent" id="mg-t-dev" tabindex="-1">Dev Patel</h3><p class="text-[.8rem] text-mg-muted">Last seen yesterday at 18:20</p></div>
      </div>
      <div class="flex flex-1 flex-col gap-3 overflow-y-auto scroll-smooth p-5 focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-mg-accent max-md:px-3 max-md:py-4 motion-reduce:scroll-auto" role="log" aria-live="polite" aria-labelledby="mg-t-dev" tabindex="0">
        <p class="self-center rounded-full bg-mg-in px-3 py-1 text-[.74rem] font-semibold text-mg-muted"><time datetime="2026-10-13">Yesterday</time></p>
        <div class="flex max-w-[min(78%,480px)] flex-col items-start gap-[3px] max-md:max-w-[88%]"><p class="ml-3 text-[.76rem] font-semibold text-mg-muted">Dev Patel</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_18px_6px] bg-mg-in px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>Thanks for the retouched set, the client loved it.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-muted" datetime="2026-10-13T18:04">18:04</time></p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[6px_18px_18px_6px] bg-mg-in px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>Could you send the September invoice?</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-muted" datetime="2026-10-13T18:05">18:05</time></p></div>
      </div>
    </div>
    <div class="flex min-h-0 flex-1 flex-col" data-thread="rosa" hidden>
      <div class="flex items-center gap-3 border-b border-mg-line px-5 py-3.5 max-md:px-3.5 max-md:py-3">
        <button class="-ml-2 hidden size-[38px] cursor-pointer place-items-center rounded-full hover:bg-mg-in focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mg-accent max-md:grid" type="button" data-back><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="sr-only">Back to conversations</span></button>
        <span class="grid size-10 flex-none place-items-center rounded-full bg-[#d5f5ee] text-[.8rem] font-bold text-[#0b5d56]" aria-hidden="true">RA</span>
        <div><h3 class="text-base font-bold focus:outline-none focus-visible:outline-2 focus-visible:outline-mg-accent" id="mg-t-rosa" tabindex="-1">Rosa Almeida</h3><p class="flex items-center gap-1.5 text-[.8rem] text-mg-muted"><i class="size-2 rounded-full bg-mg-online" aria-hidden="true"></i>Online</p></div>
      </div>
      <div class="flex flex-1 flex-col gap-3 overflow-y-auto scroll-smooth p-5 focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-mg-accent max-md:px-3 max-md:py-4 motion-reduce:scroll-auto" role="log" aria-live="polite" aria-labelledby="mg-t-rosa" tabindex="0">
        <p class="self-center rounded-full bg-mg-in px-3 py-1 text-[.74rem] font-semibold text-mg-muted"><time datetime="2026-10-11">Saturday</time></p>
        <div class="flex max-w-[min(78%,480px)] flex-col items-start gap-[3px] max-md:max-w-[88%]"><p class="ml-3 text-[.76rem] font-semibold text-mg-muted">Rosa Almeida</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_18px_6px] bg-mg-in px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>Lunch on Sunday? I'm making bacalhau.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-muted" datetime="2026-10-11T12:21">12:21</time></p></div>
        <div class="flex max-w-[min(78%,480px)] flex-col items-end gap-[3px] self-end max-md:max-w-[88%]"><p class="sr-only">You</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_6px_18px] bg-mg-accent px-[13px] pt-2 pb-[7px] whitespace-pre-wrap text-white [overflow-wrap:anywhere]"><span>Count me in for Sunday lunch.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-soft" datetime="2026-10-11T12:30">12:30</time></p><p class="text-[.72rem] text-mg-muted">Read</p></div>
      </div>
    </div>

    <form class="flex items-end gap-2.5 border-t border-mg-line px-4 pt-3 pb-4 max-md:px-3 max-md:pt-2.5 max-md:pb-3" data-composer>
      <label class="sr-only" for="mg-input">Message</label>
      <textarea class="max-h-[140px] min-w-0 flex-1 resize-none rounded-[22px] border-[1.5px] border-[#8a9895] bg-white px-4 py-[11px] leading-[1.4] text-mg-ink placeholder:text-[#5f6b68] focus-visible:border-mg-accent focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-mg-accent" id="mg-input" rows="1" placeholder="Write a message" data-input></textarea>
      <button class="grid size-[46px] flex-none cursor-pointer place-items-center rounded-full bg-mg-accent text-white transition-[background-color] duration-150 hover:bg-mg-accent-dark focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mg-accent disabled:cursor-not-allowed disabled:bg-[#c8d3d0] disabled:text-[#4d5a57] motion-reduce:transition-none" type="submit"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12 20 4l-6 16-3-7z"/><path d="M11 13 20 4"/></svg><span class="sr-only">Send</span></button>
    </form>
    <template data-tpl><div class="flex max-w-[min(78%,480px)] flex-col items-end gap-[3px] self-end max-md:max-w-[88%]"><p class="sr-only">You</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_6px_18px] bg-mg-accent px-[13px] pt-2 pb-[7px] whitespace-pre-wrap text-white [overflow-wrap:anywhere]"><span data-text></span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-mg-soft" data-time></time></p><p class="text-[.72rem] text-mg-muted">Sent</p></div></template>
  </section>
</div></div>
CSS
/* Messenger palette as theme colours. The layout switches panes with group-data-[view=…] variants under md. */
@theme {
  --color-mg-accent:#0f766e; --color-mg-accent-dark:#0b5d56; --color-mg-soft:#d5f5ee; --color-mg-in:#eef3f2;
  --color-mg-ink:#14201f; --color-mg-muted:#55625f; --color-mg-line:#dde5e3; --color-mg-side:#f7faf9; --color-mg-online:#15803d;
}
JavaScript
/* Messenger: open a conversation, go back on small screens, grow the textarea, send with Enter (Shift+Enter = new line).
   Sending only appends the message to the page. */
document.querySelectorAll('[data-messenger]').forEach(function (app) {
  var convos = app.querySelectorAll('[data-open]');
  var form = app.querySelector('[data-composer]');
  var input = app.querySelector('[data-input]');
  var send = form.querySelector('[type="submit"]');
  var tpl = app.querySelector('template[data-tpl]');
  var narrow = window.matchMedia('(max-width: 767.98px)');
  var pad = function (n) { return String(n).padStart(2, '0'); };
  var thread = function () { return app.querySelector('[data-thread]:not([hidden])'); };

  function updateTotal() {
    var n = 0;
    app.querySelectorAll('[data-unread]').forEach(function (b) { n += +b.dataset.unread; });
    app.querySelector('[data-unread-total]').textContent = n ? n + ' unread' : 'All read';
  }
  convos.forEach(function (btn) {
    btn.addEventListener('click', function () {
      convos.forEach(function (b) { b.removeAttribute('aria-current'); });
      btn.setAttribute('aria-current', 'true');
      app.querySelectorAll('[data-thread]').forEach(function (t) { t.hidden = t.dataset.thread !== btn.dataset.open; });
      var badge = btn.querySelector('[data-unread]');
      if (badge) { badge.remove(); updateTotal(); }
      var log = thread().querySelector('[role="log"]');
      log.scrollTop = log.scrollHeight;
      app.dataset.view = 'thread';
      if (narrow.matches) thread().querySelector('h3').focus();
    });
  });
  app.querySelectorAll('[data-back]').forEach(function (b) {
    b.addEventListener('click', function () {
      app.dataset.view = 'list';
      app.querySelector('[aria-current="true"]').focus();
    });
  });

  function grow() { input.style.height = 'auto'; input.style.height = input.scrollHeight + 2 + '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 = tpl.content.cloneNode(true), time = msg.querySelector('[data-time]');
    msg.querySelector('[data-text]').textContent = text;
    time.textContent = pad(d.getHours()) + ':' + pad(d.getMinutes());
    time.dateTime = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + time.textContent;
    var log = thread().querySelector('[role="log"]');
    log.appendChild(msg);
    log.scrollTop = log.scrollHeight;
    var prev = app.querySelector('[aria-current="true"] [data-preview]');
    if (prev) prev.textContent = 'You: ' + text;
    input.value = ''; grow(); input.focus();
  });
  grow();
});

About this set

A complete two-pane messenger: a conversation list on the left and the open thread with its composer on the right. The list shows initials avatars, an online dot backed by hidden text, the last message, its time and an unread count badge, and the header totals the unread messages. Selecting a conversation marks it with aria-current, shows its thread, removes its badge and updates the total. Below 768px the layout collapses to one pane: the list first, then the thread with a back button that returns focus to the conversation you came from. The composer is a textarea that grows with its text, sends on Enter, adds a line on Shift+Enter and keeps the Send button disabled while it is empty. Sending appends the message locally with a Sent status and updates the preview in the list; nothing is sent to a server. The script is about 60 lines of vanilla JavaScript driven by data attributes, so the same script runs the plain CSS, Bootstrap and Tailwind files. The Bootstrap version uses .list-group-item-action buttons, .badge, .form-control and .btn.

What’s included

  • Conversation list, thread and composer in one layout
  • Collapses to one pane under 768px with a back button
  • Unread badges with hidden text and a running total
  • Enter to send, Shift+Enter for a new line, growing textarea
  • Bootstrap .list-group, .badge, .form-control and .btn

Accessibility

Each thread is a focusable role="log" region with aria-live="polite", named by the contact's heading. Conversations are buttons with aria-current on the open one, and online status and unread counts include hidden text. On small screens focus moves to the thread heading when a conversation opens and back to the conversation when you press Back. Every control shows a 2px teal focus outline, the textarea has a visually hidden label, and the hover transitions are removed under prefers-reduced-motion.

Customise it

Set --mg-accent, --mg-soft and --mg-in on .mg for colours, and change the 767.98px media query (max-md: in Tailwind) to move the one-pane breakpoint. Bootstrap uses the same --mg-* variables plus --bs-list-group-* overrides on .mg-convos.