Open demo

CSS code

HTML
<div><span class="lbl">Indented thread with collapse buttons and inline replies</span>
<section class="tc" data-thread aria-labelledby="tc-a-h">
  <h2 class="tc-h" id="tc-a-h">Discussion <span class="tc-hn">· 5 comments</span></h2>
  <ol class="tc-list">
<li class="tc-item" data-comment data-author="Priya Raman">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c1" aria-hidden="true">PR</span><p class="tc-who"><b>Priya Raman</b> <time class="tc-time" datetime="2026-10-19T09:10">3 hours ago</time></p></div>
    <p class="tc-body">Two seasons in, my no-dig beds hold water far better through August. The trade-off is slugs: the mulch layer gives them somewhere to hide.</p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to Priya Raman</span></button><button class="tc-act" type="button" data-collapse aria-expanded="true" aria-controls="tc-a-r1"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 3 replies</span></button></div>
    <div data-reply-slot></div>
  </article>
  <ol class="tc-replies" id="tc-a-r1" data-replies>
<li class="tc-item" data-comment data-author="Tomás Ferreira">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c2" aria-hidden="true">TF</span><p class="tc-who"><b>Tomás Ferreira</b> <time class="tc-time" datetime="2026-10-19T10:02">2 hours ago</time></p></div>
    <p class="tc-body">Same here. I switched from straw to sieved compost on top and the slug damage dropped by about half.</p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to Tomás Ferreira</span></button><button class="tc-act" type="button" data-collapse aria-expanded="true" aria-controls="tc-a-r2"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 1 reply</span></button></div>
    <div data-reply-slot></div>
  </article>
  <ol class="tc-replies" id="tc-a-r2" data-replies>
<li class="tc-item" data-comment data-author="Priya Raman">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c1" aria-hidden="true">PR</span><p class="tc-who"><b>Priya Raman</b> <time class="tc-time" datetime="2026-10-19T11:05">1 hour ago</time></p></div>
    <p class="tc-body">Sieved compost is a good tip. Did you buy it in, or is it from your own heaps?</p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to Priya Raman</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
</li>
<li class="tc-item" data-comment data-author="Hana Okafor">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c3" aria-hidden="true">HO</span><p class="tc-who"><b>Hana Okafor</b> <time class="tc-time" datetime="2026-10-19T11:20">45 minutes ago</time></p></div>
    <p class="tc-body">Beer traps around the edges worked for our plot, as long as someone empties them every two days.</p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to Hana Okafor</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
</li>
<li class="tc-item" data-comment data-author="Jonas Berg">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c4" aria-hidden="true">JB</span><p class="tc-who"><b>Jonas Berg</b> <time class="tc-time" datetime="2026-10-19T07:15">5 hours ago</time></p></div>
    <p class="tc-body">Does anyone dig at all any more? I still turn my clay once in autumn and the beds seem fine.</p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to Jonas Berg</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
  <p class="tc-sr" role="status" data-announce></p>
  <template data-tpl="form"><form class="tc-form" data-reply-form><label class="tc-form-l">Reply</label><textarea class="tc-field" rows="3" required></textarea><div class="tc-form-a"><button class="tc-btn ghost" type="button" data-cancel>Cancel</button><button class="tc-btn" type="submit">Post reply</button></div></form></template>
  <template data-tpl="reply"><li class="tc-item" data-comment data-author="You">
  <article class="tc-c" tabindex="-1">
    <div class="tc-head"><span class="tc-av me" aria-hidden="true">YO</span><p class="tc-who"><b>You</b> <time class="tc-time" data-time>Just now</time></p></div>
    <p class="tc-body" data-text></p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to your comment</span></button></div>
    <div data-reply-slot></div>
  </article>
</li></template>
  <template data-tpl="list"><ol class="tc-replies" data-replies></ol></template>
  <template data-tpl="toggle"><button class="tc-act" type="button" data-collapse aria-expanded="true" data-into="[data-actions]"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 1 reply</span></button></template>
</section></div>

<div><span class="lbl">Compact thread: click a thread line to collapse it</span>
<section class="tc tc-k" data-thread aria-labelledby="tc-b-h">
  <h2 class="tc-h" id="tc-b-h">Release 4.2: offline mode <span class="tc-hn">· 6 comments</span></h2>
  <ol class="tc-list">
<li class="tc-item" data-comment data-author="Dev Patel">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c5" aria-hidden="true">DP</span><p class="tc-who"><b>Dev Patel</b> <time class="tc-time" datetime="2026-10-17T08:40">2 days ago</time></p></div>
    <p class="tc-body">Offline mode finally works on the train. Sync conflicts now keep both versions instead of silently picking one, which is exactly right.</p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to Dev Patel</span></button><button class="tc-act tc-more" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r1"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 4 replies</span></button></div>
    <div data-reply-slot></div>
  </article><button class="tc-rail" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r1"><span class="tc-rail-l"></span><span class="tc-sr" data-label>Hide 4 replies</span></button>
  <ol class="tc-replies" id="tc-b-r1" data-replies>
<li class="tc-item" data-comment data-author="Lena Vogt">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av me" aria-hidden="true">LV</span><p class="tc-who"><b>Lena Vogt</b> <span class="tc-badge">Staff</span> <time class="tc-time" datetime="2026-10-17T10:12">2 days ago</time></p></div>
    <p class="tc-body">Thanks, Dev. Conflicting copies are kept for 30 days, then the older one moves to Trash.</p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to Lena Vogt</span></button><button class="tc-act tc-more" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r2"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 2 replies</span></button></div>
    <div data-reply-slot></div>
  </article><button class="tc-rail" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r2"><span class="tc-rail-l"></span><span class="tc-sr" data-label>Hide 2 replies</span></button>
  <ol class="tc-replies" id="tc-b-r2" data-replies>
<li class="tc-item" data-comment data-author="Dev Patel">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c5" aria-hidden="true">DP</span><p class="tc-who"><b>Dev Patel</b> <time class="tc-time" datetime="2026-10-18T07:30">1 day ago</time></p></div>
    <p class="tc-body">Thirty days is plenty. Could Trash show which device each copy came from?</p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to Dev Patel</span></button><button class="tc-act tc-more" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r3"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 1 reply</span></button></div>
    <div data-reply-slot></div>
  </article><button class="tc-rail" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r3"><span class="tc-rail-l"></span><span class="tc-sr" data-label>Hide 1 reply</span></button>
  <ol class="tc-replies" id="tc-b-r3" data-replies>
<li class="tc-item" data-comment data-author="Lena Vogt">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av me" aria-hidden="true">LV</span><p class="tc-who"><b>Lena Vogt</b> <span class="tc-badge">Staff</span> <time class="tc-time" datetime="2026-10-18T09:02">1 day ago</time></p></div>
    <p class="tc-body">Good idea. Logged as FN-2291 for the next release.</p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to Lena Vogt</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
</li>
  </ol>
</li>
<li class="tc-item" data-comment data-author="Kofi Mensah">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c4" aria-hidden="true">KM</span><p class="tc-who"><b>Kofi Mensah</b> <time class="tc-time" datetime="2026-10-18T13:45">1 day ago</time></p></div>
    <p class="tc-body">Any chance of offline search inside attachments? Text notes work, PDFs do not.</p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to Kofi Mensah</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
