Open demo

CSS code

HTML
<div><span class="lbl">Bubble thread with date separators and read receipts</span>
<div class="cb" role="log" aria-live="polite" aria-label="Chat with Maya Chen">
  <p class="cb-day"><time datetime="2026-10-12">Monday, 12 October</time></p>
  <div class="cb-group in">
    <span class="cb-av" aria-hidden="true">MC</span>
    <div class="cb-stack">
      <p class="cb-who">Maya Chen</p>
      <p class="cb-b"><span>Venue just confirmed the rooftop for Thursday.</span> <time datetime="2026-10-12T17:42">17:42</time></p>
      <p class="cb-b"><span>They can only give us until 19:30 though, so golden hour is tight.</span> <time datetime="2026-10-12T17:43">17:43</time></p>
    </div>
  </div>
  <div class="cb-group out">
    <div class="cb-stack">
      <p class="cb-who sr-only">You</p>
      <p class="cb-b"><span>That works. I'll move the portraits first and do the product shots inside after.</span> <time datetime="2026-10-12T17:51">17:51</time></p>
    </div>
  </div>
  <p class="cb-day"><time datetime="2026-10-14">Today</time></p>
  <div class="cb-group in">
    <span class="cb-av" aria-hidden="true">MC</span>
    <div class="cb-stack">
      <p class="cb-who">Maya Chen</p>
      <p class="cb-b"><span>Morning. Can you bring the second reflector?</span> <time datetime="2026-10-14T08:58">08:58</time></p>
    </div>
  </div>
  <div class="cb-group out">
    <div class="cb-stack">
      <p class="cb-who sr-only">You</p>
      <p class="cb-b"><span>Already packed, along with the spare batteries.</span> <time datetime="2026-10-14T09:12">09:12</time></p>
      <p class="cb-b"><span>See you at 16:00 at the loading bay.</span> <time datetime="2026-10-14T09:12">09:12</time></p>
      <p class="cb-rcpt"><svg width="16" height="12" viewBox="0 0 16 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 6.5 4.5 10 11 2.5M7.5 9.5 8 10l6.5-7.5"/></svg>Read 09:14</p>
    </div>
  </div>
</div></div>

<div><span class="lbl">Compact rows for channels</span>
<div class="cb rows" role="log" aria-live="polite" aria-label="Channel design-review">
  <div class="cb-row">
    <span class="cb-av teal" aria-hidden="true">RK</span>
    <div><p class="cb-head"><b>Ravi Kumar</b> <time datetime="2026-10-14T10:02">10:02</time></p>
    <p>Pushed the new onboarding screens to the review board.</p></div>
  </div>
  <div class="cb-row cont">
    <time class="cb-gut" datetime="2026-10-14T10:03">10:03</time>
    <p><span class="sr-only">Ravi Kumar: </span>The empty state on step 3 still needs copy.</p>
  </div>
  <div class="cb-row">
    <span class="cb-av amber" aria-hidden="true">LO</span>
    <div><p class="cb-head"><b>Lena Ortiz</b> <time datetime="2026-10-14T10:09">10:09</time></p>
    <p>I can write it. Is it for teams with no projects yet, or for invited members?</p></div>
  </div>
  <div class="cb-row">
    <span class="cb-av teal" aria-hidden="true">RK</span>
    <div><p class="cb-head"><b>Ravi Kumar</b> <time datetime="2026-10-14T10:11">10:11</time></p>
    <p>Invited members. They land there before anyone has shared a project with them.</p></div>
  </div>
  <div class="cb-row cont">
    <time class="cb-gut" datetime="2026-10-14T10:11">10:11</time>
    <p><span class="sr-only">Ravi Kumar: </span>Thanks, Lena.</p>
  </div>
</div></div>

<div><span class="lbl">Files, link previews, notices and failed messages</span>
<div class="cb" role="log" aria-live="polite" aria-label="Chat with the Lisbon shoot group">
  <p class="cb-sys">Lena Ortiz added Tom Becker to the chat</p>
  <div class="cb-group in">
    <span class="cb-av" aria-hidden="true">LO</span>
    <div class="cb-stack">
      <p class="cb-who">Lena Ortiz</p>
      <div class="cb-b card">
        <div class="cb-file">
          <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5M9 13h6M9 17h4"/></svg>
          <span>shoot-schedule-v3.pdf <small>PDF, 1.2 MB</small></span>
          <a href="#"><svg width="16" height="16" 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 4v11M7 10.5l5 5 5-5M5 20h14"/></svg><span class="sr-only">Download shoot-schedule-v3.pdf</span></a>
        </div>
        <time datetime="2026-10-14T11:20">11:20</time>
      </div>
      <p class="cb-b"><span class="cb-gone">This message was deleted</span> <time datetime="2026-10-14T11:21">11:21</time></p>
    </div>
  </div>
  <div class="cb-group out">
    <div class="cb-stack">
      <p class="cb-who sr-only">You</p>
      <div class="cb-b card">
        <span>Sunset is at 19:04 that day:</span>
        <a class="cb-link" href="#"><strong>Golden hour times for Lisbon</strong><span>sunwatch.example</span></a>
        <time datetime="2026-10-14T11:24">11:24</time>
      </div>
      <p class="cb-b failed"><span>Tom, can you check the drone permit?</span> <time datetime="2026-10-14T11:25">11:25</time></p>
      <p class="cb-fail"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v6M12 16.5v.5"/></svg>Not delivered</p>
    </div>
  </div>
</div></div>
CSS
/* Message bubbles: a role="log" thread of sender groups. Each bubble carries its own <time>; receipts are text. */
.cb{--cb-accent:#4f46e5;--cb-on-accent:#fff;--cb-accent-soft:#e0e7ff;--cb-in:#eef0f6;--cb-ink:#1b1f2a;--cb-muted:#5d6475;
  --cb-line:#e2e5ee;--cb-bg:#fff;--cb-r:18px;--cb-r-tight:6px;--cb-danger:#b42318;
  display:flex;flex-direction:column;gap:14px;padding:24px 20px;background:var(--cb-bg);border:1px solid var(--cb-line);
  border-radius:22px;color:var(--cb-ink);font-size:.95rem;line-height:1.45;box-shadow:0 12px 32px -18px rgba(27,31,42,.25)}
.cb p{margin:0}
.cb .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}

/* date separator */
.cb-day{display:flex;align-items:center;gap:12px;margin:6px 0;font-size:.74rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--cb-muted)}
.cb-day::before,.cb-day::after{content:"";flex:1;height:1px;background:var(--cb-line)}

/* sender group */
.cb-group{display:flex;align-items:flex-end;gap:10px;max-width:min(82%,520px)}
.cb-group.out{align-self:flex-end;flex-direction:row-reverse}
.cb-av{flex:none;display:grid;place-items:center;width:32px;height:32px;border-radius:50%;background:#dbe4ff;color:#2f2a8f;font-size:.72rem;font-weight:700}
.cb-stack{display:flex;flex-direction:column;gap:3px;min-width:0}
.cb-group.out .cb-stack{align-items:flex-end}
.cb-who{margin:0 0 3px 12px;font-size:.78rem;font-weight:600;color:var(--cb-muted)}

/* bubble: text and time share one flex row, so short messages keep the time beside them */
.cb-b{display:flex;flex-wrap:wrap;align-items:flex-end;column-gap:10px;padding:8px 13px 7px;border-radius:var(--cb-r);background:var(--cb-in);overflow-wrap:anywhere}
/* grouped corners: the side facing the sender stays tight, except the top of the first bubble */
.cb-group.in .cb-b{border-radius:var(--cb-r-tight) var(--cb-r) var(--cb-r) var(--cb-r-tight)}
.cb-group.in .cb-who+.cb-b{border-top-left-radius:var(--cb-r)}
.cb-group.out .cb-b{border-radius:var(--cb-r) var(--cb-r-tight) var(--cb-r-tight) var(--cb-r);background:var(--cb-accent);color:var(--cb-on-accent)}
.cb-group.out .cb-who+.cb-b{border-top-right-radius:var(--cb-r)}
.cb-b time{margin-left:auto;font-size:.7rem;color:var(--cb-muted);white-space:nowrap}
.cb-group.out .cb-b time{color:var(--cb-accent-soft)}
.cb-rcpt{display:flex;align-items:center;gap:4px;margin-top:2px;font-size:.72rem;color:var(--cb-muted)}
.cb-rcpt svg{color:var(--cb-accent)}

/* compact rows (channel style) */
.cb.rows{gap:2px;padding:18px 12px}
.cb-row{display:grid;grid-template-columns:40px 1fr;column-gap:12px;padding:6px 10px;border-radius:10px}
.cb-row:hover{background:#f6f7fb}
.cb-row .cb-av{width:36px;height:36px;border-radius:10px;margin-top:2px}
.cb-row .cb-av.teal{background:#ccf2ea;color:#0b5a4c}
.cb-row .cb-av.amber{background:#fde7c4;color:#7a4206}
.cb-head{display:flex;align-items:baseline;gap:8px}
.cb-head b{font-weight:700}
.cb-head time,.cb-gut{font-size:.72rem;color:var(--cb-muted)}
.cb-row.cont{padding-top:1px}
.cb-gut{align-self:center;justify-self:center;white-space:nowrap}
.cb-row+.cb-row:not(.cont){margin-top:8px}

/* rich content */
.cb-sys{align-self:center;padding:5px 12px;border-radius:999px;background:var(--cb-in);font-size:.78rem;color:var(--cb-muted);text-align:center}
.cb-b.card{flex-direction:column;align-items:stretch;row-gap:8px;width:300px;max-width:100%;padding:10px}
.cb-file{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:12px;background:var(--cb-bg);color:var(--cb-ink)}
.cb-file svg{flex:none;color:var(--cb-accent)}
.cb-file span{display:flex;flex-direction:column;min-width:0;font-size:.88rem}
.cb-file small{font-size:.76rem;color:var(--cb-muted)}
.cb-file a{flex:none;display:grid;place-items:center;width:34px;height:34px;margin-left:auto;border-radius:10px;background:var(--cb-accent-soft);color:var(--cb-accent)}
.cb-file a:hover{background:#c7d2fe}
.cb-link{display:block;padding:10px 12px;border-left:3px solid var(--cb-accent-soft);border-radius:10px;background:#3f37c9;color:inherit;text-decoration:none}
.cb-link strong{display:block;font-size:.9rem}
.cb-link span{display:block;margin-top:2px;font-size:.78rem;color:var(--cb-accent-soft)}
.cb-link:hover strong{text-decoration:underline}
.cb-gone{font-style:italic;color:var(--cb-muted)}
.cb-group.out .cb-b.failed{background:var(--cb-bg);color:var(--cb-ink);border:1.5px solid var(--cb-danger)}
.cb-group.out .cb-b.failed time{color:var(--cb-muted)}
.cb-fail{display:flex;align-items:center;gap:5px;margin-top:2px;font-size:.74rem;font-weight:600;color:var(--cb-danger)}
.cb a:focus-visible{outline:2px solid var(--cb-accent);outline-offset:2px}
.cb-group.out .cb-link:focus-visible{outline-color:var(--cb-on-accent)}
@media (max-width:480px){.cb{padding:18px 12px}.cb-group{max-width:90%}.cb-group .cb-av{width:28px;height:28px;font-size:.66rem}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Bubble thread with date separators and read receipts</span>
<div class="card cb" role="log" aria-live="polite" aria-label="Chat with Maya Chen">
 <div class="card-body vstack gap-3">
  <p class="cb-day d-flex align-items-center gap-3 fw-semibold text-uppercase text-body-secondary"><time datetime="2026-10-12">Monday, 12 October</time></p>
  <div class="cb-group cb-in d-flex align-items-end gap-2">
    <span class="cb-av rounded-circle flex-shrink-0 fw-bold" aria-hidden="true">MC</span>
    <div class="vstack gap-1 align-items-start" style="min-width:0">
      <p class="cb-who fw-semibold">Maya Chen</p>
      <p class="cb-b"><span>Venue just confirmed the rooftop for Thursday.</span> <time datetime="2026-10-12T17:42">17:42</time></p>
      <p class="cb-b"><span>They can only give us until 19:30 though, so golden hour is tight.</span> <time datetime="2026-10-12T17:43">17:43</time></p>
    </div>
  </div>
  <div class="cb-group cb-out d-flex align-self-end">
    <div class="vstack gap-1 align-items-end" style="min-width:0">
      <p class="cb-who visually-hidden">You</p>
      <p class="cb-b"><span>That works. I'll move the portraits first and do the product shots inside after.</span> <time datetime="2026-10-12T17:51">17:51</time></p>
    </div>
  </div>
  <p class="cb-day d-flex align-items-center gap-3 fw-semibold text-uppercase text-body-secondary"><time datetime="2026-10-14">Today</time></p>
  <div class="cb-group cb-in d-flex align-items-end gap-2">
    <span class="cb-av rounded-circle flex-shrink-0 fw-bold" aria-hidden="true">MC</span>
    <div class="vstack gap-1 align-items-start" style="min-width:0">
      <p class="cb-who fw-semibold">Maya Chen</p>
      <p class="cb-b"><span>Morning. Can you bring the second reflector?</span> <time datetime="2026-10-14T08:58">08:58</time></p>
    </div>
  </div>
  <div class="cb-group cb-out d-flex align-self-end">
    <div class="vstack gap-1 align-items-end" style="min-width:0">
      <p class="cb-who visually-hidden">You</p>
      <p class="cb-b"><span>Already packed, along with the spare batteries.</span> <time datetime="2026-10-14T09:12">09:12</time></p>
      <p class="cb-b"><span>See you at 16:00 at the loading bay.</span> <time datetime="2026-10-14T09:12">09:12</time></p>
      <p class="cb-rcpt d-flex align-items-center gap-1 mt-1"><svg width="16" height="12" viewBox="0 0 16 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 6.5 4.5 10 11 2.5M7.5 9.5 8 10l6.5-7.5"/></svg>Read 09:14</p>
    </div>
  </div>
 </div>
</div></div>

<div><span class="lbl">Compact rows for channels</span>
<div class="card cb cb-rows" role="log" aria-live="polite" aria-label="Channel design-review">
 <div class="card-body vstack gap-0">
  <div class="cb-row">
    <span class="cb-av teal rounded-3 fw-bold" aria-hidden="true">RK</span>
    <div><p class="d-flex align-items-baseline gap-2 cb-head"><b class="fw-bold">Ravi Kumar</b> <time datetime="2026-10-14T10:02">10:02</time></p>
    <p>Pushed the new onboarding screens to the review board.</p></div>
  </div>
  <div class="cb-row cont">
    <time class="cb-gut align-self-center justify-self-center text-nowrap" datetime="2026-10-14T10:03">10:03</time>
    <p><span class="visually-hidden">Ravi Kumar: </span>The empty state on step 3 still needs copy.</p>
  </div>
  <div class="cb-row">
    <span class="cb-av amber rounded-3 fw-bold" aria-hidden="true">LO</span>
    <div><p class="d-flex align-items-baseline gap-2 cb-head"><b class="fw-bold">Lena Ortiz</b> <time datetime="2026-10-14T10:09">10:09</time></p>
    <p>I can write it. Is it for teams with no projects yet, or for invited members?</p></div>
  </div>
  <div class="cb-row">
    <span class="cb-av teal rounded-3 fw-bold" aria-hidden="true">RK</span>
    <div><p class="d-flex align-items-baseline gap-2 cb-head"><b class="fw-bold">Ravi Kumar</b> <time datetime="2026-10-14T10:11">10:11</time></p>
    <p>Invited members. They land there before anyone has shared a project with them.</p></div>
  </div>
  <div class="cb-row cont">
    <time class="cb-gut align-self-center justify-self-center text-nowrap" datetime="2026-10-14T10:11">10:11</time>
    <p><span class="visually-hidden">Ravi Kumar: </span>Thanks, Lena.</p>
  </div>
 </div>
</div></div>

<div><span class="lbl">Files, link previews, notices and failed messages</span>
<div class="card cb" role="log" aria-live="polite" aria-label="Chat with the Lisbon shoot group">
 <div class="card-body vstack gap-3">
  <p class="badge rounded-pill cb-sys align-self-center">Lena Ortiz added Tom Becker to the chat</p>
  <div class="cb-group cb-in d-flex align-items-end gap-2">
    <span class="cb-av rounded-circle flex-shrink-0 fw-bold" aria-hidden="true">LO</span>
    <div class="vstack gap-1 align-items-start" style="min-width:0">
      <p class="cb-who fw-semibold">Lena Ortiz</p>
      <div class="cb-b card-b">
        <div class="cb-file d-flex align-items-center gap-3">
          <svg class="flex-shrink-0" style="color:var(--cb-accent)" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5M9 13h6M9 17h4"/></svg>
          <span class="d-flex flex-column" style="min-width:0">shoot-schedule-v3.pdf <small>PDF, 1.2 MB</small></span>
          <a class="btn cb-dl ms-auto" href="#"><svg width="16" height="16" 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 4v11M7 10.5l5 5 5-5M5 20h14"/></svg><span class="visually-hidden">Download shoot-schedule-v3.pdf</span></a>
        </div>
        <time datetime="2026-10-14T11:20">11:20</time>
      </div>
      <p class="cb-b"><span class="fst-italic text-body-secondary">This message was deleted</span> <time datetime="2026-10-14T11:21">11:21</time></p>
    </div>
  </div>
  <div class="cb-group cb-out d-flex align-self-end">
    <div class="vstack gap-1 align-items-end" style="min-width:0">
      <p class="cb-who visually-hidden">You</p>
      <div class="cb-b card-b">
        <span>Sunset is at 19:04 that day:</span>
        <a class="cb-link" href="#"><strong class="d-block" style="font-size:.9rem">Golden hour times for Lisbon</strong><span class="d-block mt-1">sunwatch.example</span></a>
        <time datetime="2026-10-14T11:24">11:24</time>
      </div>
      <p class="cb-b failed"><span>Tom, can you check the drone permit?</span> <time datetime="2026-10-14T11:25">11:25</time></p>
      <p class="cb-fail d-flex align-items-center gap-1 mt-1 fw-semibold"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v6M12 16.5v.5"/></svg>Not delivered</p>
    </div>
  </div>
 </div>
</div></div>
CSS
/* Message bubbles on a Bootstrap .card: utilities do the layout, this block draws the bubbles. */
.cb{--cb-accent:#4f46e5;--cb-on-accent:#fff;--cb-accent-soft:#e0e7ff;--cb-in:#eef0f6;--cb-ink:#1b1f2a;--cb-muted:#5d6475;--cb-line:#e2e5ee;--cb-r:18px;--cb-r-tight:6px;--cb-danger:#b42318;
  --bs-card-spacer-y:24px;--bs-card-spacer-x:20px;--bs-card-border-color:var(--cb-line);--bs-card-border-radius:22px;--bs-card-color:var(--cb-ink);
  font-size:.95rem;line-height:1.45;box-shadow:0 12px 32px -18px rgba(27,31,42,.25)}
.cb p{margin:0}
.cb-day{font-size:.74rem;letter-spacing:.06em;margin:6px 0}
.cb-day::before,.cb-day::after{content:"";flex:1;height:1px;background:var(--cb-line)}
.cb-group{max-width:min(82%,520px)}
.cb-av{display:grid;place-items:center;width:32px;height:32px;background:#dbe4ff;color:#2f2a8f;font-size:.72rem}
.cb-who{margin:0 0 3px 12px;font-size:.78rem;color:var(--cb-muted)}
.cb-b{display:flex;flex-wrap:wrap;align-items:flex-end;column-gap:10px;padding:8px 13px 7px;background:var(--cb-in);overflow-wrap:anywhere}
.cb-in .cb-b{border-radius:var(--cb-r-tight) var(--cb-r) var(--cb-r) var(--cb-r-tight)}
.cb-in .cb-who+.cb-b{border-top-left-radius:var(--cb-r)}
.cb-out .cb-b{border-radius:var(--cb-r) var(--cb-r-tight) var(--cb-r-tight) var(--cb-r);background:var(--cb-accent);color:var(--cb-on-accent)}
.cb-out .cb-who+.cb-b{border-top-right-radius:var(--cb-r)}
.cb-b time{margin-left:auto;font-size:.7rem;color:var(--cb-muted);white-space:nowrap}
.cb-out .cb-b time{color:var(--cb-accent-soft)}
.cb-rcpt{font-size:.72rem;color:var(--cb-muted)}
.cb-rcpt svg{color:var(--cb-accent)}
.cb-rows{--bs-card-spacer-y:18px;--bs-card-spacer-x:12px}
.cb-row{display:grid;grid-template-columns:40px 1fr;column-gap:12px;padding:6px 10px;border-radius:10px}
.cb-row:hover{background:#f6f7fb}
.cb-row .cb-av{width:36px;height:36px;margin-top:2px}
.cb-av.teal{background:#ccf2ea;color:#0b5a4c}
.cb-av.amber{background:#fde7c4;color:#7a4206}
.cb-row.cont{padding-top:1px}
.cb-row+.cb-row:not(.cont){margin-top:8px}
.cb-gut,.cb-head time{font-size:.72rem;color:var(--cb-muted)}
.cb-sys{--bs-badge-padding-x:12px;--bs-badge-padding-y:5px;--bs-badge-font-size:.78rem;--bs-badge-font-weight:400;--bs-badge-color:var(--cb-muted);background:var(--cb-in);white-space:normal}
.cb-b.card-b{flex-direction:column;align-items:stretch;row-gap:8px;width:300px;max-width:100%;padding:10px}
.cb-file{padding:10px 12px;border-radius:12px;background:#fff;color:var(--cb-ink);font-size:.88rem}
.cb-file small{font-size:.76rem;color:var(--cb-muted)}
.cb-dl{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-border-radius:10px;--bs-btn-color:var(--cb-accent);--bs-btn-bg:var(--cb-accent-soft);--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--cb-accent);--bs-btn-hover-bg:#c7d2fe;--bs-btn-hover-border-color:transparent;
  display:grid;place-items:center;flex:none;width:34px;height:34px}
.cb-link{display:block;padding:10px 12px;border-left:3px solid var(--cb-accent-soft);border-radius:10px;background:#3f37c9;color:inherit;text-decoration:none}
.cb-link span{color:var(--cb-accent-soft);font-size:.78rem}
.cb-link:hover strong{text-decoration:underline}
.cb-out .cb-b.failed{background:#fff;color:var(--cb-ink);border:1.5px solid var(--cb-danger)}
.cb-out .cb-b.failed time{color:var(--cb-muted)}
.cb-fail{font-size:.74rem;color:var(--cb-danger)}
.cb a:focus-visible{outline:2px solid var(--cb-accent);outline-offset:2px;box-shadow:none}
.cb-out .cb-link:focus-visible{outline-color:var(--cb-on-accent)}
@media (max-width:480px){.cb{--bs-card-spacer-y:18px;--bs-card-spacer-x:12px}.cb-group{max-width:90%}.cb-group .cb-av{width:28px;height:28px;font-size:.66rem}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-cb-muted uppercase">Bubble thread with date separators and read receipts</span>
<div class="flex flex-col gap-3.5 rounded-[22px] border border-cb-line bg-white px-5 py-6 text-[.95rem] leading-[1.45] text-cb-ink shadow-[0_12px_32px_-18px_rgba(27,31,42,.25)] max-[480px]:px-3 max-[480px]:py-[18px]" role="log" aria-live="polite" aria-label="Chat with Maya Chen">
  <p class="my-1.5 flex items-center gap-3 text-[.74rem] font-semibold tracking-[.06em] text-cb-muted uppercase before:h-px before:flex-1 before:bg-cb-line after:h-px after:flex-1 after:bg-cb-line"><time datetime="2026-10-12">Monday, 12 October</time></p>
  <div class="flex max-w-[min(82%,520px)] items-end gap-2.5 max-[480px]:max-w-[90%]">
    <span class="grid size-8 flex-none place-items-center rounded-full bg-[#dbe4ff] text-[.72rem] font-bold text-[#2f2a8f] max-[480px]:size-7" aria-hidden="true">MC</span>
    <div class="flex min-w-0 flex-col items-start gap-[3px]">
      <p class="mb-[3px] ml-3 text-[.78rem] font-semibold text-cb-muted">Maya Chen</p>
      <p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_18px_6px] bg-cb-in px-[13px] pt-2 pb-[7px] [overflow-wrap:anywhere]"><span>Venue just confirmed the rooftop for Thursday.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-cb-muted" datetime="2026-10-12T17:42">17:42</time></p>
      <p class="flex flex-wrap items-end gap-x-2.5 rounded-[6px_18px_18px_6px] bg-cb-in px-[13px] pt-2 pb-[7px] [overflow-wrap:anywhere]"><span>They can only give us until 19:30 though, so golden hour is tight.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-cb-muted" datetime="2026-10-12T17:43">17:43</time></p>
    </div>
  </div>
  <div class="flex max-w-[min(82%,520px)] self-end max-[480px]:max-w-[90%]">
    <div class="flex min-w-0 flex-col items-end gap-[3px]">
      <p class="sr-only">You</p>
      <p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_6px_18px] bg-cb-accent px-[13px] pt-2 pb-[7px] text-white [overflow-wrap:anywhere]"><span>That works. I'll move the portraits first and do the product shots inside after.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-cb-accent-soft" datetime="2026-10-12T17:51">17:51</time></p>
    </div>
  </div>
  <p class="my-1.5 flex items-center gap-3 text-[.74rem] font-semibold tracking-[.06em] text-cb-muted uppercase before:h-px before:flex-1 before:bg-cb-line after:h-px after:flex-1 after:bg-cb-line"><time datetime="2026-10-14">Today</time></p>
  <div class="flex max-w-[min(82%,520px)] items-end gap-2.5 max-[480px]:max-w-[90%]">
    <span class="grid size-8 flex-none place-items-center rounded-full bg-[#dbe4ff] text-[.72rem] font-bold text-[#2f2a8f] max-[480px]:size-7" aria-hidden="true">MC</span>
    <div class="flex min-w-0 flex-col items-start gap-[3px]">
      <p class="mb-[3px] ml-3 text-[.78rem] font-semibold text-cb-muted">Maya Chen</p>
      <p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_18px_6px] bg-cb-in px-[13px] pt-2 pb-[7px] [overflow-wrap:anywhere]"><span>Morning. Can you bring the second reflector?</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-cb-muted" datetime="2026-10-14T08:58">08:58</time></p>
    </div>
  </div>
  <div class="flex max-w-[min(82%,520px)] self-end max-[480px]:max-w-[90%]">
    <div class="flex min-w-0 flex-col items-end gap-[3px]">
      <p class="sr-only">You</p>
      <p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_18px_6px_18px] bg-cb-accent px-[13px] pt-2 pb-[7px] text-white [overflow-wrap:anywhere]"><span>Already packed, along with the spare batteries.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-cb-accent-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-cb-accent px-[13px] pt-2 pb-[7px] 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-cb-accent-soft" datetime="2026-10-14T09:12">09:12</time></p>
      <p class="mt-0.5 flex items-center gap-1 text-[.72rem] text-cb-muted"><svg class="text-cb-accent" width="16" height="12" viewBox="0 0 16 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 6.5 4.5 10 11 2.5M7.5 9.5 8 10l6.5-7.5"/></svg>Read 09:14</p>
    </div>
  </div>
</div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-cb-muted uppercase">Compact rows for channels</span>
<div class="flex flex-col gap-0.5 rounded-[22px] border border-cb-line bg-white px-3 py-[18px] text-[.95rem] leading-[1.45] text-cb-ink shadow-[0_12px_32px_-18px_rgba(27,31,42,.25)]" role="log" aria-live="polite" aria-label="Channel design-review">
  <div class="grid grid-cols-[40px_1fr] gap-x-3 rounded-[10px] px-2.5 py-1.5 hover:bg-[#f6f7fb]">
    <span class="mt-0.5 grid size-9 place-items-center rounded-[10px] bg-[#ccf2ea] text-[.72rem] font-bold text-[#0b5a4c]" aria-hidden="true">RK</span>
    <div><p class="flex items-baseline gap-2"><b class="font-bold">Ravi Kumar</b> <time class="text-[.72rem] text-cb-muted" datetime="2026-10-14T10:02">10:02</time></p>
    <p>Pushed the new onboarding screens to the review board.</p></div>
  </div>
  <div class="grid grid-cols-[40px_1fr] gap-x-3 rounded-[10px] px-2.5 pt-px pb-1.5 hover:bg-[#f6f7fb]">
    <time class="self-center justify-self-center text-[.72rem] whitespace-nowrap text-cb-muted" datetime="2026-10-14T10:03">10:03</time>
    <p><span class="sr-only">Ravi Kumar: </span>The empty state on step 3 still needs copy.</p>
  </div>
  <div class="mt-2 grid grid-cols-[40px_1fr] gap-x-3 rounded-[10px] px-2.5 py-1.5 hover:bg-[#f6f7fb]">
    <span class="mt-0.5 grid size-9 place-items-center rounded-[10px] bg-[#fde7c4] text-[.72rem] font-bold text-[#7a4206]" aria-hidden="true">LO</span>
    <div><p class="flex items-baseline gap-2"><b class="font-bold">Lena Ortiz</b> <time class="text-[.72rem] text-cb-muted" datetime="2026-10-14T10:09">10:09</time></p>
    <p>I can write it. Is it for teams with no projects yet, or for invited members?</p></div>
  </div>
  <div class="mt-2 grid grid-cols-[40px_1fr] gap-x-3 rounded-[10px] px-2.5 py-1.5 hover:bg-[#f6f7fb]">
    <span class="mt-0.5 grid size-9 place-items-center rounded-[10px] bg-[#ccf2ea] text-[.72rem] font-bold text-[#0b5a4c]" aria-hidden="true">RK</span>
    <div><p class="flex items-baseline gap-2"><b class="font-bold">Ravi Kumar</b> <time class="text-[.72rem] text-cb-muted" datetime="2026-10-14T10:11">10:11</time></p>
    <p>Invited members. They land there before anyone has shared a project with them.</p></div>
  </div>
  <div class="grid grid-cols-[40px_1fr] gap-x-3 rounded-[10px] px-2.5 pt-px pb-1.5 hover:bg-[#f6f7fb]">
    <time class="self-center justify-self-center text-[.72rem] whitespace-nowrap text-cb-muted" datetime="2026-10-14T10:11">10:11</time>
    <p><span class="sr-only">Ravi Kumar: </span>Thanks, Lena.</p>
  </div>
</div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-cb-muted uppercase">Files, link previews, notices and failed messages</span>
<div class="flex flex-col gap-3.5 rounded-[22px] border border-cb-line bg-white px-5 py-6 text-[.95rem] leading-[1.45] text-cb-ink shadow-[0_12px_32px_-18px_rgba(27,31,42,.25)] max-[480px]:px-3 max-[480px]:py-[18px]" role="log" aria-live="polite" aria-label="Chat with the Lisbon shoot group">
  <p class="self-center rounded-full bg-cb-in px-3 py-[5px] text-center text-[.78rem] text-cb-muted">Lena Ortiz added Tom Becker to the chat</p>
  <div class="flex max-w-[min(82%,520px)] items-end gap-2.5 max-[480px]:max-w-[90%]">
    <span class="grid size-8 flex-none place-items-center rounded-full bg-[#dbe4ff] text-[.72rem] font-bold text-[#2f2a8f] max-[480px]:size-7" aria-hidden="true">LO</span>
    <div class="flex min-w-0 flex-col items-start gap-[3px]">
      <p class="mb-[3px] ml-3 text-[.78rem] font-semibold text-cb-muted">Lena Ortiz</p>
      <div class="flex w-[300px] max-w-full flex-col gap-2 rounded-[18px_18px_18px_6px] bg-cb-in p-2.5">
        <div class="flex items-center gap-3 rounded-xl bg-white px-3 py-2.5">
          <svg class="flex-none text-cb-accent" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5M9 13h6M9 17h4"/></svg>
          <span class="flex min-w-0 flex-col text-[.88rem] [overflow-wrap:anywhere]">shoot-schedule-v3.pdf <small class="text-[.76rem] text-cb-muted">PDF, 1.2 MB</small></span>
          <a class="ml-auto grid size-[34px] flex-none place-items-center rounded-[10px] bg-cb-accent-soft text-cb-accent hover:bg-[#c7d2fe] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cb-accent" href="#"><svg width="16" height="16" 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 4v11M7 10.5l5 5 5-5M5 20h14"/></svg><span class="sr-only">Download shoot-schedule-v3.pdf</span></a>
        </div>
        <time class="ml-auto text-[.7rem] whitespace-nowrap text-cb-muted" datetime="2026-10-14T11:20">11:20</time>
      </div>
      <p class="flex flex-wrap items-end gap-x-2.5 rounded-[6px_18px_18px_6px] bg-cb-in px-[13px] pt-2 pb-[7px] [overflow-wrap:anywhere]"><span class="text-cb-muted italic">This message was deleted</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-cb-muted" datetime="2026-10-14T11:21">11:21</time></p>
    </div>
  </div>
  <div class="flex max-w-[min(82%,520px)] self-end max-[480px]:max-w-[90%]">
    <div class="flex min-w-0 flex-col items-end gap-[3px]">
      <p class="sr-only">You</p>
      <div class="flex w-[300px] max-w-full flex-col gap-2 rounded-[18px_18px_6px_18px] bg-cb-accent p-2.5 text-white">
        <span>Sunset is at 19:04 that day:</span>
        <a class="group block rounded-[10px] border-l-[3px] border-cb-accent-soft bg-[#3f37c9] px-3 py-2.5 text-inherit no-underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white" href="#"><strong class="block text-[.9rem] group-hover:underline">Golden hour times for Lisbon</strong><span class="mt-0.5 block text-[.78rem] text-cb-accent-soft">sunwatch.example</span></a>
        <time class="ml-auto text-[.7rem] whitespace-nowrap text-cb-accent-soft" datetime="2026-10-14T11:24">11:24</time>
      </div>
      <p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_6px_6px_18px] border-[1.5px] border-cb-danger bg-white px-[13px] pt-2 pb-[7px] text-cb-ink [overflow-wrap:anywhere]"><span>Tom, can you check the drone permit?</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-cb-muted" datetime="2026-10-14T11:25">11:25</time></p>
      <p class="mt-0.5 flex items-center gap-[5px] text-[.74rem] font-semibold text-cb-danger"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v6M12 16.5v.5"/></svg>Not delivered</p>
    </div>
  </div>
</div></div>
CSS
/* Chat palette as theme colours: bg-cb-accent, text-cb-muted and so on. */
@theme {
  --color-cb-accent:#4f46e5; --color-cb-accent-soft:#e0e7ff; --color-cb-in:#eef0f6; --color-cb-ink:#1b1f2a;
  --color-cb-muted:#5d6475; --color-cb-line:#e2e5ee; --color-cb-danger:#b42318;
}

About this set

Three message threads that cover the static side of a chat interface. Bubble thread with date separators and read receipts groups consecutive messages from the same sender, keeps the tight corner on the sender’s side, puts each message’s time inside its bubble and writes the receipt as text, Read 09:14, under your last message. Compact rows for channels is the team-chat layout: square initials avatars, a name and time line, and continuation rows that show the time in the gutter. Files, link previews, notices and failed messages adds a system notice, a file card with a download link, a link preview inside a sent bubble, a deleted message and a message that failed, marked by a red border and the words Not delivered. Use the bubble thread for one-to-one chat, the rows for busy channels and the third block for the edge cases every chat runs into sooner or later. Each thread is a div with role=”log” and aria-live=”polite”, each message has a sender name, visually hidden for your own messages, and a time element with a datetime attribute. None of it needs JavaScript.

What’s included

  • Grouped bubbles with the tight corner on the sender's side
  • A time element with a datetime attribute on every message
  • Read, delivered and failed states written as text, not colour alone
  • Date separators, system notices, file cards and link previews
  • No JavaScript; each thread is a role="log" region

Accessibility

Each thread is a role="log" region with aria-live="polite" and a name such as Chat with Maya Chen, so only messages added later are announced. Every message has a sender name, visually hidden for your own messages, and a time element with a machine-readable datetime. Receipts and the failed state are written out as text, the download link has a hidden file name, and links show a 2px focus outline.

Customise it

Change --cb-accent, --cb-in, --cb-r and --cb-r-tight on .cb for colour and bubble shape. In Bootstrap the same --cb-* variables sit on the .card; in Tailwind edit the --color-cb-* theme colours and the rounded-[…] utilities.