Navigation

v0.0.4
Content

Thread

Comments and their replies, each joined by a line to the comment it answers.

On this page

A thread is comments and their replies: the discussion under a document, a review or an issue. A comment is an avatar on a column and its words beside it. Replies stand under the words, and a hairline runs down from the comment's avatar and turns into each of them, so a reader follows a line, not an indent, to see what answers what.

Thread

<div class="card max-w(480) mx-auto">  <ol class="thread">    <li class="thread-item">      <span class="thread-avatar">        <span class="avatar">AL</span>      </span>      <div class="thread-body">        <p class="thread-header">          <span class="thread-author">Ada Lovelace</span>          <time class="thread-meta" datetime="2026-11-12T09:12">2h</time>        </p>        <p class="thread-text">The focus ring is cut by the mask. Should it move to the group?</p>      </div>      <ol class="thread-replies">        <li class="thread-item">          <span class="thread-avatar">            <span class="avatar avatar-sm">AT</span>          </span>          <div class="thread-body">            <p class="thread-header">              <span class="thread-author">Alan Turing</span>              <time class="thread-meta" datetime="2026-11-12T10:05">1h</time>            </p>            <p class="thread-text">Yes. The group already has the corner, so the ring follows it.</p>          </div>        </li>        <li class="thread-item">          <span class="thread-avatar">            <span class="avatar avatar-sm">GH</span>          </span>          <div class="thread-body">            <p class="thread-header">              <span class="thread-author">Grace Hopper</span>              <time class="thread-meta" datetime="2026-11-12T10:31">35m</time>            </p>            <p class="thread-text">I will make the change this afternoon.</p>          </div>        </li>      </ol>    </li>    <li class="thread-item">      <span class="thread-avatar">        <span class="avatar">KJ</span>      </span>      <div class="thread-body">        <p class="thread-header">          <span class="thread-author">Katherine Johnson</span>          <time class="thread-meta" datetime="2026-11-12T10:54">12m</time>        </p>        <p class="thread-text">The figures in the second table are right.</p>      </div>    </li>  </ol></div>

#Structure

A thread is a list, and each thread-item is one comment with three children, in this order:

  1. thread-avatar, around an avatar. It is the column the line runs down.
  2. thread-body, the words: a thread-header with the thread-author and a thread-meta beside the name, then one thread-text for each paragraph. Both keep no margin of their own, whatever the page gives a paragraph. Anything else may follow: a quote, a row of quiet buttons.
  3. thread-replies, when there are any: a list of thread-items that answer this one.

The line appears by itself when replies follow a comment, and each reply is joined to it by an elbow that stops short of its avatar. The pieces of the line never overlap, because its ink is translucent and two layers would show as a darker stretch.

#Nesting

A reply can have replies: put a thread-replies inside it and the line continues one level in. A comment's avatar is 32px, the default avatar; a reply's is 24px, avatar-sm, at every depth.

Two or three levels are enough. Each level costs 36px of width, and a deep thread on a phone leaves no room for its words: past that, start the reply with the name of the person it answers and keep it on the same level.

Nesting

<div class="card max-w(480) mx-auto">  <ol class="thread">    <li class="thread-item">      <span class="thread-avatar">        <span class="avatar">GH</span>      </span>      <div class="thread-body">        <p class="thread-header">          <span class="thread-author">Grace Hopper</span>          <time class="thread-meta" datetime="2026-11-12T08:02">3h</time>        </p>        <p class="thread-text">Can we ship the contracts before the framework components?</p>      </div>      <ol class="thread-replies">        <li class="thread-item">          <span class="thread-avatar">            <span class="avatar avatar-sm">AL</span>          </span>          <div class="thread-body">            <p class="thread-header">              <span class="thread-author">Ada Lovelace</span>              <time class="thread-meta" datetime="2026-11-12T08:40">2h</time>            </p>            <p class="thread-text">Yes, they do not depend on each other.</p>          </div>          <ol class="thread-replies">            <li class="thread-item">              <span class="thread-avatar">                <span class="avatar avatar-sm">GH</span>              </span>              <div class="thread-body">                <p class="thread-header">                  <span class="thread-author">Grace Hopper</span>                  <time class="thread-meta" datetime="2026-11-12T08:44">2h</time>                </p>                <p class="thread-text">Then Tuesday.</p>              </div>            </li>          </ol>        </li>      </ol>    </li>  </ol></div>

#Thread or message

A thread is a discussion about something: comments are equals and any of them can be answered. A message is a turn of a conversation between two sides, as a bubble that points at its author. Events that nobody answers are a timeline.

#Accessibility

  • Write the thread as an <ol> and the replies as nested <ol>s. The nesting is the relation: a screen reader announces the level and the number of replies, which is what the line shows the eye.
  • Give each time a <time> with a datetime, since "2h" is short for something exact.
  • The line is drawn by the stylesheet and is not announced. In forced colors it takes the text color.
  • An avatar that shows initials or a photo beside the author's name is decoration: the name is already there in words.

#Classes