</li>
<li class="tc-item" data-comment data-author="Aiko Sato">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c2" aria-hidden="true">AS</span><p class="tc-who"><b>Aiko Sato</b> <time class="tc-time" datetime="2026-10-18T16:20">20 hours ago</time></p></div>
    <p class="tc-body">Export to Markdown keeps my tags as front matter. Small thing, big win.</p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to Aiko Sato</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
  <p class="tc-sr" role="status" data-announce></p>
  <template data-tpl="form"><form class="tc-form" data-reply-form><label class="tc-form-l">Reply</label><textarea class="tc-field" rows="3" required></textarea><div class="tc-form-a"><button class="tc-btn ghost" type="button" data-cancel>Cancel</button><button class="tc-btn" type="submit">Post reply</button></div></form></template>
  <template data-tpl="reply"><li class="tc-item" data-comment data-author="You">
  <article class="tc-c" tabindex="-1">
    <div class="tc-head"><span class="tc-av me" aria-hidden="true">YO</span><p class="tc-who"><b>You</b> <time class="tc-time" data-time>Just now</time></p></div>
    <p class="tc-body" data-text></p>
    <div class="tc-acts" data-actions><button class="tc-act" type="button" data-reply aria-expanded="false">Reply<span class="tc-sr"> to your comment</span></button></div>
    <div data-reply-slot></div>
  </article>
</li></template>
  <template data-tpl="list"><ol class="tc-replies" data-replies></ol></template>
  <template data-tpl="toggle"><button class="tc-rail" type="button" data-collapse aria-expanded="true"><span class="tc-rail-l"></span><span class="tc-sr" data-label>Hide 1 reply</span></button><button class="tc-act tc-more" type="button" data-collapse aria-expanded="true" data-into="[data-actions]"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 1 reply</span></button></template>
