Accessibility pattern · Communication

Comment thread

Each comment is an article named by its author, and replies are a list nested inside the comment they answer, so the thread's shape is in the markup. Reply, edit and delete open in place and take focus; Escape puts focus back on the button that opened them, and every change is confirmed in a polite status.

WCAG criteria
7
Keyboard rules
4
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Comments 6

500 characters left

  1. Asha Rao

    Carry two pairs of socks. The trail turns into a stream after Udhewadi, and wet feet blister fast.

    1. Vikram Shetty

      Seconded. Quick-dry socks were worth every rupee.

    2. Neha Kulkarni

      Is the path marked, or do we need a guide from Lonavala?

    3. You

      It's marked up to the village. After that, follow the fort walls.

  2. Rohan Mehta

    Start before 7. The last shared jeeps back to Lonavala leave by 5.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Tab or ShiftTabMoves through the comment box, each comment's buttons and the thread toggles in reading order.
Enter or SpaceOn Reply, opens a reply box under that comment and moves focus into it. On a thread toggle, hides or shows its replies.
EscapeIn a reply box, the edit box or the delete question, closes it and puts focus back on the button that opened it.
EnterOn Post comment or Post reply, posts it and moves focus to the new comment. On Save, keeps the change and returns focus to Edit.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches a Reply buttonReply to Asha, button, collapsed
Enter opens the reply boxReply to Asha, edit text, multi line
The reply is postedYou, article (focus on the new reply), then: Reply posted
Focus reaches a thread toggleHide 3 replies, button, expanded
Delete is pressed on your commentDelete this comment? This cannot be undone. Group. Cancel, button
A comment is postedYou, article, then: Comment posted

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<section class="ap-comments" data-ap-comments aria-labelledby="cmt-h">
  <div class="ap-comments__head">
    <h3 class="ap-comments__count" id="cmt-h" tabindex="-1">Comments <span class="ap-comments__n" data-ap-cmt-total>6</span></h3>
    <p class="ap-comments__status" role="status"></p>
  </div>

  <form class="ap-comments__new" data-ap-cmt-new data-max="500" novalidate>
    <label class="ap-label" for="cmt-new-text">Add a comment</label>
    <textarea class="ap-input" id="cmt-new-text" name="comment" rows="3" aria-describedby="cmt-new-count"></textarea>
    <p class="ap-error" id="cmt-new-error" hidden><svg class="ap-comments__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><span></span></p>
    <div class="ap-comments__bar">
      <p class="ap-comments__left" id="cmt-new-count"><span data-ap-cmt-left>500</span> <span data-ap-cmt-words>characters left</span></p>
      <button type="submit" class="ap-btn ap-btn--primary">Post comment</button>
    </div>
  </form>

  <ol class="ap-comments__list">
    <li>
      <article class="ap-comments__c" id="cmt-1" aria-labelledby="cmt-1-who" tabindex="-1">
        <span class="ap-comments__avatar" aria-hidden="true" translate="no">AR</span>
        <div class="ap-comments__main">
          <p class="ap-comments__meta"><span class="ap-comments__who" id="cmt-1-who">Asha Rao</span> <time datetime="2026-10-05T09:12+05:30" title="5 October 2026 at 09:12">3 hours ago<span class="ap-comments__vh">, <span>5 October 2026 at 09:12</span></span></time></p>
          <p class="ap-comments__text">Carry two pairs of socks. The trail turns into a stream after Udhewadi, and wet feet blister fast.</p>
          <div class="ap-comments__acts">
            <button type="button" class="ap-comments__act" data-ap-cmt-reply aria-label="Reply to Asha" aria-expanded="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 14 4 9l5-5"/><path d="M4 9h10a6 6 0 0 1 6 6v4"/></svg>Reply</button>
          </div>
        </div>
      </article>
      <button type="button" class="ap-comments__toggle" data-ap-cmt-toggle aria-expanded="true" aria-controls="cmt-1-replies"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg><span>Hide 3 replies</span></button>
      <ol class="ap-comments__replies" id="cmt-1-replies">
        <li>
          <article class="ap-comments__c" id="cmt-1a" aria-labelledby="cmt-1a-who" tabindex="-1">
            <span class="ap-comments__avatar" aria-hidden="true" translate="no">VS</span>
            <div class="ap-comments__main">
              <p class="ap-comments__meta"><span class="ap-comments__who" id="cmt-1a-who">Vikram Shetty</span> <time datetime="2026-10-05T10:05+05:30" title="5 October 2026 at 10:05">2 hours ago<span class="ap-comments__vh">, <span>5 October 2026 at 10:05</span></span></time></p>
              <p class="ap-comments__text">Seconded. Quick-dry socks were worth every rupee.</p>
              <div class="ap-comments__acts">
                <button type="button" class="ap-comments__act" data-ap-cmt-reply aria-label="Reply to Vikram" aria-expanded="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 14 4 9l5-5"/><path d="M4 9h10a6 6 0 0 1 6 6v4"/></svg>Reply</button>
              </div>
            </div>
          </article>
        </li>
        <li>
          <article class="ap-comments__c" id="cmt-1b" aria-labelledby="cmt-1b-who" tabindex="-1">
            <span class="ap-comments__avatar" aria-hidden="true" translate="no">NK</span>
            <div class="ap-comments__main">
              <p class="ap-comments__meta"><span class="ap-comments__who" id="cmt-1b-who">Neha Kulkarni</span> <time datetime="2026-10-05T10:31+05:30" title="5 October 2026 at 10:31">2 hours ago<span class="ap-comments__vh">, <span>5 October 2026 at 10:31</span></span></time></p>
              <p class="ap-comments__text">Is the path marked, or do we need a guide from Lonavala?</p>
              <div class="ap-comments__acts">
                <button type="button" class="ap-comments__act" data-ap-cmt-reply aria-label="Reply to Neha" aria-expanded="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 14 4 9l5-5"/><path d="M4 9h10a6 6 0 0 1 6 6v4"/></svg>Reply</button>
              </div>
            </div>
          </article>
        </li>
        <li>
          <article class="ap-comments__c" id="cmt-1c" aria-labelledby="cmt-1c-who" tabindex="-1" data-own>
            <span class="ap-comments__avatar ap-comments__avatar--me" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><circle cx="12" cy="8.5" r="3.5"/><path d="M5 20a7 7 0 0 1 14 0"/></svg></span>
            <div class="ap-comments__main">
              <p class="ap-comments__meta"><span class="ap-comments__who" id="cmt-1c-who">You</span> <time datetime="2026-10-05T11:02+05:30" title="5 October 2026 at 11:02">1 hour ago<span class="ap-comments__vh">, <span>5 October 2026 at 11:02</span></span></time></p>
              <p class="ap-comments__text">It's marked up to the village. After that, follow the fort walls.</p>
              <div class="ap-comments__acts">
                <button type="button" class="ap-comments__act" data-ap-cmt-edit aria-label="Edit your comment"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>Edit</button>
                <button type="button" class="ap-comments__act" data-ap-cmt-delete aria-label="Delete your comment"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h16"/><path d="M10 11v6M14 11v6"/><path d="M6 7l1 13h10l1-13"/><path d="M9 7V4h6v3"/></svg>Delete</button>
              </div>
            </div>
          </article>
        </li>
      </ol>
    </li>
    <li>
      <article class="ap-comments__c" id="cmt-2" aria-labelledby="cmt-2-who" tabindex="-1">
        <span class="ap-comments__avatar" aria-hidden="true" translate="no">RM</span>
        <div class="ap-comments__main">
          <p class="ap-comments__meta"><span class="ap-comments__who" id="cmt-2-who">Rohan Mehta</span> <time datetime="2026-10-04T18:40+05:30" title="4 October 2026 at 18:40">17 hours ago<span class="ap-comments__vh">, <span>4 October 2026 at 18:40</span></span></time></p>
          <p class="ap-comments__text">Start before 7. The last shared jeeps back to Lonavala leave by 5.</p>
          <div class="ap-comments__acts">
            <button type="button" class="ap-comments__act" data-ap-cmt-reply aria-label="Reply to Rohan" aria-expanded="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 14 4 9l5-5"/><path d="M4 9h10a6 6 0 0 1 6 6v4"/></svg>Reply</button>
          </div>
        </div>
      </article>
      <button type="button" class="ap-comments__toggle" data-ap-cmt-toggle aria-expanded="false" aria-controls="cmt-2-replies"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg><span>Show 1 reply</span></button>
      <ol class="ap-comments__replies" id="cmt-2-replies" hidden>
        <li>
          <article class="ap-comments__c" id="cmt-2a" aria-labelledby="cmt-2a-who" tabindex="-1">
            <span class="ap-comments__avatar" aria-hidden="true" translate="no">AR</span>
            <div class="ap-comments__main">
              <p class="ap-comments__meta"><span class="ap-comments__who" id="cmt-2a-who">Asha Rao</span> <time datetime="2026-10-04T19:15+05:30" title="4 October 2026 at 19:15">16 hours ago<span class="ap-comments__vh">, <span>4 October 2026 at 19:15</span></span></time></p>
              <p class="ap-comments__text">And carry cash. The village stalls can't take UPI when the network drops.</p>
              <div class="ap-comments__acts">
                <button type="button" class="ap-comments__act" data-ap-cmt-reply aria-label="Reply to Asha" aria-expanded="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 14 4 9l5-5"/><path d="M4 9h10a6 6 0 0 1 6 6v4"/></svg>Reply</button>
              </div>
            </div>
          </article>
        </li>
      </ol>
    </li>
  </ol>

  <div class="ap-comments__spare">
    <form class="ap-comments__form" id="cmt-replyform" data-ap-cmt-replyform novalidate hidden>
      <label class="ap-label" for="cmt-reply-text" data-ap-cmt-replylabel>Reply</label>
      <textarea class="ap-input" id="cmt-reply-text" name="reply" rows="2"></textarea>
      <p class="ap-error" id="cmt-reply-error" hidden><svg class="ap-comments__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><span>Write a reply first.</span></p>
      <div class="ap-comments__btns">
        <button type="button" class="ap-btn" data-ap-cmt-cancel>Cancel</button>
        <button type="submit" class="ap-btn ap-btn--primary">Post reply</button>
      </div>
    </form>
    <form class="ap-comments__form" id="cmt-editform" data-ap-cmt-editform novalidate hidden>
      <label class="ap-label" for="cmt-edit-text">Edit your comment</label>
      <textarea class="ap-input" id="cmt-edit-text" name="edit" rows="2"></textarea>
      <p class="ap-error" id="cmt-edit-error" hidden><svg class="ap-comments__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><span>Write something, or delete the comment instead.</span></p>
      <div class="ap-comments__btns">
        <button type="button" class="ap-btn" data-ap-cmt-cancel>Cancel</button>
        <button type="submit" class="ap-btn ap-btn--primary">Save</button>
      </div>
    </form>
    <div class="ap-comments__confirm" role="group" aria-labelledby="cmt-confirm-q" data-ap-cmt-confirm hidden>
      <p class="ap-comments__q" id="cmt-confirm-q">Delete this comment? This cannot be undone.</p>
      <div class="ap-comments__btns">
        <button type="button" class="ap-btn" data-ap-cmt-keep>Cancel</button>
        <button type="button" class="ap-btn ap-btn--danger" data-ap-cmt-remove>Delete</button>
      </div>
    </div>
  </div>
  <p class="ap-comments__vh" aria-live="polite" data-ap-cmt-say></p>
