Lightweight Discussion Forum for Students

By Vandu
Jul 9, 2025

Follow us on


A minimal threaded discussion forum using HTML, JS, and Tailwind CSS.

Lightweight Discussion Forum for Students

Building a Responsive Threaded Comment Section with HTML, CSS, and JavaScript

Introduction

In this tutorial, we'll create a beautiful, responsive threaded comment section similar to what you might find on discussion forums or blog platforms. This component will allow users to post comments and reply to existing ones, creating nested conversation threads.

Live Demo

What We'll Build

Our comment section will include:

  • A clean, modern UI with Tailwind CSS

  • Nested replies with visual hierarchy

  • Smooth animations and transitions

  • Responsive design that works on all devices

  • Interactive reply forms

Step 1: Setting Up the HTML Structure

First, let's create the basic HTML structure for our comment section.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Discussion Forum</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="bg-gray-50 min-h-screen">
    <div class="container mx-auto px-4 py-8 max-w-3xl">
        <div class="bg-white rounded-lg shadow-md p-6 mb-6">
            <h1 class="text-2xl font-bold text-gray-800 mb-2">Discussion Forum</h1>
            <p class="text-gray-600 mb-6">Share your thoughts and join the conversation</p>
            
            <!-- New Comment Form -->
            <div class="bg-gray-50 p-4 rounded-lg mb-8">
                <h3 class="font-medium text-gray-700 mb-3">Start a new discussion</h3>
                <textarea id="new-comment" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" rows="3" placeholder="What's on your mind?"></textarea>
                <div class="flex justify-end mt-3">
                    <button id="post-comment" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition duration-200">
                        Post Comment
                    </button>
                </div>
            </div>
            
            <!-- Comments Section -->
            <div class="space-y-6">
                <h2 class="text-xl font-semibold text-gray-700 mb-4">Recent Discussions</h2>
                <div id="comments-container">
                    <!-- Comments will be dynamically inserted here -->
                </div>
            </div>
        </div>
    </div>
</body>
</html>

Step 2: Adding Custom CSS

Let's add some custom CSS to enhance our comment section with animations and transitions.

<style>
    .comment-container {
        transition: all 0.3s ease;
    }
    .comment-container:hover {
        background-color: rgba(249, 250, 251, 0.5);
    }
    .reply-box {
        max-height: 0;
        overflow: hidden;
        transition: max-height 0.3s ease;
    }
    .reply-box.active {
        max-height: 200px;
        margin-top: 0.5rem;
    }
    .fade-in {
        animation: fadeIn 0.3s ease-in-out;
    }
    @keyframes fadeIn {
        from { opacity: 0; transform: translateY(10px); }
        to { opacity: 1; transform: translateY(0); }
    }
</style>

Step 3: Creating the JavaScript Logic

Now, let's add the JavaScript that will power our comment section.

3.1 Sample Data Structure

First, we'll define our sample data structure:

let comments = [
    {
        id: 1,
        author: "Alex Johnson",
        avatar: "https://randomuser.me/api/portraits/men/32.jpg",
        content: "Has anyone tried the new React 18 features?",
        timestamp: "2 hours ago",
        replies: [
            {
                id: 2,
                author: "Sarah Miller",
                avatar: "https://randomuser.me/api/portraits/women/44.jpg",
                content: "Yes! The automatic batching is a game changer...",
                timestamp: "1 hour ago",
                replies: []
            }
        ]
    }
];

3.2 Rendering Comments

Next, we'll create functions to render our comments:

function renderComments(commentsArray, container, depth = 0) {
    container.innerHTML = '';
    commentsArray.forEach(comment => {
        const commentElement = createCommentElement(comment, depth);
        container.appendChild(commentElement);
        
        if (comment.replies.length > 0) {
            const repliesContainer = document.createElement('div');
            repliesContainer.className = `ml-${depth >= 4 ? 4 : depth + 4} mt-2 border-l-2 border-gray-200 pl-4`;
            commentElement.appendChild(repliesContainer);
            renderComments(comment.replies, repliesContainer, depth + 1);
        }
    });
}

