Components
A fully functional comment system component with support for replies, user avatars, and animated UI using framer-motion. It includes individual cards for comments and replies, shows how long ago each was posted, and allows the current user to delete their own entries. A form at the bottom handles new comment or reply submission, and a skeleton loader appears while replies are loading—giving it a polished, interactive feel for blogs, posts, or discussion threads.
npx shadcn@latest add https://21st.dev/r/sshahaider/commentsLoading preview...
'use client';
import React, { useEffect } from 'react';
import {
type User,
type CommentType,
CommentCard,
SkeletonCard,
CommentForm,
delay,
} from '@/components/ui/comments';
import { AnimatePresence, motion } from 'framer-motion';
export const currentUser: User = {
id: '1',
name: 'Shaban Haider',
image: 'https://avatar.vercel.sh/shaban',
};
export const commentsData: CommentType[] = [
{
id: '1',
content: 'What a helpful post! Really made my day.',
user: {
id: '2',
name: 'Ayesha',
image: 'https://avatar.vercel.sh/ayesha',
},
repliesCount: 2,
createdAt: new Date('2025-07-19T08:30:00Z'),
},
{
id: '2',
content: 'I think there’s a bug in the logic.',
user: {
id: '5',
name: 'Ali',
image: 'https://avatar.vercel.sh/ali',
},
repliesCount: 1,
createdAt: new Date('2025-07-18T14:45:00Z'),
},
{
id: '3',
content: 'Awesome explanation. Helped me understand recursion.',
user: {
id: '6',
name: 'Zara',
image: 'https://avatar.vercel.sh/zara',
},
repliesCount: 3,
createdAt: new Date('2025-07-17T09:20:00Z'),
},
{
id: '4',
content: 'Could you add more examples in the doc?',
user: {
id: '7',
name: 'Fahad',
image: 'https://avatar.vercel.sh/fahad',
},
repliesCount: 0,
createdAt: new Date('2025-07-20T16:10:00Z'),
},
{
id: '5',
content: 'Brilliant work as always!',
user: {
id: '8',
name: 'Nimra',
image: 'https://avatar.vercel.sh/nimra',
},
repliesCount: 2,
createdAt: new Date('2025-07-21T07:50:00Z'),
},
{
id: '6',
content: 'Not sure this applies in all cases.',
user: {
id: '9',
name: 'Bilal',
image: 'https://avatar.vercel.sh/bilal',
},
repliesCount: 1,
createdAt: new Date('2025-07-22T12:15:00Z'),
},
];
export default function Demo() {
const [comments, setComments] = React.useState<CommentType[]>([]);
const [commentsCount, setCommentsCount] = React.useState(commentsData.length);
const setData = (data: CommentType) => {
setComments([data, ...comments]);
setCommentsCount(commentsCount + 1);
};
const fetchComments = async () => {
await delay(1000);
setComments(commentsData);
};
useEffect(() => {
fetchComments();
}, []);
const handleDeleteComment = (id: string) => {
const filteredComments = comments.filter((comment) => comment.id !== id);
setComments(filteredComments);
setCommentsCount(commentsCount - 1);
};
return (
<div className="py-10 px-4 flex w-full items-start justify-center">
<div className="w-full max-w-xl">
<h2 className="mb-4 text-2xl font-bold">Comments Section</h2>
<div className="bg-border h-px w-full" />
<div className="space-y-2">
<CommentForm setData={setData} currentUser={currentUser} />
<AnimatePresence initial={false}>
{comments.length === 0 && commentsCount > 0
? Array.from({
length: 10,
}).map((_, index) => <SkeletonCard key={index} />)
: comments.map((comment, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="w-full"
>
<CommentCard
onDelete={handleDeleteComment}
currentUser={currentUser}
data={comment}
/>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
</div>
);
}