</section>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Comments are articles named by their author, replies are lists nested inside the comment they answer, and the count is part of the heading.

  • 2.1.1 Keyboard Level A

    Replying, collapsing threads, editing and deleting all work from the keyboard with ordinary buttons.

  • 2.4.3 Focus Order Level A

    Opening a reply or edit box moves focus into it; posting moves focus to the new comment; cancelling returns it to the button that started it.

  • 3.3.1 Error Identification Level A

    An empty or too-long comment is not posted; an error under the box says why, tied to the box and announced.

  • 3.3.4 Error Prevention (Legal, Financial, Data) Level AA

    Deleting asks first, in place, with Cancel focused, so one stray press never removes a comment.

  • 4.1.2 Name, Role, Value Level A

    Every Reply button and reply box names whom it answers; thread toggles and Reply buttons report whether they are open with aria-expanded.

  • 4.1.3 Status Messages Level AA

    Comment posted, Reply posted, Comment updated and Comment deleted are written to a status region, read without moving focus.

Usage

When to use it

Use it

  • Discussions under an article, a product, a design or a task, where replies answer one comment.
  • Review threads where people edit or remove what they wrote.

Use something else

  • Real-time back-and-forth between a few people: a chat with a live log suits it better.
  • Threads more than one level deep: past one level of replies, nesting is hard to follow by ear and by eye.
  • Short ratings or reactions: a reaction bar says more with less.

