npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kingstack/comment-tree

v0.2.2

Published

A virtualized comment tree component for React with depth indicators

Downloads

265

Readme

@kingstack/comment-tree

A virtualized comment tree component for React with depth indicators.

Features

  • Virtualization: Efficiently renders large comment threads using @tanstack/react-virtual
  • Depth indicators: Colored vertical lines that indicate comment depth
  • Collapsible threads: Expand/collapse comment threads with reply counts
  • Action overflow menu: Inline actions with smart overflow menu that stays within viewport
  • Theming support: Customize with classNames prop or go fully unstyled
  • Light & dark mode: Easy theming for any color scheme
  • Custom rendering: Bring your own comment renderer
  • TypeScript: Full type safety with generics support
  • Accessible: Proper ARIA labels and keyboard support

Installation

npm install @kingstack/comment-tree
# or
yarn add @kingstack/comment-tree
# or
pnpm add @kingstack/comment-tree

Peer Dependencies

This package requires the following peer dependencies:

{
  "react": "^18.0.0 || ^19.0.0",
  "react-dom": "^18.0.0 || ^19.0.0"
}

Quick Start

import { useState } from "react";
import { CommentTree, CommentItems, CommentData } from "@kingstack/comment-tree";

interface MyComment extends CommentData {
  content: string;
  author: string;
  createdAt: Date;
}

const comments: CommentItems<MyComment> = [
  {
    id: "1",
    data: {
      id: "1",
      content: "This is a great post!",
      author: "alice",
      createdAt: new Date(),
    },
    children: [
      {
        id: "1-1",
        data: {
          id: "1-1",
          content: "I agree!",
          author: "bob",
          createdAt: new Date(),
        },
        children: [],
      },
    ],
  },
];

function MyComments() {
  const [items, setItems] = useState(comments);
  const [selectedId, setSelectedId] = useState<string | null>(null);

  return (
    <CommentTree<MyComment>
      id="my-comments"
      items={items}
      onItemsChange={setItems}
      selectedId={selectedId}
      onSelect={setSelectedId}
      collapsible
    />
  );
}

Virtualization

Enable virtualization for large comment threads by setting the height prop:

<CommentTree
  id="virtualized-comments"
  items={items}
  height={400} // Enable virtualization
  estimatedItemHeight={100}
  overscan={5}
/>

When height is set, only visible comments are rendered, making it efficient for threads with thousands of comments.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | id | string | Required | Unique identifier for the tree | | items | CommentItems<T> | Required | Comment data | | onItemsChange | (items) => void | - | Called when items change | | selectedId | string \| null | - | Currently selected comment | | onSelect | (id) => void | - | Called when a comment is selected | | onCollapseChange | (id, collapsed) => void | - | Called when collapse state changes | | commentActions | CommentAction[] \| (comment) => CommentAction[] | - | Actions shown on each comment | | onAction | (key, id) => void | - | Called when an action is triggered | | collapsible | boolean | true | Whether comments can be collapsed | | indentationWidth | number | 20 | Pixels per depth level | | renderComment | (props) => ReactNode | - | Custom comment renderer | | height | number | - | Enables virtualization | | width | number \| string | - | Fixed width for the container | | estimatedItemHeight | number | 80 | Estimated height for virtualization | | overscan | number | 5 | Extra items to render outside viewport | | depthColors | string[] | Reddit colors | Custom depth line colors (hex values) | | maxInlineActions | number | 2 | Max actions shown inline before overflow menu | | classNames | CommentTreeClassNames | - | Custom class names for theming | | unstyled | boolean | false | Remove all default styles | | emptyState | ReactNode | - | Content to show when no comments | | initialExpandedIds | string[] | - | IDs of initially expanded comments |

Actions

Add action buttons to comments. Actions beyond maxInlineActions appear in an overflow menu:

const actions: CommentAction[] = [
  { key: "reply", label: "Reply", icon: <ReplyIcon /> },
  { key: "upvote", label: "Upvote", icon: <ThumbUpIcon /> },
  { key: "share", label: "Share", icon: <ShareIcon /> },
  { key: "delete", label: "Delete", icon: <TrashIcon />, destructive: true },
];

<CommentTree
  items={items}
  commentActions={actions}
  maxInlineActions={2} // Reply & Upvote inline, others in overflow menu
  onAction={(actionKey, commentId) => {
    console.log(`Action ${actionKey} on comment ${commentId}`);
  }}
/>

Dynamic Actions

Pass a function to show different actions per comment:

<CommentTree
  items={items}
  commentActions={(comment) => [
    { key: "reply", label: "Reply" },
    // Only show delete for own comments
    ...(comment.data.author === currentUser
      ? [{ key: "delete", label: "Delete", destructive: true }]
      : []),
  ]}
/>

Theming

Dark Mode (Default)

The default styling is optimized for dark backgrounds:

<div className="bg-zinc-900">
  <CommentTree id="comments" items={items} />
</div>

Light Mode

Use classNames and custom depthColors for light themes:

const lightColors = [
  "#2563eb", // blue-600
  "#ea580c", // orange-600
  "#16a34a", // green-600
  "#9333ea", // purple-600
  "#db2777", // pink-600
];

<CommentTree
  id="light-comments"
  items={items}
  depthColors={lightColors}
  classNames={{
    container: "bg-white",
    comment: "hover:bg-zinc-50",
    commentSelected: "!bg-blue-50",
    author: "!text-zinc-900 !font-semibold",
    timestamp: "!text-zinc-500",
    text: "!text-zinc-700",
    actionButton: "!text-zinc-500 hover:!text-zinc-900 hover:!bg-zinc-100",
    actionButtonDestructive: "!text-red-600 hover:!bg-red-50",
    collapseButton: "!text-zinc-400 hover:!text-zinc-700 hover:!bg-zinc-100",
    overflowButton: "!text-zinc-400 hover:!text-zinc-700 hover:!bg-zinc-100",
    overflowMenu: "!bg-white !border-zinc-200 !shadow-lg",
    overflowMenuItem: "!text-zinc-700 hover:!bg-zinc-100",
  }}
/>

Available classNames Keys

| Key | Description | |-----|-------------| | container | Main container element | | comment | Individual comment wrapper | | commentSelected | Comment when selected | | depthLine | Depth indicator line | | content | Comment content area | | author | Author name | | timestamp | Timestamp text | | text | Comment text content | | actions | Actions container | | actionButton | Individual action button | | actionButtonDestructive | Destructive action button | | overflowButton | The "..." overflow menu button | | overflowMenu | Overflow menu dropdown container | | overflowMenuItem | Overflow menu item | | collapseButton | Collapse/expand button | | replyCount | Reply count badge |

Fully Unstyled

For complete control, use unstyled={true} to remove all default styles:

<CommentTree
  items={items}
  unstyled
  classNames={{
    comment: "flex p-4 border-b border-gray-200",
    author: "font-bold text-gray-900",
    text: "mt-2 text-gray-700",
  }}
/>

Custom Rendering

Provide a custom renderer for complete control over comment appearance:

<CommentTree
  id="custom-comments"
  items={items}
  renderComment={({
    comment,
    depth,
    isCollapsed,
    hasReplies,
    replyCount,
    isSelected,
    onCollapse,
    onSelect,
    actions,
    onAction,
    depthColors,
    indentationWidth,
  }) => (
    <div className="flex" onClick={onSelect}>
      {/* Render depth lines */}
      {Array.from({ length: depth }).map((_, i) => (
        <div
          key={i}
          className="border-l-2"
          style={{
            borderColor: depthColors[i % depthColors.length],
            width: indentationWidth,
          }}
        />
      ))}

      {/* Your custom comment UI */}
      <div className="flex-1 p-2">
        <p className="font-bold">{comment.data.author}</p>
        {!isCollapsed && <p>{comment.data.content}</p>}
        {isCollapsed && hasReplies && (
          <span className="text-gray-500">({replyCount} replies)</span>
        )}
      </div>
    </div>
  )}
/>

Utilities

The package exports utilities for working with comment data:

import {
  flattenComments,
  findComment,
  removeComment,
  getReplyCount,
  getDepthColor,
  DEFAULT_DEPTH_COLORS,
} from "@kingstack/comment-tree";

// Flatten nested comments for rendering
const flat = flattenComments(comments);

// Find a specific comment
const comment = findComment(comments, "1-1");

// Remove a comment from the tree
const newComments = removeComment(comments, "1-1");

// Get total replies for a comment (including nested)
const count = getReplyCount(comments, "1");

// Get color for a depth level
const color = getDepthColor(2, DEFAULT_DEPTH_COLORS);

Depth Colors

Default colors cycle through 10 Reddit-inspired colors:

  1. Blue (#3b82f6)
  2. Orange (#f97316)
  3. Green (#22c55e)
  4. Purple (#a855f7)
  5. Pink (#ec4899)
  6. Yellow (#eab308)
  7. Cyan (#06b6d4)
  8. Red (#ef4444)
  9. Indigo (#6366f1)
  10. Emerald (#10b981)

Customize with the depthColors prop (use hex color values):

const myColors = [
  "#ef4444", // red
  "#3b82f6", // blue
  "#22c55e", // green
];

<CommentTree depthColors={myColors} />

TypeScript

The component is fully typed with generics support:

interface MyComment extends CommentData {
  content: string;
  author: string;
  createdAt: Date;
  upvotes: number;
  // Add any custom fields
}

// All props and callbacks are typed to MyComment
<CommentTree<MyComment>
  items={items}
  commentActions={(comment) => {
    // comment.data is typed as MyComment
    console.log(comment.data.upvotes);
    return [];
  }}
/>

Browser Support

  • Chrome, Firefox, Safari, Edge (latest 2 versions)
  • Requires ResizeObserver (polyfill needed for older browsers)

License

MIT