Skip to content

Comment thread

A recursive thread of comments. The same component renders the root and the replies — depth is handled by indentation, not by separate components.

'use client';
import type { ReactNode } from 'react';
import { Avatar, Button, Stack, Text } from '@helixui/core';
export interface Comment {
id: string;
author: { name: string; avatarUrl?: string };
body: ReactNode;
postedAt: string; // ISO 8601
replies?: Comment[];
}
export function CommentThread({
comment,
depth = 0,
onReply,
}: {
comment: Comment;
depth?: number;
onReply?: (parentId: string) => void;
}) {
const fmt = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' });
return (
<div style={{ paddingInlineStart: depth === 0 ? 0 : 'var(--helixui-space-6)' }}>
<Stack gap={2}>
<Stack direction="row" gap={3} align="start">
<Avatar
src={comment.author.avatarUrl}
fallback={comment.author.name.split(' ').map((p) => p[0]).slice(0, 2).join('')}
size="sm"
/>
<Stack gap={1} style={{ flex: 1 }}>
<Stack direction="row" align="center" gap={2}>
<Text size="sm" weight="semibold">{comment.author.name}</Text>
<Text size="xs" tone="muted">{fmt.format(new Date(comment.postedAt))}</Text>
</Stack>
<Text size="sm">{comment.body}</Text>
{onReply ? (
<Button variant="ghost" tone="neutral" size="sm" onClick={() => onReply(comment.id)}>
Reply
</Button>
) : null}
</Stack>
</Stack>
{comment.replies?.length ? (
<div
style={{
borderInlineStart: '2px solid var(--helixui-color-border-subtle)',
paddingInlineStart: 'var(--helixui-space-3)',
marginInlineStart: 'var(--helixui-space-3)',
}}
>
<Stack gap={3}>
{comment.replies.map((reply) => (
<CommentThread key={reply.id} comment={reply} depth={depth + 1} onReply={onReply} />
))}
</Stack>
</div>
) : null}
</Stack>
</div>
);
}

Usage

<CommentThread
comment={{
id: '1',
author: { name: 'Wayne Ryu' },
body: 'Should the DataTable v0 accept a render-row prop?',
postedAt: '2026-05-26T09:12:00Z',
replies: [
{
id: '2',
author: { name: 'Reviewer A' },
body: 'Probably not — would complicate the virtualization story.',
postedAt: '2026-05-26T09:30:00Z',
},
],
}}
onReply={(id) => openReplyFormFor(id)}
/>

The recursion bottoms out cleanly when a comment has no replies. Depth is unbounded in code; in practice cap visual depth at 5 with a “continued in thread →” link.