Common failures

How it usually goes wrong

  • Reply buttons that all say Reply

    A list of identical Reply buttons is meaningless out of context. Each one here is named Reply to Asha, Reply to Neha and so on.

  • A reply box that opens somewhere else

    A box opened at the top of the page, or with focus left on the button, leaves people hunting for it. Here it opens under the comment and takes focus.

  • Focus dropped after posting

    If focus stays on a button that has just disappeared, keyboard users land at the top of the page. Focus moves to the new comment, and the status confirms it.

  • Delete with no way back

    A one-press delete next to Edit is easy to hit by mistake. The question here appears in place, with Cancel focused.

  • Indentation as the only sign of a reply

    Indented divs look nested but read flat. Nested lists make each reply's place in the thread available to screen readers too.

  • Relative dates with no actual date

    "3 hours ago" goes stale and is vague. The time element carries the exact date and time, and screen readers hear it after the relative one.

Notes

Building it

  • One reply form, one edit form and one delete question are moved to where they are needed, so there is never more than one open at a time and ids stay unique.
  • The visually hidden full date sits inside the time element after the relative one; the title attribute shows the same date to mouse users on hover.
  • Thread toggles keep aria-expanded and also change their words between Hide and Show, as the GOV.UK accordion does; both say the same thing, so they never disagree.
  • Posting a reply into a collapsed thread opens the thread first, so focus never lands on something hidden.
  • Each count phrase (Hide 3 replies) is one piece of text, so translations can reorder the words and the number.

Sources: WAI-ARIA Authoring Practices: Disclosure · HTML: the article element

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction