Comment Thread UI — Free HTML CSS JS Snippet

Comment Thread · Cards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Recursive nesting: self-similar markup works at any reply depth
Thread line: left border + indent on .replies, Reddit/HN convention
Toggle voting with base+state tracking — no double-counting
:scope selectors to target immediate children in the recursive tree
Inline reply boxes injected at the correct depth
Sort by Top (votes) or Newest (time) via data attributes
appendChild node-move reordering preserves handlers and state
Auto-growing composer textarea via scrollHeight

About this UI Snippet

Comment Thread — Nested Replies, Voting, Sorting & Auto-Grow Composer

Screenshot of the Comment Thread snippet rendered live

A threaded comment system is one of the most complex common UI patterns — it combines recursion, voting state, dynamic insertion, and sorting. It sits below a social post card in most feeds. This snippet delivers a complete, working comment thread with nested replies, a Reddit-style up/down voting widget (compare the emoji reaction bar), top/newest sorting, an author badge, relative timestamps, and an auto-growing comment composer.

The nesting structure

Each comment is a .comment element containing an avatar and a .comment-body. The body holds the comment content and a .replies container, which itself holds more .comment elements — making the structure naturally recursive. Replies are visually indented with a left padding and a 2px left border that acts as a thread line, the convention popularised by Reddit and Hacker News. Because the markup is self-similar at every depth, the same JavaScript functions work at any nesting level.

The voting widget

The vote() function implements toggle voting with proper state tracking. It stores the base vote count and a per-comment state of 0, 1, or −1. Clicking upvote toggles between +1 and 0; clicking downvote toggles between −1 and 0; switching from up to down moves directly between states. The displayed count is always base + state, and the active arrow gets a coloured highlight. This matches the exact behaviour users expect from social platforms and prevents the common bug of double-counting repeated clicks.

Dynamic reply insertion

toggleReply() injects an inline reply box directly beneath a comment's actions, using the :scope selector to target only the immediate children — critical in a recursive structure where a naive querySelector would match nested descendants. sendReply() builds a new comment node via buildComment() and appends it to that comment's own .replies container, so the reply lands at the correct depth.

Sorting

sortThread() reorders top-level comments by either vote count (Top) or recency (Newest), reading data-votes and data-time attributes. It sorts an array of the DOM nodes and re-appends them in order — appendChild on an existing node moves it rather than cloning, so reordering is efficient and preserves all event handlers and state.

The auto-growing composer

The composer textarea grows to fit its content via autoGrow() — the same auto-resize textarea technique — which resets the height to auto and then sets it to scrollHeight on every input. This avoids inner scrollbars and gives the comfortable expanding-input feel of modern comment boxes. Posting a root comment prepends it to the thread and updates the comment count.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to reconstruct the vote state machine or the recursive DOM targeting in your head. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why vote() stores a separate base value and a tri-state (0, 1, -1) rather than just incrementing a counter directly, and why toggleReply and sendReply use :scope-qualified queries instead of a plain querySelector. The same assistant can help optimize it — for instance asking whether sortThread's full re-append of every top-level comment node is necessary versus a more targeted DOM reorder for very long threads. It's also useful for extending the thread: ask it to add comment editing and deletion, collapse/expand controls for deeply nested replies on narrow screens, or wire the whole thing to a real backend so votes and new comments persist across reloads. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a nested, threaded comment system in plain HTML, CSS, and JavaScript with voting and sorting — no frameworks, no state library.

Requirements:
- Each comment must be a self-similar DOM structure (avatar, author, timestamp, text, action row, and a replies container that itself can hold more comments of the identical structure) so the same functions work correctly at any nesting depth, with replies visually indented and marked with a left border thread line.
- An upvote/downvote control per comment that tracks a base score plus a separate tri-state value (neutral, upvoted, downvoted) so the displayed count is always base plus state: clicking the active arrow again returns to neutral, and clicking the opposite arrow switches directly between the two active states without ever double-counting or drifting from repeated clicks.
- A "Reply" action that inserts an inline textarea box immediately after that specific comment's own action row — using a scoped child query (not a query that could accidentally match a nested descendant's reply box) — with Cancel and Reply buttons, and submitting appends the new comment into that exact parent's own replies container, not the root thread.
- A composer textarea at the top of the thread that grows its height automatically to fit its content as the user types, instead of scrolling internally, and clears itself after a successful post.
- A sort control that reorders only the top-level comments by either total votes (descending) or recency, by reading data attributes on each comment node and re-inserting the existing DOM nodes in the new order (moving them, not cloning or rebuilding them) so all attached event handlers and any open reply boxes are preserved.
- A live comment counter that updates whenever a new top-level comment or reply is posted.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Write a top-level commentType in the composer at the top. It grows as you type. Click Comment to post — your comment appears at the top of the thread and the count updates.
  2. 2
    Reply to a commentClick Reply under any comment to open an inline reply box at that exact level. Type your reply and click Reply to nest it under the parent.
  3. 3
    Vote on commentsClick the up or down arrow to vote. Clicking the same arrow again removes your vote; clicking the opposite arrow switches it. The count and arrow colour update instantly.
  4. 4
    Sort the discussionUse the Top / Newest dropdown to reorder top-level comments by vote count or recency.
  5. 5
    Wire to a backendReplace the in-DOM buildComment with API calls: POST new comments and replies to your server, and render the returned comment objects. Store votes server-side keyed by user and comment.
  6. 6
    Export for your frameworkClick "JSX" for a React component that renders comments recursively from a nested data array. Click "Vue" for a Vue 3 SFC using a recursive component.

Real-world uses

Common Use Cases

Blog and article comment section
Drop the thread below articles to enable reader discussion. Wire posting and voting to your backend, render server-stored comments on load, and add pagination or lazy-loading for long threads. The nested structure supports the back-and-forth that flat comment lists cannot.
Community forum and discussion platform
Build a Reddit- or Hacker News-style forum where vote sorting surfaces the best replies. Add moderation actions (pin, remove, lock), user flair via the badge slot, and collapse controls for deep threads. The recursive model scales to arbitrary discussion depth.
Code review and document annotation tool
Adapt threaded comments for inline code review or document feedback, where each thread is anchored to a line or selection. Replies form the review conversation, and resolving a thread collapses it. Voting becomes agreement signalling among reviewers.
Product feedback and feature-request board
Let users post feature requests as top-level comments and discuss them in replies. Vote sorting prioritises the most-wanted features, giving the product team a ranked backlog driven directly by user demand. Add a status badge (Planned, Shipped) to each top-level item.
Study recursion, :scope selectors, and DOM moves
Threaded comments are a masterclass in recursive UI: self-similar markup, scoped DOM queries that avoid matching descendants, toggle-state voting, and efficient reordering via node moves. These patterns underpin file trees, org charts, nested menus, and any hierarchical interface.
Live event or stream chat with threaded replies
Use threaded comments for a live Q&A or stream chat where viewers reply to each other. Vote sorting floats the best questions to the top for the host to answer. Combine with real-time updates (WebSocket) to append incoming comments and replies as they arrive.

Got questions?

Frequently Asked Questions

Each vote count stores two pieces of data: a base value (the original score) and a state of 0, 1, or −1 representing the current user's vote. The displayed number is always base + state. Clicking upvote sets state to 1, or back to 0 if it was already 1. Clicking downvote sets it to −1, or back to 0. Switching directly from up to down moves from 1 to −1 in one click. Because the display is derived from base + state rather than incremented, repeated clicks can never accumulate a runaway count.

In a recursive structure, a comment contains a .replies container that holds more comments, each with their own .comment-actions and .replies. A plain querySelector(".replies") from a comment body would match the first nested .replies anywhere in its subtree, not necessarily its own direct child. The :scope pseudo-class (querySelector(":scope > .replies")) restricts the match to immediate children, so reply boxes and new comments are inserted at the correct depth rather than leaking into a descendant.

Track the depth as you render and stop indenting past a threshold (commonly 3-5 levels), which is how Reddit handles it. Beyond the limit, render deeper replies at the same indentation as their parent and prefix them with "replying to @user" for context. In CSS, you can also cap the cumulative left padding with a max value so the thread line never pushes content off a narrow screen.

Model comments as a nested array where each comment has a replies array. Create a recursive Comment component that renders its content and maps over comment.replies, rendering a Comment for each — the recursion mirrors the DOM nesting. Manage votes and the open reply box in component state or a normalised store keyed by comment id. For posting, update the tree immutably by inserting the new reply into the correct parent's replies array.