</section></div>
CSS
/* Threaded comments: nested lists joined by thread lines, collapse buttons with aria-expanded and "n replies" text, and an inline reply form. */
.tc{--tc-accent:#0f766e;--tc-accent-dark:#115e59;--tc-soft:#e3f3f0;--tc-ink:#14211f;--tc-muted:#4f5f5c;--tc-line:#dbe5e3;--tc-rail:#b9ccc8;--tc-ind:28px;padding:26px;border:1px solid var(--tc-line);border-radius:18px;background:#fff;color:var(--tc-ink);font-size:.94rem;line-height:1.55;box-shadow:0 18px 40px -28px rgba(20,33,31,.35)}
.tc.tc-k{font-size:.9rem;line-height:1.5}
.tc :where(ol){list-style:none;padding:0}
.tc-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
:where(.tc) :is(button,textarea){font:inherit;color:inherit}
.tc [hidden]{display:none!important}
.tc :where(h2,p,ol){margin:0}
.tc-h{margin-bottom:18px;font-size:1.1rem;font-weight:700;line-height:1.3}
.tc-hn{color:var(--tc-muted);font-weight:500}
.tc-list{display:flex;flex-direction:column;gap:22px;margin:0;padding:0}
.tc-k .tc-list{gap:16px}
.tc-item{position:relative}
.tc-c{position:relative;border-radius:8px}
.tc-c:focus-visible{outline:2px solid var(--tc-accent);outline-offset:4px}
.tc-head{display:flex;align-items:center;gap:10px}
.tc-av{position:relative;z-index:1;flex:none;display:grid;place-items:center;width:36px;height:36px;border-radius:50%;font-size:.74rem;font-weight:700}
.tc .c1{background:#fecdd3;color:#881337}
.tc .c2{background:#bae6fd;color:#0c4a6e}
.tc .c3{background:#d9f99d;color:#365314}
.tc .c4{background:#fde68a;color:#713f12}
.tc .c5{background:#ddd6fe;color:#4c1d95}
.tc .me{background:#99f6e4;color:#134e4a}
.tc-who{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 8px;font-size:.88rem}
.tc-who b{font-weight:700}
.tc-time{font-size:.8rem;color:var(--tc-muted)}
.tc-body{margin:6px 0 0 46px;text-wrap:pretty}
.tc-acts{display:flex;flex-wrap:wrap;gap:4px;margin:6px 0 0 38px}
.tc-chev{transition:transform .2s}
[aria-expanded=false]>.tc-chev{transform:rotate(-90deg)}
.tc-more[aria-expanded=true]{display:none}
.tc-k .tc-av{width:24px;height:24px;font-size:.6rem}
.tc-k .tc-body{margin:4px 0 0 32px}
.tc-k .tc-acts{margin:2px 0 0 24px}
.tc-rail{position:absolute;top:30px;bottom:0;left:2px;width:20px;padding:0;border:0;border-radius:6px;background:none;cursor:pointer}
.tc-rail[aria-expanded=false]{display:none}
.tc-rail-l{position:absolute;top:0;bottom:0;left:9px;width:2px;background:var(--tc-rail);transition:background-color .15s}
.tc-rail:hover .tc-rail-l,.tc-rail:focus-visible .tc-rail-l{background:var(--tc-accent)}
.tc-rail:focus-visible{outline:2px solid var(--tc-accent);outline-offset:0}
.tc-form{display:grid;gap:8px;margin:10px 0 0 46px}
.tc-k .tc-form{margin-left:32px}
.tc-form-l{margin:0;font-size:.8rem;font-weight:600;color:var(--tc-muted)}
.tc-form-a{display:flex;justify-content:flex-end;gap:8px}
/* thread lines: a curved connector into each reply, a straight rail between siblings, and a rail from the avatar down to the replies */
.tc-replies{display:flex;flex-direction:column;gap:18px;margin:18px 0 0 17px;padding-left:var(--tc-ind)}
.tc-replies>li::before{content:"";position:absolute;left:calc(-1 * var(--tc-ind));top:-18px;width:calc(var(--tc-ind) - 6px);height:36px;border:solid var(--tc-rail);border-width:0 0 2px 2px;border-bottom-left-radius:14px}
.tc-replies>li:not(:last-child)::after{content:"";position:absolute;left:calc(-1 * var(--tc-ind));top:0;bottom:-18px;border-left:2px solid var(--tc-rail)}
.tc-item:has(> .tc-replies:not([hidden]))>.tc-c::after{content:"";position:absolute;left:17px;top:40px;bottom:-18px;border-left:2px solid var(--tc-rail)}
/* compact variant: straight rails that are buttons */
.tc-k .tc-replies{gap:12px;margin:12px 0 0 11px;padding-left:20px}
.tc-k .tc-replies>li::before,.tc-k .tc-replies>li::after,.tc-k .tc-item>.tc-c::after{content:none}
@media (max-width:480px){.tc{--tc-ind:14px}}
@media (prefers-reduced-motion:reduce){.tc *{transition:none}}
.tc-act{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:0 8px;border:0;border-radius:8px;background:none;color:var(--tc-muted);font-size:.82rem;font-weight:600;cursor:pointer;transition:background-color .15s,color .15s}
.tc-act:hover,.tc-act[data-reply][aria-expanded=true]{background:var(--tc-soft);color:var(--tc-accent-dark)}
.tc-badge{align-self:center;padding:1px 8px;border-radius:999px;background:var(--tc-soft);color:var(--tc-accent-dark);font-size:.7rem;font-weight:700;letter-spacing:.02em}
.tc-field{width:100%;min-height:84px;padding:10px 12px;border:1.5px solid #7f918d;border-radius:10px;background:#fff;color:var(--tc-ink);resize:vertical}
.tc-field:focus{border-color:var(--tc-accent);outline:2px solid transparent;box-shadow:0 0 0 3px rgba(15,118,110,.22)}
.tc-btn{display:inline-flex;align-items:center;min-height:36px;padding:0 16px;border:0;border-radius:999px;background:var(--tc-accent);color:#fff;font-size:.86rem;font-weight:600;cursor:pointer}
.tc-btn:hover{background:var(--tc-accent-dark)}
.tc-btn.ghost{background:none;color:var(--tc-muted)}
.tc-btn.ghost:hover{background:var(--tc-soft)}
.tc :is(.tc-act,.tc-btn):focus-visible{outline:2px solid var(--tc-accent);outline-offset:1px}
@media (max-width:640px){.tc{padding:16px}}
JavaScript
/* Threaded comments: collapse buttons (aria-expanded, "n replies" text) and an inline reply form that posts into the thread.
   Frontend only: replace the part of the submit handler that builds the reply with a call to your backend. */
var tcId = 0;
document.querySelectorAll('[data-thread]').forEach(function (thread) {
  var live = thread.querySelector('[data-announce]');
  var tpl = function (name) { return thread.querySelector('template[data-tpl="' + name + '"]').content.cloneNode(true); };
  function label(btn) {
    var list = document.getElementById(btn.getAttribute('aria-controls')), n = list.querySelectorAll('[data-comment]').length;
    var open = btn.getAttribute('aria-expanded') === 'true';
    btn.querySelector('[data-label]').textContent = (open ? 'Hide ' : 'Show ') + n + (n === 1 ? ' reply' : ' replies');
  }
  function setOpen(id, open) {
    var list = document.getElementById(id);
    list.hidden = !open; list.parentElement.toggleAttribute('data-collapsed', !open);
    thread.querySelectorAll('[data-collapse][aria-controls="' + id + '"]').forEach(function (b) { b.setAttribute('aria-expanded', open); label(b); });
  }
  function closeForm(item, refocus) {
    var art = item.querySelector('article'), btn = art.querySelector('[data-reply]');
    art.querySelector('[data-reply-slot]').replaceChildren();
    btn.setAttribute('aria-expanded', 'false');
    if (refocus) btn.focus();
  }
  function openForm(item, btn) {
    var slot = item.querySelector('article [data-reply-slot]');
    if (slot.firstElementChild) { slot.querySelector('textarea').focus(); return; }
    var form = tpl('form').firstElementChild, field = form.querySelector('textarea'), id = 'tc-reply-' + (++tcId);
    form.querySelector('label').textContent = 'Reply to ' + item.dataset.author;
    form.querySelector('label').htmlFor = field.id = id;
    slot.appendChild(form); btn.setAttribute('aria-expanded', 'true'); field.focus();
    form.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeForm(item, true); });
    form.addEventListener('submit', function (e) {
      e.preventDefault();
      var text = field.value.trim();
      if (!text) { field.focus(); return; }
      var list = item.querySelector(':scope > [data-replies]');
      if (!list) {
        list = tpl('list').firstElementChild; list.id = 'tc-list-' + (++tcId); item.appendChild(list);
        Array.from(tpl('toggle').children).forEach(function (b) {
          b.setAttribute('aria-controls', list.id);
          if (b.dataset.into) item.querySelector('article ' + b.dataset.into).appendChild(b); else item.insertBefore(b, list);
        });
      }
      var reply = tpl('reply').firstElementChild, time = reply.querySelector('[data-time]'), now = new Date();
      reply.querySelector('[data-text]').textContent = text;
      time.dateTime = now.toISOString();
      list.appendChild(reply); setOpen(list.id, true);
      thread.querySelectorAll('[data-collapse]').forEach(label);
      closeForm(item, false); reply.querySelector('article').focus();
      live.textContent = 'Reply posted.';
    });
  }
  thread.addEventListener('click', function (e) {
    var b = e.target.closest('button');
    if (!b) return;
    if (b.matches('[data-collapse]')) {
      var id = b.getAttribute('aria-controls');
      setOpen(id, b.getAttribute('aria-expanded') !== 'true');
      if (!b.offsetParent) {
        var other = Array.from(thread.querySelectorAll('[data-collapse][aria-controls="' + id + '"]')).find(function (x) { return x.offsetParent; });
        if (other) other.focus();
      }
    } else if (b.matches('[data-reply]')) openForm(b.closest('[data-comment]'), b);
    else if (b.matches('[data-cancel]')) closeForm(b.closest('[data-comment]'), true);
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Indented thread with collapse buttons and inline replies</span>
<section class="card tc" data-thread aria-labelledby="tc-a-h">
  <h2 class="tc-h" id="tc-a-h">Discussion <span class="tc-hn">· 5 comments</span></h2>
  <ol class="list-unstyled tc-list">
<li class="tc-item" data-comment data-author="Priya Raman">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c1" aria-hidden="true">PR</span><p class="tc-who"><b>Priya Raman</b> <time class="tc-time" datetime="2026-10-19T09:10">3 hours ago</time></p></div>
    <p class="tc-body">Two seasons in, my no-dig beds hold water far better through August. The trade-off is slugs: the mulch layer gives them somewhere to hide.</p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to Priya Raman</span></button><button class="btn tc-act" type="button" data-collapse aria-expanded="true" aria-controls="tc-a-r1"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 3 replies</span></button></div>
    <div data-reply-slot></div>
  </article>
  <ol class="list-unstyled tc-replies" id="tc-a-r1" data-replies>
<li class="tc-item" data-comment data-author="Tomás Ferreira">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c2" aria-hidden="true">TF</span><p class="tc-who"><b>Tomás Ferreira</b> <time class="tc-time" datetime="2026-10-19T10:02">2 hours ago</time></p></div>
    <p class="tc-body">Same here. I switched from straw to sieved compost on top and the slug damage dropped by about half.</p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to Tomás Ferreira</span></button><button class="btn tc-act" type="button" data-collapse aria-expanded="true" aria-controls="tc-a-r2"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 1 reply</span></button></div>
    <div data-reply-slot></div>
  </article>
  <ol class="list-unstyled tc-replies" id="tc-a-r2" data-replies>
<li class="tc-item" data-comment data-author="Priya Raman">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c1" aria-hidden="true">PR</span><p class="tc-who"><b>Priya Raman</b> <time class="tc-time" datetime="2026-10-19T11:05">1 hour ago</time></p></div>
    <p class="tc-body">Sieved compost is a good tip. Did you buy it in, or is it from your own heaps?</p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to Priya Raman</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
</li>
<li class="tc-item" data-comment data-author="Hana Okafor">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c3" aria-hidden="true">HO</span><p class="tc-who"><b>Hana Okafor</b> <time class="tc-time" datetime="2026-10-19T11:20">45 minutes ago</time></p></div>
    <p class="tc-body">Beer traps around the edges worked for our plot, as long as someone empties them every two days.</p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to Hana Okafor</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
</li>
<li class="tc-item" data-comment data-author="Jonas Berg">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c4" aria-hidden="true">JB</span><p class="tc-who"><b>Jonas Berg</b> <time class="tc-time" datetime="2026-10-19T07:15">5 hours ago</time></p></div>
    <p class="tc-body">Does anyone dig at all any more? I still turn my clay once in autumn and the beds seem fine.</p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to Jonas Berg</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
  <p class="visually-hidden" role="status" data-announce></p>
  <template data-tpl="form"><form class="tc-form" data-reply-form><label class="form-label tc-form-l">Reply</label><textarea class="form-control tc-field" rows="3" required></textarea><div class="tc-form-a"><button class="btn tc-ghost" type="button" data-cancel>Cancel</button><button class="btn tc-btn" type="submit">Post reply</button></div></form></template>
  <template data-tpl="reply"><li class="tc-item" data-comment data-author="You">
  <article class="tc-c" tabindex="-1">
    <div class="tc-head"><span class="tc-av me" aria-hidden="true">YO</span><p class="tc-who"><b>You</b> <time class="tc-time" data-time>Just now</time></p></div>
    <p class="tc-body" data-text></p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to your comment</span></button></div>
    <div data-reply-slot></div>
  </article>
</li></template>
  <template data-tpl="list"><ol class="list-unstyled tc-replies" data-replies></ol></template>
  <template data-tpl="toggle"><button class="btn tc-act" type="button" data-collapse aria-expanded="true" data-into="[data-actions]"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 1 reply</span></button></template>
</section></div>

<div><span class="lbl">Compact thread: click a thread line to collapse it</span>
<section class="card tc tc-k" data-thread aria-labelledby="tc-b-h">
  <h2 class="tc-h" id="tc-b-h">Release 4.2: offline mode <span class="tc-hn">· 6 comments</span></h2>
  <ol class="list-unstyled tc-list">
<li class="tc-item" data-comment data-author="Dev Patel">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c5" aria-hidden="true">DP</span><p class="tc-who"><b>Dev Patel</b> <time class="tc-time" datetime="2026-10-17T08:40">2 days ago</time></p></div>
    <p class="tc-body">Offline mode finally works on the train. Sync conflicts now keep both versions instead of silently picking one, which is exactly right.</p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to Dev Patel</span></button><button class="btn tc-act tc-more" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r1"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 4 replies</span></button></div>
    <div data-reply-slot></div>
  </article><button class="tc-rail" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r1"><span class="tc-rail-l"></span><span class="visually-hidden" data-label>Hide 4 replies</span></button>
  <ol class="list-unstyled tc-replies" id="tc-b-r1" data-replies>
<li class="tc-item" data-comment data-author="Lena Vogt">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av me" aria-hidden="true">LV</span><p class="tc-who"><b>Lena Vogt</b> <span class="badge tc-badge">Staff</span> <time class="tc-time" datetime="2026-10-17T10:12">2 days ago</time></p></div>
    <p class="tc-body">Thanks, Dev. Conflicting copies are kept for 30 days, then the older one moves to Trash.</p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to Lena Vogt</span></button><button class="btn tc-act tc-more" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r2"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 2 replies</span></button></div>
    <div data-reply-slot></div>
  </article><button class="tc-rail" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r2"><span class="tc-rail-l"></span><span class="visually-hidden" data-label>Hide 2 replies</span></button>
  <ol class="list-unstyled tc-replies" id="tc-b-r2" data-replies>
<li class="tc-item" data-comment data-author="Dev Patel">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c5" aria-hidden="true">DP</span><p class="tc-who"><b>Dev Patel</b> <time class="tc-time" datetime="2026-10-18T07:30">1 day ago</time></p></div>
    <p class="tc-body">Thirty days is plenty. Could Trash show which device each copy came from?</p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to Dev Patel</span></button><button class="btn tc-act tc-more" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r3"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 1 reply</span></button></div>
    <div data-reply-slot></div>
  </article><button class="tc-rail" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r3"><span class="tc-rail-l"></span><span class="visually-hidden" data-label>Hide 1 reply</span></button>
  <ol class="list-unstyled tc-replies" id="tc-b-r3" data-replies>
<li class="tc-item" data-comment data-author="Lena Vogt">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av me" aria-hidden="true">LV</span><p class="tc-who"><b>Lena Vogt</b> <span class="badge tc-badge">Staff</span> <time class="tc-time" datetime="2026-10-18T09:02">1 day ago</time></p></div>
    <p class="tc-body">Good idea. Logged as FN-2291 for the next release.</p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to Lena Vogt</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
</li>
  </ol>
</li>
<li class="tc-item" data-comment data-author="Kofi Mensah">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c4" aria-hidden="true">KM</span><p class="tc-who"><b>Kofi Mensah</b> <time class="tc-time" datetime="2026-10-18T13:45">1 day ago</time></p></div>
    <p class="tc-body">Any chance of offline search inside attachments? Text notes work, PDFs do not.</p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to Kofi Mensah</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
</li>
<li class="tc-item" data-comment data-author="Aiko Sato">
  <article class="tc-c">
    <div class="tc-head"><span class="tc-av c2" aria-hidden="true">AS</span><p class="tc-who"><b>Aiko Sato</b> <time class="tc-time" datetime="2026-10-18T16:20">20 hours ago</time></p></div>
    <p class="tc-body">Export to Markdown keeps my tags as front matter. Small thing, big win.</p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to Aiko Sato</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
  <p class="visually-hidden" role="status" data-announce></p>
  <template data-tpl="form"><form class="tc-form" data-reply-form><label class="form-label tc-form-l">Reply</label><textarea class="form-control tc-field" rows="3" required></textarea><div class="tc-form-a"><button class="btn tc-ghost" type="button" data-cancel>Cancel</button><button class="btn tc-btn" type="submit">Post reply</button></div></form></template>
  <template data-tpl="reply"><li class="tc-item" data-comment data-author="You">
  <article class="tc-c" tabindex="-1">
    <div class="tc-head"><span class="tc-av me" aria-hidden="true">YO</span><p class="tc-who"><b>You</b> <time class="tc-time" data-time>Just now</time></p></div>
    <p class="tc-body" data-text></p>
    <div class="tc-acts" data-actions><button class="btn tc-act" type="button" data-reply aria-expanded="false">Reply<span class="visually-hidden"> to your comment</span></button></div>
    <div data-reply-slot></div>
  </article>
</li></template>
  <template data-tpl="list"><ol class="list-unstyled tc-replies" data-replies></ol></template>
  <template data-tpl="toggle"><button class="tc-rail" type="button" data-collapse aria-expanded="true"><span class="tc-rail-l"></span><span class="visually-hidden" data-label>Hide 1 reply</span></button><button class="btn tc-act tc-more" type="button" data-collapse aria-expanded="true" data-into="[data-actions]"><svg class="tc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 1 reply</span></button></template>
</section></div>
CSS
/* Threaded comments on Bootstrap: a .card with .list-unstyled lists, .btn actions, a .badge and a .form-control reply box.
   Nested lists are joined by thread lines; collapse buttons carry aria-expanded and "n replies" text. */
.tc{--tc-accent:#0f766e;--tc-accent-dark:#115e59;--tc-soft:#e3f3f0;--tc-ink:#14211f;--tc-muted:#4f5f5c;--tc-line:#dbe5e3;--tc-rail:#b9ccc8;--tc-ind:28px;--bs-card-border-color:var(--tc-line);--bs-card-border-radius:18px;--bs-card-color:var(--tc-ink);padding:26px;font-size:.94rem;line-height:1.55;box-shadow:0 18px 40px -28px rgba(20,33,31,.35)}
.tc.tc-k{font-size:.9rem;line-height:1.5}
.tc :where(h2,p,ol){margin:0}
.tc-h{margin-bottom:18px;font-size:1.1rem;font-weight:700;line-height:1.3}
.tc-hn{color:var(--tc-muted);font-weight:500}
.tc-list{display:flex;flex-direction:column;gap:22px;margin:0;padding:0}
.tc-k .tc-list{gap:16px}
.tc-item{position:relative}
.tc-c{position:relative;border-radius:8px}
.tc-c:focus-visible{outline:2px solid var(--tc-accent);outline-offset:4px}
.tc-head{display:flex;align-items:center;gap:10px}
.tc-av{position:relative;z-index:1;flex:none;display:grid;place-items:center;width:36px;height:36px;border-radius:50%;font-size:.74rem;font-weight:700}
.tc .c1{background:#fecdd3;color:#881337}
.tc .c2{background:#bae6fd;color:#0c4a6e}
.tc .c3{background:#d9f99d;color:#365314}
.tc .c4{background:#fde68a;color:#713f12}
.tc .c5{background:#ddd6fe;color:#4c1d95}
.tc .me{background:#99f6e4;color:#134e4a}
.tc-who{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 8px;font-size:.88rem}
.tc-who b{font-weight:700}
.tc-time{font-size:.8rem;color:var(--tc-muted)}
.tc-body{margin:6px 0 0 46px;text-wrap:pretty}
.tc-acts{display:flex;flex-wrap:wrap;gap:4px;margin:6px 0 0 38px}
.tc-chev{transition:transform .2s}
[aria-expanded=false]>.tc-chev{transform:rotate(-90deg)}
.tc-more[aria-expanded=true]{display:none}
.tc-k .tc-av{width:24px;height:24px;font-size:.6rem}
.tc-k .tc-body{margin:4px 0 0 32px}
.tc-k .tc-acts{margin:2px 0 0 24px}
.tc-rail{position:absolute;top:30px;bottom:0;left:2px;width:20px;padding:0;border:0;border-radius:6px;background:none;cursor:pointer}
.tc-rail[aria-expanded=false]{display:none}
.tc-rail-l{position:absolute;top:0;bottom:0;left:9px;width:2px;background:var(--tc-rail);transition:background-color .15s}
.tc-rail:hover .tc-rail-l,.tc-rail:focus-visible .tc-rail-l{background:var(--tc-accent)}
.tc-rail:focus-visible{outline:2px solid var(--tc-accent);outline-offset:0}
.tc-form{display:grid;gap:8px;margin:10px 0 0 46px}
.tc-k .tc-form{margin-left:32px}
.tc-form-l{margin:0;font-size:.8rem;font-weight:600;color:var(--tc-muted)}
.tc-form-a{display:flex;justify-content:flex-end;gap:8px}
/* thread lines: a curved connector into each reply, a straight rail between siblings, and a rail from the avatar down to the replies */
.tc-replies{display:flex;flex-direction:column;gap:18px;margin:18px 0 0 17px;padding-left:var(--tc-ind)}
.tc-replies>li::before{content:"";position:absolute;left:calc(-1 * var(--tc-ind));top:-18px;width:calc(var(--tc-ind) - 6px);height:36px;border:solid var(--tc-rail);border-width:0 0 2px 2px;border-bottom-left-radius:14px}
.tc-replies>li:not(:last-child)::after{content:"";position:absolute;left:calc(-1 * var(--tc-ind));top:0;bottom:-18px;border-left:2px solid var(--tc-rail)}
.tc-item:has(> .tc-replies:not([hidden]))>.tc-c::after{content:"";position:absolute;left:17px;top:40px;bottom:-18px;border-left:2px solid var(--tc-rail)}
/* compact variant: straight rails that are buttons */
.tc-k .tc-replies{gap:12px;margin:12px 0 0 11px;padding-left:20px}
.tc-k .tc-replies>li::before,.tc-k .tc-replies>li::after,.tc-k .tc-item>.tc-c::after{content:none}
@media (max-width:480px){.tc{--tc-ind:14px}}
@media (prefers-reduced-motion:reduce){.tc *{transition:none}}
.tc .btn{--bs-btn-focus-box-shadow:none}
.tc-act{--bs-btn-padding-x:8px;--bs-btn-padding-y:0;--bs-btn-font-size:.82rem;--bs-btn-font-weight:600;--bs-btn-border-width:0;--bs-btn-border-radius:8px;--bs-btn-color:var(--tc-muted);--bs-btn-hover-color:var(--tc-accent-dark);--bs-btn-hover-bg:var(--tc-soft);--bs-btn-active-color:var(--tc-accent-dark);--bs-btn-active-bg:var(--tc-soft);display:inline-flex;align-items:center;gap:6px;min-height:32px}
.tc-act[data-reply][aria-expanded=true]{background:var(--tc-soft);color:var(--tc-accent-dark)}
.tc-badge{--bs-badge-padding-x:8px;--bs-badge-padding-y:2px;--bs-badge-font-size:.7rem;--bs-badge-color:var(--tc-accent-dark);--bs-badge-border-radius:999px;align-self:center;background:var(--tc-soft);letter-spacing:.02em}
.tc-field{min-height:84px;padding:10px 12px;border:1.5px solid #7f918d;border-radius:10px;color:var(--tc-ink);font-size:inherit}
.tc-field:focus{border-color:var(--tc-accent);box-shadow:0 0 0 3px rgba(15,118,110,.22)}
.tc-btn,.tc-ghost{--bs-btn-padding-x:16px;--bs-btn-padding-y:0;--bs-btn-font-size:.86rem;--bs-btn-font-weight:600;--bs-btn-border-width:0;--bs-btn-border-radius:999px;display:inline-flex;align-items:center;min-height:36px}
.tc-btn{--bs-btn-color:#fff;--bs-btn-bg:var(--tc-accent);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--tc-accent-dark);--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--tc-accent-dark)}
.tc-ghost{--bs-btn-color:var(--tc-muted);--bs-btn-hover-color:var(--tc-muted);--bs-btn-hover-bg:var(--tc-soft);--bs-btn-active-bg:var(--tc-soft)}
.tc .btn:focus-visible{outline:2px solid var(--tc-accent);outline-offset:1px}
@media (max-width:640px){.tc{padding:16px}}
JavaScript
/* Threaded comments: collapse buttons (aria-expanded, "n replies" text) and an inline reply form that posts into the thread.
   Frontend only: replace the part of the submit handler that builds the reply with a call to your backend. */
var tcId = 0;
document.querySelectorAll('[data-thread]').forEach(function (thread) {
  var live = thread.querySelector('[data-announce]');
  var tpl = function (name) { return thread.querySelector('template[data-tpl="' + name + '"]').content.cloneNode(true); };
  function label(btn) {
    var list = document.getElementById(btn.getAttribute('aria-controls')), n = list.querySelectorAll('[data-comment]').length;
    var open = btn.getAttribute('aria-expanded') === 'true';
    btn.querySelector('[data-label]').textContent = (open ? 'Hide ' : 'Show ') + n + (n === 1 ? ' reply' : ' replies');
  }
  function setOpen(id, open) {
    var list = document.getElementById(id);
    list.hidden = !open; list.parentElement.toggleAttribute('data-collapsed', !open);
    thread.querySelectorAll('[data-collapse][aria-controls="' + id + '"]').forEach(function (b) { b.setAttribute('aria-expanded', open); label(b); });
  }
  function closeForm(item, refocus) {
    var art = item.querySelector('article'), btn = art.querySelector('[data-reply]');
    art.querySelector('[data-reply-slot]').replaceChildren();
    btn.setAttribute('aria-expanded', 'false');
    if (refocus) btn.focus();
  }
  function openForm(item, btn) {
    var slot = item.querySelector('article [data-reply-slot]');
    if (slot.firstElementChild) { slot.querySelector('textarea').focus(); return; }
    var form = tpl('form').firstElementChild, field = form.querySelector('textarea'), id = 'tc-reply-' + (++tcId);
    form.querySelector('label').textContent = 'Reply to ' + item.dataset.author;
    form.querySelector('label').htmlFor = field.id = id;
    slot.appendChild(form); btn.setAttribute('aria-expanded', 'true'); field.focus();
    form.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeForm(item, true); });
    form.addEventListener('submit', function (e) {
      e.preventDefault();
      var text = field.value.trim();
      if (!text) { field.focus(); return; }
      var list = item.querySelector(':scope > [data-replies]');
      if (!list) {
        list = tpl('list').firstElementChild; list.id = 'tc-list-' + (++tcId); item.appendChild(list);
        Array.from(tpl('toggle').children).forEach(function (b) {
          b.setAttribute('aria-controls', list.id);
          if (b.dataset.into) item.querySelector('article ' + b.dataset.into).appendChild(b); else item.insertBefore(b, list);
        });
      }
      var reply = tpl('reply').firstElementChild, time = reply.querySelector('[data-time]'), now = new Date();
      reply.querySelector('[data-text]').textContent = text;
      time.dateTime = now.toISOString();
      list.appendChild(reply); setOpen(list.id, true);
      thread.querySelectorAll('[data-collapse]').forEach(label);
      closeForm(item, false); reply.querySelector('article').focus();
      live.textContent = 'Reply posted.';
    });
  }
  thread.addEventListener('click', function (e) {
    var b = e.target.closest('button');
    if (!b) return;
    if (b.matches('[data-collapse]')) {
      var id = b.getAttribute('aria-controls');
      setOpen(id, b.getAttribute('aria-expanded') !== 'true');
      if (!b.offsetParent) {
        var other = Array.from(thread.querySelectorAll('[data-collapse][aria-controls="' + id + '"]')).find(function (x) { return x.offsetParent; });
        if (other) other.focus();
      }
    } else if (b.matches('[data-reply]')) openForm(b.closest('[data-comment]'), b);
    else if (b.matches('[data-cancel]')) closeForm(b.closest('[data-comment]'), true);
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4f5f5c] uppercase">Indented thread with collapse buttons and inline replies</span>
<section class="tc rounded-[18px] border border-tc-line bg-white p-[26px] text-[.94rem] leading-[1.55] text-tc-ink shadow-[0_18px_40px_-28px_rgba(20,33,31,.35)] max-sm:p-4" data-thread aria-labelledby="tc-a-h">
  <h2 class="mb-[18px] text-[1.1rem] leading-[1.3] font-bold" id="tc-a-h">Discussion <span class="font-medium text-tc-muted">· 5 comments</span></h2>
  <ol class="flex flex-col gap-[22px]">
<li class="relative" data-comment data-author="Priya Raman">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#fecdd3] text-[#881337]" aria-hidden="true">PR</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>Priya Raman</b> <time class="text-[.8rem] text-tc-muted" datetime="2026-10-19T09:10">3 hours ago</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1">Two seasons in, my no-dig beds hold water far better through August. The trade-off is slugs: the mulch layer gives them somewhere to hide.</p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to Priya Raman</span></button><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-collapse aria-expanded="true" aria-controls="tc-a-r1"><svg class="transition-transform duration-200 motion-reduce:transition-none [[aria-expanded=false]>&]:-rotate-90" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 3 replies</span></button></div>
    <div data-reply-slot></div>
  </article>
  <ol class="mt-[18px] ml-[17px] flex flex-col gap-[18px] pl-[var(--tc-ind)] [--tc-ind:28px] max-[480px]:[--tc-ind:14px]" id="tc-a-r1" data-replies>
<li class="relative" data-comment data-author="Tomás Ferreira">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#bae6fd] text-[#0c4a6e]" aria-hidden="true">TF</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>Tomás Ferreira</b> <time class="text-[.8rem] text-tc-muted" datetime="2026-10-19T10:02">2 hours ago</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1">Same here. I switched from straw to sieved compost on top and the slug damage dropped by about half.</p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to Tomás Ferreira</span></button><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-collapse aria-expanded="true" aria-controls="tc-a-r2"><svg class="transition-transform duration-200 motion-reduce:transition-none [[aria-expanded=false]>&]:-rotate-90" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 1 reply</span></button></div>
    <div data-reply-slot></div>
  </article>
  <ol class="mt-[18px] ml-[17px] flex flex-col gap-[18px] pl-[var(--tc-ind)] [--tc-ind:28px] max-[480px]:[--tc-ind:14px]" id="tc-a-r2" data-replies>
<li class="relative" data-comment data-author="Priya Raman">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#fecdd3] text-[#881337]" aria-hidden="true">PR</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>Priya Raman</b> <time class="text-[.8rem] text-tc-muted" datetime="2026-10-19T11:05">1 hour ago</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1">Sieved compost is a good tip. Did you buy it in, or is it from your own heaps?</p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to Priya Raman</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
</li>
<li class="relative" data-comment data-author="Hana Okafor">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#d9f99d] text-[#365314]" aria-hidden="true">HO</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>Hana Okafor</b> <time class="text-[.8rem] text-tc-muted" datetime="2026-10-19T11:20">45 minutes ago</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1">Beer traps around the edges worked for our plot, as long as someone empties them every two days.</p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to Hana Okafor</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
</li>
<li class="relative" data-comment data-author="Jonas Berg">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#fde68a] text-[#713f12]" aria-hidden="true">JB</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>Jonas Berg</b> <time class="text-[.8rem] text-tc-muted" datetime="2026-10-19T07:15">5 hours ago</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1">Does anyone dig at all any more? I still turn my clay once in autumn and the beds seem fine.</p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to Jonas Berg</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
  <p class="sr-only" role="status" data-announce></p>
  <template data-tpl="form"><form class="mt-2.5 ml-[46px] grid gap-2 [.tc-k_&]:ml-8" data-reply-form><label class="text-[.8rem] font-semibold text-tc-muted">Reply</label><textarea class="min-h-[84px] w-full resize-y rounded-[10px] border-[1.5px] border-[#7f918d] bg-white px-3 py-2.5 text-tc-ink focus:border-tc-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.22)] focus:outline-2 focus:outline-transparent" rows="3" required></textarea><div class="flex justify-end gap-2"><button class="inline-flex min-h-9 cursor-pointer items-center rounded-full px-4 text-[.86rem] font-semibold text-tc-muted hover:bg-tc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tc-accent" type="button" data-cancel>Cancel</button><button class="inline-flex min-h-9 cursor-pointer items-center rounded-full bg-tc-accent px-4 text-[.86rem] font-semibold text-white hover:bg-tc-accent-dark focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tc-accent" type="submit">Post reply</button></div></form></template>
  <template data-tpl="reply"><li class="relative" data-comment data-author="You">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent" tabindex="-1">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#99f6e4] text-[#134e4a]" aria-hidden="true">YO</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>You</b> <time class="text-[.8rem] text-tc-muted" data-time>Just now</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1" data-text></p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to your comment</span></button></div>
    <div data-reply-slot></div>
  </article>
</li></template>
  <template data-tpl="list"><ol class="mt-[18px] ml-[17px] flex flex-col gap-[18px] pl-[var(--tc-ind)] [--tc-ind:28px] max-[480px]:[--tc-ind:14px]" data-replies></ol></template>
  <template data-tpl="toggle"><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-collapse aria-expanded="true" data-into="[data-actions]"><svg class="transition-transform duration-200 motion-reduce:transition-none [[aria-expanded=false]>&]:-rotate-90" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 1 reply</span></button></template>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4f5f5c] uppercase">Compact thread: click a thread line to collapse it</span>
<section class="tc tc-k rounded-[18px] border border-tc-line bg-white p-[26px] text-[.9rem] leading-[1.5] text-tc-ink shadow-[0_18px_40px_-28px_rgba(20,33,31,.35)] max-sm:p-4" data-thread aria-labelledby="tc-b-h">
  <h2 class="mb-[18px] text-[1.1rem] leading-[1.3] font-bold" id="tc-b-h">Release 4.2: offline mode <span class="font-medium text-tc-muted">· 6 comments</span></h2>
  <ol class="flex flex-col gap-4">
<li class="relative" data-comment data-author="Dev Patel">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#ddd6fe] text-[#4c1d95]" aria-hidden="true">DP</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>Dev Patel</b> <time class="text-[.8rem] text-tc-muted" datetime="2026-10-17T08:40">2 days ago</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1">Offline mode finally works on the train. Sync conflicts now keep both versions instead of silently picking one, which is exactly right.</p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to Dev Patel</span></button><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent aria-expanded:hidden" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r1"><svg class="transition-transform duration-200 motion-reduce:transition-none [[aria-expanded=false]>&]:-rotate-90" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 4 replies</span></button></div>
    <div data-reply-slot></div>
  </article><button class="group/r absolute top-[30px] bottom-0 left-0.5 w-5 cursor-pointer rounded-md aria-[expanded=false]:hidden focus-visible:outline-2 focus-visible:outline-tc-accent" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r1"><span class="absolute top-0 bottom-0 left-[9px] w-0.5 bg-tc-rail transition-colors duration-150 group-hover/r:bg-tc-accent group-focus-visible/r:bg-tc-accent motion-reduce:transition-none"></span><span class="sr-only" data-label>Hide 4 replies</span></button>
  <ol class="mt-3 ml-[11px] flex flex-col gap-3 pl-5" id="tc-b-r1" data-replies>
<li class="relative" data-comment data-author="Lena Vogt">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#99f6e4] text-[#134e4a]" aria-hidden="true">LV</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>Lena Vogt</b> <span class="self-center rounded-full bg-tc-soft px-2 py-px text-[.7rem] font-bold tracking-[.02em] text-tc-accent-dark">Staff</span> <time class="text-[.8rem] text-tc-muted" datetime="2026-10-17T10:12">2 days ago</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1">Thanks, Dev. Conflicting copies are kept for 30 days, then the older one moves to Trash.</p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to Lena Vogt</span></button><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent aria-expanded:hidden" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r2"><svg class="transition-transform duration-200 motion-reduce:transition-none [[aria-expanded=false]>&]:-rotate-90" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 2 replies</span></button></div>
    <div data-reply-slot></div>
  </article><button class="group/r absolute top-[30px] bottom-0 left-0.5 w-5 cursor-pointer rounded-md aria-[expanded=false]:hidden focus-visible:outline-2 focus-visible:outline-tc-accent" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r2"><span class="absolute top-0 bottom-0 left-[9px] w-0.5 bg-tc-rail transition-colors duration-150 group-hover/r:bg-tc-accent group-focus-visible/r:bg-tc-accent motion-reduce:transition-none"></span><span class="sr-only" data-label>Hide 2 replies</span></button>
  <ol class="mt-3 ml-[11px] flex flex-col gap-3 pl-5" id="tc-b-r2" data-replies>
<li class="relative" data-comment data-author="Dev Patel">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#ddd6fe] text-[#4c1d95]" aria-hidden="true">DP</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>Dev Patel</b> <time class="text-[.8rem] text-tc-muted" datetime="2026-10-18T07:30">1 day ago</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1">Thirty days is plenty. Could Trash show which device each copy came from?</p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to Dev Patel</span></button><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent aria-expanded:hidden" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r3"><svg class="transition-transform duration-200 motion-reduce:transition-none [[aria-expanded=false]>&]:-rotate-90" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 1 reply</span></button></div>
    <div data-reply-slot></div>
  </article><button class="group/r absolute top-[30px] bottom-0 left-0.5 w-5 cursor-pointer rounded-md aria-[expanded=false]:hidden focus-visible:outline-2 focus-visible:outline-tc-accent" type="button" data-collapse aria-expanded="true" aria-controls="tc-b-r3"><span class="absolute top-0 bottom-0 left-[9px] w-0.5 bg-tc-rail transition-colors duration-150 group-hover/r:bg-tc-accent group-focus-visible/r:bg-tc-accent motion-reduce:transition-none"></span><span class="sr-only" data-label>Hide 1 reply</span></button>
  <ol class="mt-3 ml-[11px] flex flex-col gap-3 pl-5" id="tc-b-r3" data-replies>
<li class="relative" data-comment data-author="Lena Vogt">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#99f6e4] text-[#134e4a]" aria-hidden="true">LV</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>Lena Vogt</b> <span class="self-center rounded-full bg-tc-soft px-2 py-px text-[.7rem] font-bold tracking-[.02em] text-tc-accent-dark">Staff</span> <time class="text-[.8rem] text-tc-muted" datetime="2026-10-18T09:02">1 day ago</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1">Good idea. Logged as FN-2291 for the next release.</p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to Lena Vogt</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
</li>
  </ol>
</li>
<li class="relative" data-comment data-author="Kofi Mensah">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#fde68a] text-[#713f12]" aria-hidden="true">KM</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>Kofi Mensah</b> <time class="text-[.8rem] text-tc-muted" datetime="2026-10-18T13:45">1 day ago</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1">Any chance of offline search inside attachments? Text notes work, PDFs do not.</p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to Kofi Mensah</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
</li>
<li class="relative" data-comment data-author="Aiko Sato">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#bae6fd] text-[#0c4a6e]" aria-hidden="true">AS</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>Aiko Sato</b> <time class="text-[.8rem] text-tc-muted" datetime="2026-10-18T16:20">20 hours ago</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1">Export to Markdown keeps my tags as front matter. Small thing, big win.</p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to Aiko Sato</span></button></div>
    <div data-reply-slot></div>
  </article>
</li>
  </ol>
  <p class="sr-only" role="status" data-announce></p>
  <template data-tpl="form"><form class="mt-2.5 ml-[46px] grid gap-2 [.tc-k_&]:ml-8" data-reply-form><label class="text-[.8rem] font-semibold text-tc-muted">Reply</label><textarea class="min-h-[84px] w-full resize-y rounded-[10px] border-[1.5px] border-[#7f918d] bg-white px-3 py-2.5 text-tc-ink focus:border-tc-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.22)] focus:outline-2 focus:outline-transparent" rows="3" required></textarea><div class="flex justify-end gap-2"><button class="inline-flex min-h-9 cursor-pointer items-center rounded-full px-4 text-[.86rem] font-semibold text-tc-muted hover:bg-tc-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tc-accent" type="button" data-cancel>Cancel</button><button class="inline-flex min-h-9 cursor-pointer items-center rounded-full bg-tc-accent px-4 text-[.86rem] font-semibold text-white hover:bg-tc-accent-dark focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tc-accent" type="submit">Post reply</button></div></form></template>
  <template data-tpl="reply"><li class="relative" data-comment data-author="You">
  <article class="tc-c relative rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tc-accent" tabindex="-1">
    <div class="flex items-center gap-2.5"><span class="relative z-[1] grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold [.tc-k_&]:size-6 [.tc-k_&]:text-[.6rem] bg-[#99f6e4] text-[#134e4a]" aria-hidden="true">YO</span><p class="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-[.88rem] [&_b]:font-bold"><b>You</b> <time class="text-[.8rem] text-tc-muted" data-time>Just now</time></p></div>
    <p class="mt-1.5 ml-[46px] text-pretty [.tc-k_&]:ml-8 [.tc-k_&]:mt-1" data-text></p>
    <div class="mt-1.5 ml-[38px] flex flex-wrap gap-1 [.tc-k_&]:ml-6 [.tc-k_&]:mt-0.5" data-actions><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent" type="button" data-reply aria-expanded="false">Reply<span class="sr-only"> to your comment</span></button></div>
    <div data-reply-slot></div>
  </article>
</li></template>
  <template data-tpl="list"><ol class="mt-3 ml-[11px] flex flex-col gap-3 pl-5" data-replies></ol></template>
  <template data-tpl="toggle"><button class="group/r absolute top-[30px] bottom-0 left-0.5 w-5 cursor-pointer rounded-md aria-[expanded=false]:hidden focus-visible:outline-2 focus-visible:outline-tc-accent" type="button" data-collapse aria-expanded="true"><span class="absolute top-0 bottom-0 left-[9px] w-0.5 bg-tc-rail transition-colors duration-150 group-hover/r:bg-tc-accent group-focus-visible/r:bg-tc-accent motion-reduce:transition-none"></span><span class="sr-only" data-label>Hide 1 reply</span></button><button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[.82rem] font-semibold text-tc-muted transition-colors duration-150 hover:bg-tc-soft hover:text-tc-accent-dark aria-expanded:[&[data-reply]]:bg-tc-soft aria-expanded:[&[data-reply]]:text-tc-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-tc-accent aria-expanded:hidden" type="button" data-collapse aria-expanded="true" data-into="[data-actions]"><svg class="transition-transform duration-200 motion-reduce:transition-none [[aria-expanded=false]>&]:-rotate-90" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span data-label>Hide 1 reply</span></button></template>
</section></div>
CSS
/* Palette as theme colours. The thread lines are pseudo-elements, so they live here as plain CSS on the data-replies lists;
   everything else is utilities in the markup (aria-expanded:, aria-[expanded=false]:, group-hover/r:, motion-reduce:). */
@theme {
  --color-tc-accent:#0f766e; --color-tc-accent-dark:#115e59; --color-tc-soft:#e3f3f0; --color-tc-ink:#14211f;
  --color-tc-muted:#4f5f5c; --color-tc-line:#dbe5e3; --color-tc-rail:#b9ccc8;
}
[data-replies]>li::before{content:"";position:absolute;left:calc(-1 * var(--tc-ind));top:-18px;width:calc(var(--tc-ind) - 6px);height:36px;border:solid var(--color-tc-rail);border-width:0 0 2px 2px;border-bottom-left-radius:14px}
[data-replies]>li:not(:last-child)::after{content:"";position:absolute;left:calc(-1 * var(--tc-ind));top:0;bottom:-18px;border-left:2px solid var(--color-tc-rail)}
[data-comment]:has(> [data-replies]:not([hidden]))>.tc-c::after{content:"";position:absolute;left:17px;top:40px;bottom:-18px;border-left:2px solid var(--color-tc-rail)}
.tc-k [data-replies]>li::before,.tc-k [data-replies]>li::after,.tc-k [data-comment]>.tc-c::after{content:none}
JavaScript
/* Threaded comments: collapse buttons (aria-expanded, "n replies" text) and an inline reply form that posts into the thread.
   Frontend only: replace the part of the submit handler that builds the reply with a call to your backend. */
var tcId = 0;
document.querySelectorAll('[data-thread]').forEach(function (thread) {
  var live = thread.querySelector('[data-announce]');
  var tpl = function (name) { return thread.querySelector('template[data-tpl="' + name + '"]').content.cloneNode(true); };
  function label(btn) {
    var list = document.getElementById(btn.getAttribute('aria-controls')), n = list.querySelectorAll('[data-comment]').length;
    var open = btn.getAttribute('aria-expanded') === 'true';
    btn.querySelector('[data-label]').textContent = (open ? 'Hide ' : 'Show ') + n + (n === 1 ? ' reply' : ' replies');
  }
  function setOpen(id, open) {
    var list = document.getElementById(id);
    list.hidden = !open; list.parentElement.toggleAttribute('data-collapsed', !open);
    thread.querySelectorAll('[data-collapse][aria-controls="' + id + '"]').forEach(function (b) { b.setAttribute('aria-expanded', open); label(b); });
  }
  function closeForm(item, refocus) {
    var art = item.querySelector('article'), btn = art.querySelector('[data-reply]');
    art.querySelector('[data-reply-slot]').replaceChildren();
    btn.setAttribute('aria-expanded', 'false');
    if (refocus) btn.focus();
  }
  function openForm(item, btn) {
    var slot = item.querySelector('article [data-reply-slot]');
    if (slot.firstElementChild) { slot.querySelector('textarea').focus(); return; }
    var form = tpl('form').firstElementChild, field = form.querySelector('textarea'), id = 'tc-reply-' + (++tcId);
    form.querySelector('label').textContent = 'Reply to ' + item.dataset.author;
    form.querySelector('label').htmlFor = field.id = id;
    slot.appendChild(form); btn.setAttribute('aria-expanded', 'true'); field.focus();
    form.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeForm(item, true); });
    form.addEventListener('submit', function (e) {
      e.preventDefault();
      var text = field.value.trim();
      if (!text) { field.focus(); return; }
      var list = item.querySelector(':scope > [data-replies]');
      if (!list) {
        list = tpl('list').firstElementChild; list.id = 'tc-list-' + (++tcId); item.appendChild(list);
        Array.from(tpl('toggle').children).forEach(function (b) {
          b.setAttribute('aria-controls', list.id);
          if (b.dataset.into) item.querySelector('article ' + b.dataset.into).appendChild(b); else item.insertBefore(b, list);
        });
      }
      var reply = tpl('reply').firstElementChild, time = reply.querySelector('[data-time]'), now = new Date();
      reply.querySelector('[data-text]').textContent = text;
      time.dateTime = now.toISOString();
      list.appendChild(reply); setOpen(list.id, true);
      thread.querySelectorAll('[data-collapse]').forEach(label);
      closeForm(item, false); reply.querySelector('article').focus();
      live.textContent = 'Reply posted.';
    });
  }
  thread.addEventListener('click', function (e) {
    var b = e.target.closest('button');
    if (!b) return;
    if (b.matches('[data-collapse]')) {
      var id = b.getAttribute('aria-controls');
      setOpen(id, b.getAttribute('aria-expanded') !== 'true');
      if (!b.offsetParent) {
        var other = Array.from(thread.querySelectorAll('[data-collapse][aria-controls="' + id + '"]')).find(function (x) { return x.offsetParent; });
        if (other) other.focus();
      }
    } else if (b.matches('[data-reply]')) openForm(b.closest('[data-comment]'), b);
    else if (b.matches('[data-cancel]')) closeForm(b.closest('[data-comment]'), true);
  });
});

About this set

Two nested comment threads built from ordered lists inside ordered lists, so the reply structure is in the markup and not only in the indentation. The indented thread draws a rail down from each avatar and a curved connector into every reply, and each comment with replies has a button such as Hide 3 replies; pressing it hides the nested list, flips aria-expanded and changes the text to Show 3 replies. The compact thread is the dense, Reddit-like layout with small avatars and a Staff badge: the thread line itself is the collapse button, and once a branch is collapsed a visible Show replies button takes its place and receives focus. Reply opens an inline form under that comment, labelled Reply to and the author’s name, with Cancel and Esc returning focus to the Reply button. Posting adds your reply to the right list, creating it and its collapse control if needed, updates every reply count, moves focus to the new comment and announces Reply posted. Use the indented version for article comments and the compact one for busy product or community threads.

What’s included

  • Nested ordered lists with rails and curved connectors drawn in CSS
  • Collapse buttons with aria-expanded and live "n replies" counts
  • Clickable thread lines in the compact variant
  • Inline reply form that posts into the thread and creates new branches
  • Esc and Cancel close the form and return focus to Reply

Accessibility

Replies are real nested lists, so screen readers announce the depth. Collapse controls are buttons with aria-expanded, aria-controls and text that says how many replies they hide; the thread-line button has visually hidden text. Reply buttons carry the author's name, the reply field is labelled, new replies receive focus and a polite status message, and every control has a 2px focus outline; transitions stop under prefers-reduced-motion.

Customise it

Change --tc-accent, --tc-rail and --tc-ind (the indent per level) on .tc. Bootstrap uses the same variables on the .card; in Tailwind edit the --color-tc-* theme colours and the [--tc-ind:28px] utility on each reply list.