function createCommentElement(comment, depth) {
    const commentDiv = document.createElement('div');
    commentDiv.className = `comment-container bg-white rounded-lg p-4 mb-2 fade-in ${depth > 0 ? 'shadow-sm' : 'shadow'}`;
    commentDiv.dataset.id = comment.id;
    
    commentDiv.innerHTML = `
        <div class="flex items-start">
            <img src="${comment.avatar}" alt="${comment.author}" class="w-10 h-10 rounded-full mr-3">
            <div class="flex-1">
                <div class="flex items-center justify-between">
                    <div>
                        <span class="font-semibold text-gray-800">${comment.author}</span>
                        <span class="text-xs text-gray-500 ml-2">${comment.timestamp}</span>
                    </div>
                    <button class="text-gray-400 hover:text-gray-600 reply-btn" data-id="${comment.id}">
                        <i class="fas fa-reply"></i>
                    </button>
                </div>
                <p class="text-gray-700 mt-1">${comment.content}</p>
                <div class="reply-box mt-2">
                    <textarea class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm" rows="2" placeholder="Write your reply..."></textarea>
                    <div class="flex justify-end space-x-2 mt-2">
                        <button class="cancel-reply text-gray-500 hover:text-gray-700 text-sm">Cancel</button>
                        <button class="submit-reply bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm">Reply</button>
                    </div>
                </div>
            </div>
        </div>
    `;
    
    return commentDiv;
}

3.3 Helper Functions

We'll need some helper functions to manage our comments:

// Find comment by ID (including nested replies)
function findCommentById(commentsArray, id) {
    for (const comment of commentsArray) {
        if (comment.id === id) return comment;
        if (comment.replies.length > 0) {
            const found = findCommentById(comment.replies, id);
            if (found) return found;
        }
    }
    return null;
}

// Generate a unique ID
function generateId() {
    return Math.floor(Math.random() * 1000000);
}

// Toggle reply box
function toggleReplyBox(commentId) {
    const commentElement = document.querySelector(`.comment-container[data-id="${commentId}"]`);
    if (commentElement) {
        const replyBox = commentElement.querySelector('.reply-box');
        replyBox.classList.toggle('active');
    }
}

3.4 Event Handlers

Finally, we'll add our event handlers:

// Event Delegation for reply buttons
commentsContainer.addEventListener('click', (e) => {
    if (e.target.closest('.reply-btn')) {
        const commentId = parseInt(e.target.closest('.reply-btn').dataset.id);
        toggleReplyBox(commentId);
    }
    
    if (e.target.classList.contains('cancel-reply')) {
        const replyBox = e.target.closest('.reply-box');
        replyBox.classList.remove('active');
        replyBox.querySelector('textarea').value = '';
    }
    
    if (e.target.classList.contains('submit-reply')) {
        const replyBox = e.target.closest('.reply-box');
        const textarea = replyBox.querySelector('textarea');
        const content = textarea.value.trim();
        
        if (content) {
            const commentId = parseInt(replyBox.closest('.comment-container').dataset.id);
            const comment = findCommentById(comments, commentId);
            
            const newReply = {
                id: generateId(),
                author: "You",
                avatar: "https://randomuser.me/api/portraits/men/1.jpg",
                content: content,
                timestamp: "Just now",
                replies: []
            };
            
            comment.replies.push(newReply);
            renderComments(comments, commentsContainer);
            replyBox.classList.remove('active');
            textarea.value = '';
            
            // Scroll to the new reply
            setTimeout(() => {
                const newReplyElement = document.querySelector(`.comment-container[data-id="${newReply.id}"]`);
                newReplyElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            }, 100);
        }
    }
});

// Post new comment
postCommentBtn.addEventListener('click', () => {
    const content = newCommentInput.value.trim();
    if (content) {
        const newComment = {
            id: generateId(),
            author: "You",
            avatar: "https://randomuser.me/api/portraits/men/1.jpg",
            content: content,
            timestamp: "Just now",
            replies: []
        };
        
        comments.unshift(newComment);
        renderComments(comments, commentsContainer);
        newCommentInput.value = '';
        
        // Scroll to the new comment
        setTimeout(() => {
            const newCommentElement = document.querySelector(`.comment-container[data-id="${newComment.id}"]`);
            newCommentElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
        }, 100);
    }
});

// Initialize
document.addEventListener('DOMContentLoaded', () => {
    renderComments(comments, commentsContainer);
});

Step 4: Putting It All Together