ClassDescription
threadThe list of comments, 20px apart
thread-itemOne comment: its avatar on a column, its words beside it and its replies under the words
thread-avatarThe avatar's column, around an avatar; under the avatar the line begins when replies follow
thread-bodyThe words: a thread-header and the thread-text under it. The first line is level with the middle of the avatar
thread-headerThe first line of the body: the author, and the time beside the name
thread-textWhat they wrote, in the body under the header; several paragraphs stand 8px apart
thread-authorWho wrote it, in strong ink
thread-metaWhen, and anything else said quietly beside the name
thread-repliesThe list of replies inside a thread-item, after its body; each reply is a thread-item joined to the comment by an elbow of the line

#Custom properties

PropertyDefaultDescription
--thread-avatar2remSize of a comment's avatar; replies use 1.5rem
--thread-gap.75remSpace between an avatar and its words
--thread-space1.25remSpace between comments; replies stand .875rem apart

#Source

@master/ui/components/thread.css
/* * Thread: comments and their replies, each joined to the comment it answers. * A comment is an avatar on a column and its words beside it. Replies stand under the words, and a hairline * runs down from the comment's avatar and turns into each of them: a reader follows a line, not an indent, * to see what answers what. The line is drawn in pieces that never overlap, since its ink is translucent: * the stretch under the avatar, an elbow into each reply, and the stretch on to the next one. * Geometry: a 32px avatar for a comment and 24px for a reply (avatar-sm), 12px between an avatar and its * words, 20px between comments and 14px between replies. The name stands level with the middle of its avatar. */@layer components {  .thread {    --thread-avatar: 2rem;    --thread-gap: .75rem;    --thread-space: 1.25rem;    display: grid;    gap: var(--thread-space);    min-width: 0;    margin: 0;    padding: 0;    color: var(--color-text-body);    font-size: .875rem;    line-height: 1.55;    list-style: none;  }  /* One comment: the avatar's column, the words, and under the words its replies. */  .thread-item {    /* Where this comment's line runs, for the replies that turn off it. */    --thread-rail: calc(var(--thread-avatar) / 2);    position: relative;    display: grid;    grid-template-columns: var(--thread-avatar) minmax(0, 1fr);    column-gap: var(--thread-gap);    min-width: 0;  }  /* The avatar's column. Under the avatar the line begins, when replies follow. */  .thread-avatar {    --avatar-size: var(--thread-avatar);    display: flex;    grid-area: 1 / 1;    flex-direction: column;    align-items: center;    min-width: 0;    &:has(~ .thread-replies)::after {      content: '';      flex: 1 1 0;      width: 1px;      margin-top: .25rem;      background: var(--color-line-divider);    }    @media (forced-colors: active) {      &::after {        background: CanvasText;      }    }  }  /* The words: a line that says who and when, then what they wrote. The first line is level with the middle of the avatar. */  .thread-body {    display: grid;    grid-area: 1 / 2;    gap: .125rem;    min-width: 0;    padding-top: calc((var(--thread-avatar) - 1.25rem) / 2);    overflow-wrap: anywhere;  }  /* The line with the name on it, and what is said quietly beside the name. */  .thread-header {    display: flex;    flex-wrap: wrap;    align-items: baseline;    gap: 0 .5rem;    min-width: 0;    margin: 0;  }  /* What they wrote. Several paragraphs stand 8px apart. */  .thread-text {    min-width: 0;    margin: 0;    text-wrap: pretty;    & + & {      margin-top: .375rem;    }  }  /* Who wrote it. */  .thread-author {    color: var(--color-text-strong);    font-weight: var(--font-weight-control);    line-height: 1.25rem;  }  /* When, and anything else said quietly beside the name. */  .thread-meta {    color: var(--color-text-muted);    font-size: .78125rem;    font-variant-numeric: tabular-nums;    line-height: 1.25rem;  }  /* The replies, under the words. Their avatars are a size smaller. */  .thread-replies {    --thread-parent: var(--thread-rail);    --thread-avatar: 1.5rem;    --thread-space: .875rem;    display: grid;    grid-area: 2 / 2;    min-width: 0;    margin: 0;    padding: 0;    list-style: none;    .thread-item {      padding-top: var(--thread-space);      /* The elbow: down the comment's line, then round into this reply's avatar, stopping 4px short of it. */      &::before {        content: '';        position: absolute;        top: 0;        inset-inline-start: calc(-1 * (var(--thread-gap) + var(--thread-parent)) - .5px);        width: calc(var(--thread-gap) + var(--thread-parent) - .25rem);        height: calc(var(--thread-space) + var(--thread-avatar) / 2);        border-block-end: 1px solid var(--color-line-divider);        border-inline-start: 1px solid var(--color-line-divider);        border-end-start-radius: .625rem;        pointer-events: none;      }      /* The line goes on to the next reply, from where the elbow leaves it. */      &:not(:last-child)::after {        content: '';        position: absolute;        top: calc(var(--thread-space) + var(--thread-avatar) / 2 - .625rem);        bottom: 0;        inset-inline-start: calc(-1 * (var(--thread-gap) + var(--thread-parent)) - .5px);        width: 1px;        background: var(--color-line-divider);        pointer-events: none;      }      @media (forced-colors: active) {        &::before {          border-color: CanvasText;        }        &::after {          background: CanvasText;        }      }    }  }}