Efferd

Comments

Cards 41

About

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.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Comments to your project and fits it to your theme.

Code

How Comments is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

'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://cdn.21st.dev/assets/mirror/8b/8bde1e752a4f6699d0fafc5a956dc4e87c59892be246660c90a903ddb287584e.png',
};

export const commentsData: CommentType[] = [
	{
		id: '1',
		content: 'What a helpful post! Really made my day.',
		user: {
			id: '2',
			name: 'Ayesha',
			image: 'https://cdn.21st.dev/assets/mirror/c8/c81e45f45590e28f736f70d600ef14ca53300426623bfda197d80c6923b23f5c.png',
		},
		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://cdn.21st.dev/assets/mirror/e9/e94cef46f18ef6977e08e43c7c6df4049811b13790d9d9a2cd6ef386fcd83863.png',
		},
		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://cdn.21st.dev/assets/mirror/30/30b75b922400030fab943af0bfafff00c56442ae576e6767dc9bf23f33151602.png',
		},
		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://cdn.21st.dev/assets/mirror/54/542c2110a779c7d15b1e2944178adf3fc831f53dba6b4013a42ac930a8258b7b.png',
		},
		repliesCount: 0,
		createdAt: new Date('2025-07-20T16:10:00Z'),
	},
	{
		id: '5',
		content: 'Brilliant work as always!',
		user: {
			id: '8',
			name: 'Nimra',
			image: 'https://cdn.21st.dev/assets/mirror/9f/9ff1482ee895e4438946e6ebd8a159fd9d64f846bac5359224fcd69f83bae1f3.png',
		},
		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://cdn.21st.dev/assets/mirror/36/3688433360a7e6ef8fa5d35a607263f965f2f85500584adc7165f02d3fa3cf8b.png',
		},
		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>
	);
}

Dependencies

More by Efferd

View all
  • Testimonials Columns

    Testimonials Columns

    7.1K
  • Zoom Parallax

    Zoom Parallax

    5.2K
  • Dotted Surface

    Dotted Surface

    3.5K
  • Footer Section

    Footer Section

    2.7K
  • Grid Feature Cards

    Grid Feature Cards

    1.9K
  • Logo Cloud 2

    Logo Cloud 2

    1.6K
  • Header 3

    Header 3

    1.6K
  • Logo Cloud 3

    Logo Cloud 3

    1.5K

More in Cards

View all
  • Statistics Card 2

    Statistics Card 2

    264
  • Course Design Cards

    Course Design Cards

    1.1K
  • Card

    Card

    70
  • Stats cards with links

    Stats cards with links

    39
  • The Item one

    The Item one

    3
  • Card Studio

    Card Studio

    7
  • Statistics Card 7

    Statistics Card 7

    292
  • Item

    Item

    10