Now that we have all the pieces, here's the complete implementation:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Discussion Forum</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        .comment-container {
            transition: all 0.3s ease;
        }
        .comment-container:hover {
            background-color: rgba(249, 250, 251, 0.5);
        }
        .reply-box {
            max-height: 0;
            overflow: hidden;
            transition: max-height 0.3s ease;
        }
        .reply-box.active {
            max-height: 200px;
            margin-top: 0.5rem;
        }
        .fade-in {
            animation: fadeIn 0.3s ease-in-out;
        }
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }
    </style>
</head>
<body class="bg-gray-50 min-h-screen">
    <div class="container mx-auto px-4 py-8 max-w-3xl">
        <div class="bg-white rounded-lg shadow-md p-6 mb-6">
            <h1 class="text-2xl font-bold text-gray-800 mb-2">Discussion Forum</h1>
            <p class="text-gray-600 mb-6">Share your thoughts and join the conversation</p>
            
            <!-- New Comment Form -->
            <div class="bg-gray-50 p-4 rounded-lg mb-8">
                <h3 class="font-medium text-gray-700 mb-3">Start a new discussion</h3>
                <textarea id="new-comment" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" rows="3" placeholder="What's on your mind?"></textarea>
                <div class="flex justify-end mt-3">
                    <button id="post-comment" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition duration-200">
                        Post Comment
                    </button>
                </div>
            </div>
            
            <!-- Comments Section -->
            <div class="space-y-6">
                <h2 class="text-xl font-semibold text-gray-700 mb-4">Recent Discussions</h2>
                <div id="comments-container">
                    <!-- Comments will be dynamically inserted here -->
                </div>
            </div>
        </div>
    </div>

    <script>
        // Sample data
        let comments = [
            {
                id: 1,
                author: "Alex Johnson",
                avatar: "https://randomuser.me/api/portraits/men/32.jpg",
                content: "Has anyone tried the new React 18 features? I'm particularly interested in the concurrent rendering capabilities.",
                timestamp: "2 hours ago",
                replies: [
                    {
                        id: 2,
                        author: "Sarah Miller",
                        avatar: "https://randomuser.me/api/portraits/women/44.jpg",
                        content: "Yes! The automatic batching is a game changer for performance. Really simplifies state updates.",
                        timestamp: "1 hour ago",
                        replies: []
                    },
                    {
                        id: 3,
                        author: "David Chen",
                        avatar: "https://randomuser.me/api/portraits/men/22.jpg",
                        content: "The transition API is what excites me most. Makes it so much easier to handle loading states.",
                        timestamp: "45 minutes ago",
                        replies: [
                            {
                                id: 4,
                                author: "Alex Johnson",
                                avatar: "https://randomuser.me/api/portraits/men/32.jpg",
                                content: "Agreed! Have you tried combining it with Suspense?",
                                timestamp: "30 minutes ago",
                                replies: []
                            }
                        ]
                    }
                ]
            },
            {
                id: 5,
                author: "Maria Garcia",
                avatar: "https://randomuser.me/api/portraits/women/63.jpg",
                content: "What's everyone's favorite CSS framework in 2023? I'm debating between Tailwind and Bootstrap for a new project.",
                timestamp: "3 hours ago",
                replies: [
                    {
                        id: 6,
                        author: "James Wilson",
                        avatar: "https://randomuser.me/api/portraits/men/75.jpg",
                        content: "Tailwind all the way! The utility-first approach gives so much more control without leaving your HTML.",
                        timestamp: "2 hours ago",
                        replies: []
                    }
                ]
            }
        ];

        // DOM Elements
        const commentsContainer = document.getElementById('comments-container');
        const newCommentInput = document.getElementById('new-comment');
        const postCommentBtn = document.getElementById('post-comment');

        // Render all comments
        function renderComments(commentsArray, container, depth = 0) {
            container.innerHTML = '';
            commentsArray.forEach(comment => {
                const commentElement = createCommentElement(comment, depth);
                container.appendChild(commentElement);
                
                if (comment.replies.length > 0) {
                    const repliesContainer = document.createElement('div');
                    repliesContainer.className = `ml-${depth >= 4 ? 4 : depth + 4} mt-2 border-l-2 border-gray-200 pl-4`;
                    commentElement.appendChild(repliesContainer);
                    renderComments(comment.replies, repliesContainer, depth + 1);
                }
            });
        }

        // Create a single comment element
        function createCommentElement(comment, depth) {
            const commentDiv = document.createElement('div');
            commentDiv.className = `comment-container bg-white rounded-lg p-4 mb-2 fade-in ${depth > 0 ? 'shadow-sm' : 'shadow'}`;
            commentDiv.dataset.id = comment.id;
            
            commentDiv.innerHTML = `
                <div class="flex items-start">
                    <img src="${comment.avatar}" alt="${comment.author}" class="w-10 h-10 rounded-full mr-3">
                    <div class="flex-1">
                        <div class="flex items-center justify-between">
                            <div>
                                <span class="font-semibold text-gray-800">${comment.author}</span>
                                <span class="text-xs text-gray-500 ml-2">${comment.timestamp}</span>
                            </div>
                            <button class="text-gray-400 hover:text-gray-600 reply-btn" data-id="${comment.id}">
                                <i class="fas fa-reply"></i>
                            </button>
                        </div>
                        <p class="text-gray-700 mt-1">${comment.content}</p>
                        <div class="reply-box mt-2">
                            <textarea class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm" rows="2" placeholder="Write your reply..."></textarea>
                            <div class="flex justify-end space-x-2 mt-2">
                                <button class="cancel-reply text-gray-500 hover:text-gray-700 text-sm">Cancel</button>
                                <button class="submit-reply bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm">Reply</button>
                            </div>
                        </div>
                    </div>
                </div>
            `;
            
            return commentDiv;
        }

        // Find comment by ID (including nested replies)
        function findCommentById(commentsArray, id) {
            for (const comment of commentsArray) {
                if (comment.id === id) return comment;
                if (comment.replies.length > 0) {
                    const found = findCommentById(comment.replies, id);
                    if (found) return found;
                }
            }
            return null;
        }

        // Generate a unique ID
        function generateId() {
            return Math.floor(Math.random() * 1000000);
        }

        // Toggle reply box
        function toggleReplyBox(commentId) {
            const commentElement = document.querySelector(`.comment-container[data-id="${commentId}"]`);
            if (commentElement) {
                const replyBox = commentElement.querySelector('.reply-box');
                replyBox.classList.toggle('active');
            }
        }

        // Event Delegation for reply buttons
        commentsContainer.addEventListener('click', (e) => {
            if (e.target.closest('.reply-btn')) {
                const commentId = parseInt(e.target.closest('.reply-btn').dataset.id);
                toggleReplyBox(commentId);
            }
            
            if (e.target.classList.contains('cancel-reply')) {
                const replyBox = e.target.closest('.reply-box');
                replyBox.classList.remove('active');
                replyBox.querySelector('textarea').value = '';
            }
            
            if (e.target.classList.contains('submit-reply')) {
                const replyBox = e.target.closest('.reply-box');
                const textarea = replyBox.querySelector('textarea');
                const content = textarea.value.trim();
                
                if (content) {
                    const commentId = parseInt(replyBox.closest('.comment-container').dataset.id);
                    const comment = findCommentById(comments, commentId);
                    
                    const newReply = {
                        id: generateId(),
                        author: "You",
                        avatar: "https://randomuser.me/api/portraits/men/1.jpg",
                        content: content,
                        timestamp: "Just now",
                        replies: []
                    };
                    
                    comment.replies.push(newReply);
                    renderComments(comments, commentsContainer);
                    replyBox.classList.remove('active');
                    textarea.value = '';
                    
                    // Scroll to the new reply
                    setTimeout(() => {
                        const newReplyElement = document.querySelector(`.comment-container[data-id="${newReply.id}"]`);
                        newReplyElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
                    }, 100);
                }
            }
        });

        // Post new comment
        postCommentBtn.addEventListener('click', () => {
            const content = newCommentInput.value.trim();
            if (content) {
                const newComment = {
                    id: generateId(),
                    author: "You",
                    avatar: "https://randomuser.me/api/portraits/men/1.jpg",
                    content: content,
                    timestamp: "Just now",
                    replies: []
                };
                
                comments.unshift(newComment);
                renderComments(comments, commentsContainer);
                newCommentInput.value = '';
                
                // Scroll to the new comment
                setTimeout(() => {
                    const newCommentElement = document.querySelector(`.comment-container[data-id="${newComment.id}"]`);
                    newCommentElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
                }, 100);
            }
        });

        // Initialize
        document.addEventListener('DOMContentLoaded', () => {
            renderComments(comments, commentsContainer);
        });
    </script>
</body>
</html>

 


TOPICS MENTIONED IN THIS ARTICLE

© 2025 Pay18News. All rights reserved.