
WhatsApp Clone – Realistic Chat App UI with Tailwind CSS, HTML & JavaScript
Are you passionate about front-end development or looking to strengthen your UI/UX skills by building real-world projects? Presenting a fully responsive WhatsApp Clone — a modern web-based chat interface that mimics the official WhatsApp Web experience. Developed using HTML, Tailwind CSS, and JavaScript, this project offers a hands-on approach to understanding how modern chat apps work, right from UI rendering to user interaction.
🧠 What This Project Offers
This WhatsApp Clone is not just a UI demo — it’s a feature-rich front-end application that demonstrates how today’s popular messaging platforms are structured visually. Built with simplicity, performance, and clarity in mind, this project helps developers and learners dive deep into:
-
UI State Management
-
Dynamic DOM Rendering
-
Responsive Web Design
-
User Interaction Flow
🔍 Key Features of the WhatsApp Clone
1. 🗂️ Dynamic Chat List
The left panel contains a searchable and scrollable list of recent chats. Each chat card showcases:
-
Profile image
-
Last message preview
-
Timestamp
-
Unread message badge
All elements are updated dynamically to simulate real conversation flow.
2. 💬 Interactive Chat Window
The central section is a fully interactive message window with:
-
Real-time message rendering
-
Sent and received message bubbles
-
Timestamps and read receipts
-
Date separators for better readability
The layout and flow closely resemble WhatsApp Web, making the user experience intuitive and familiar.
3. 📁 File Sharing UI
You can visually explore how shared files appear in a WhatsApp-like environment. This includes:
-
File name and preview
-
Download button
-
Display card layout
This feature prepares the ground for backend integration in future versions.
4. 📱 Fully Responsive Design
Thanks to Tailwind CSS, the app scales beautifully across screen sizes. Whether you’re on a mobile, tablet, or desktop, the layout adapts perfectly, offering a consistent user experience across devices.
5. 🔄 Smooth User Interaction
From selecting chats to typing messages, every interaction is smooth and responsive. Event handling and conditional rendering make the interface feel alive, much like the real WhatsApp.
💡 Why Build This?
This project is perfect for:
-
Front-end developers looking to master responsive UI design
-
Students who want to build portfolio-worthy projects
-
JavaScript learners interested in event handling and DOM manipulation
-
Professionals aiming to prototype real-time applications
🚀 What You’ll Learn
By exploring or customizing this project, you’ll gain practical knowledge of:
-
Modern web UI principles
-
Managing UI states without frameworks
-
Working with responsive utilities in Tailwind CSS
-
Structuring reusable and maintainable front-end code
This project can also act as the front-end layer for a complete full-stack chat application when connected to real-time databases like Firebase or Socket.io.
🔗 Source Code & Demo
You can view the live demo and access the full source code from the link below:
HTML Code
<div class="flex h-screen">
<!-- Left Sidebar -->
<aside class="w-72 bg-[#1e1e1e] flex flex-col border-r border-[#2a2a2a]">
<!-- Header -->
<header class="flex items-center justify-between px-4 py-3 border-b border-[#2a2a2a]">
<div class="flex items-center space-x-2">
<i class="fab fa-whatsapp text-[#25d366] text-xl">
</i>
<span class="text-white font-semibold text-lg">
WhatsApp
</span>
</div>
<div class="flex space-x-3 text-gray-400 text-lg">
<button aria-label="New chat" class="hover:text-white" id="newChatBtn">
<i class="fas fa-edit">
</i>
</button>
<button aria-label="Menu" class="hover:text-white">
<i class="fas fa-bars">
</i>
</button>
</div>
</header>
<!-- Tabs -->
<nav class="flex flex-col space-y-4 px-2 py-3 border-b border-[#2a2a2a]">
<button aria-label="Chats" class="text-[#25d366] border-l-4 border-[#25d366] pl-3 flex items-center space-x-2">
<i class="fas fa-comment-alt text-[#25d366]">
</i>
<span class="font-bold text-sm">
Chats
</span>
</button>
<button aria-label="Calls" class="text-gray-400 hover:text-white flex items-center space-x-2 pl-3">
<i class="fas fa-phone">
</i>
<span class="text-sm">
Calls
</span>
</button>
<button aria-label="Status" class="text-gray-400 hover:text-white flex items-center space-x-2 pl-3">
<i class="fas fa-circle-notch">
</i>
<span class="text-sm">
Status
</span>
</button>
</nav>
<!-- Search -->
<div class="px-4 py-2">
<input class="w-full bg-[#2a2a2a] rounded-md text-gray-300 placeholder-gray-500 text-sm px-3 py-2 focus:outline-none focus:ring-1 focus:ring-[#25d366]" id="searchInput" placeholder="Search or start a new chat" type="text"/>
</div>
<!-- Chats List -->
<div class="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-[#2a2a2a] scrollbar-track-transparent" id="chatList">
<ul class="divide-y divide-[#2a2a2a]" id="chatListUl">
<!-- Chat items will be rendered here by JS -->
</ul>
</div>
<!-- Bottom icons -->
<center>
<div class="flex flex-row items-center space-x-4 py-4 border-t border-[#2a2a2a]">
<button aria-label="Starred" class="text-gray-400 hover:text-white text-lg">
<i class="fas fa-star"></i>
</button>
<button aria-label="Trash" class="text-gray-400 hover:text-white text-lg">
<i class="fas fa-trash"></i>
</button>
<button aria-label="Settings" class="text-gray-400 hover:text-white text-lg">
<i class="fas fa-cog"></i>
</button>
<button aria-label="Profile" class="text-gray-400 hover:text-white text-lg">
<i class="fas fa-user-circle"></i>
</button>
</div>
</center>
</aside>
<!-- Main Chat Area -->
<main class="flex-1 flex flex-col bg-[#121212] relative">
<!-- Chat header -->
<header class="flex items-center justify-between px-4 py-3 border-b border-[#2a2a2a]" id="chatHeader" style="min-height:56px;">
<div class="flex items-center space-x-3">
<img alt="Profile picture" class="rounded-full w-10 h-10 object-cover" height="40" id="chatHeaderImg" src="" width="40"/>
<h1 class="text-white font-semibold text-sm truncate max-w-xs" id="chatHeaderName">
Select a chat
</h1>
</div>
<div class="flex items-center space-x-3 text-gray-400 text-lg" id="chatHeaderActions" style="visibility:hidden;">
<button aria-label="Video call" class="hover:text-white" id="videoCallBtn">
<i class="fas fa-video">
</i>
</button>
<button aria-label="Voice call" class="hover:text-white" id="voiceCallBtn">
<i class="fas fa-phone">
</i>
</button>
<button aria-label="Search" class="hover:text-white" id="searchChatBtn">
<i class="fas fa-search">
</i>
</button>
</div>
</header>
<!-- Chat messages -->
<section class="flex-1 overflow-y-auto px-6 py-4 space-y-6 scrollbar-thin scrollbar-thumb-[#2a2a2a] scrollbar-track-transparent bg-[url('https://i.ibb.co/7QpKsCX/whatsapp-bg.png')] bg-repeat" id="chatMessages" style="background-size: 60px 60px;">
<div class="flex justify-center" id="dateLabel" style="display:none;">
<span class="bg-[#1a1a1a] text-gray-400 text-xs px-2 py-0.5 rounded" id="dateLabelText">
</span>
</div>
<div id="messagesContainer" class="space-y-3 max-w-full">
<!-- Messages will be rendered here -->
</div>
</section>
<!-- Message input -->
<form class="flex items-center px-4 py-3 border-t border-[#2a2a2a]" id="messageForm">
<button aria-label="Attach" class="text-gray-400 hover:text-white text-lg mr-3" type="button" id="attachBtn">
<i class="fas fa-paperclip">
</i>
</button>
<input autocomplete="off" class="flex-1 bg-[#2a2a2a] rounded-full text-gray-300 placeholder-gray-500 text-sm px-4 py-2 focus:outline-none focus:ring-1 focus:ring-[#25d366]" id="messageInput" placeholder="Type a message" type="text"/>
<button aria-label="Send" class="text-gray-400 hover:text-white text-lg ml-3" type="submit" id="sendBtn">
<i class="fas fa-paper-plane">
</i>
</button>
</form>
</main>
</div>
<script src="https://cdn.tailwindcss.com">
</script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" rel="stylesheet"/>
CSS Code
/* Custom scrollbar for chat list */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-thumb { background-color: #2a2a2a; border-radius: 3px; } ::-webkit-scrollbar-track { background: transparent; } /* Hide number input arrows */ input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
JavaScript Code
// Data for chats and messages const chats = [ { id: 1, name: "Rahul Gurugram Pg209", lastMessage: "Voice call · Accepted on another dev…", lastMessageTime: "Yesterday", unreadCount: 0, avatar: null, messages: [ { id: 1, text: "Voice call accepted on another device", time: "Yesterday", fromMe: false, type: "text", date: "2025-06-04" } ], }, { id: 2, name: "Abhishek New Opjs", lastMessage: "Image", lastMessageTime: "Yesterday", unreadCount: 0, avatar: "https://storage.googleapis.com/a1aa/image/6032d1e5-0cd2-42d0-af7f-01a9ff93a9a5.jpg", messages: [ { id: 1, text: "Image", time: "Yesterday", fromMe: false, type: "text", date: "2025-06-04" } ], }, { id: 3, name: "VACANCY GROUP KONDAGAON...", lastMessage: "~Satendar Netam😍🥰 Image", lastMessageTime: "Yesterday", unreadCount: 82, avatar: "https://storage.googleapis.com/a1aa/image/c863f19b-9fdb-4779-7861-2bb0d6e4ea4f.jpg", messages: [ { id: 1, text: "~Satendar Netam😍🥰 Image", time: "Yesterday", fromMe: false, type: "text", date: "2025-06-04" } ], }, { id: 4, name: "JioAICloud", lastMessage: "All-New JioAICloud is here! Live In…", lastMessageTime: "Yesterday", unreadCount: 1, avatar: "https://storage.googleapis.com/a1aa/image/f98151c6-12f4-47be-c929-0e8ac68092db.jpg", messages: [ { id: 1, text: "All-New JioAICloud is here! Live In…", time: "Yesterday", fromMe: false, type: "text", date: "2025-06-04" } ], }, { id: 5, name: "Adsense Buy & Sell", lastMessage: "~Rahul Kumar left", lastMessageTime: "3:00 pm", unreadCount: 0, avatar: "https://storage.googleapis.com/a1aa/image/3137f0a7-11cf-44b7-15fe-8a0155ed7220.jpg", messages: [ { id: 1, text: "~Rahul Kumar left", time: "3:00 pm", fromMe: false, type: "text", date: "2025-06-04" } ], }, { id: 6, name: "MICROSOFT TRAINING AN...", lastMessage: "~Diya: STUDENTS WHO HAVE BEEN S…", lastMessageTime: "06/06/2025", unreadCount: 8, avatar: "https://storage.googleapis.com/a1aa/image/6e0f2990-62f6-4f45-2b99-d6205625dd0d.jpg", messages: [ { id: 1, text: "~Diya: STUDENTS WHO HAVE BEEN S…", time: "06/06/2025", fromMe: false, type: "text", date: "2025-06-06" } ], }, { id: 7, name: "🥂 छत्तीसगढ़ मराठा🥂", lastMessage: "~Nitish Kumar 😊 ☺️ This message…", lastMessageTime: "06/06/2025", unreadCount: 168, avatar: "https://storage.googleapis.com/a1aa/image/bc8fad23-d4b9-450e-b068-ae9f599ec340.jpg", messages: [ { id: 1, text: "~Nitish Kumar 😊 ☺️ This message…", time: "06/06/2025", fromMe: false, type: "text", date: "2025-06-06" } ], }, { id: 8, name: "Kunal Punia Gurgaon University", lastMessage: "✓✓ Tu kar va liya??", lastMessageTime: "06/06/2025", unreadCount: 0, avatar: "https://storage.googleapis.com/a1aa/image/ef6b136a-c7bc-4355-0255-1eeb831f94b8.jpg", messages: [ { id: 1, text: "ok", time: "6:31 am", fromMe: false, type: "text", date: "2025-06-05" }, { id: 2, text: "Hmm, bag may hai phone\nAa raha hai", time: "6:31 am", fromMe: true, type: "text", date: "2025-06-05" }, { id: 3, text: "Noc", time: "12:18 pm", fromMe: false, type: "text", date: "2025-06-06" }, { id: 4, text: "Sign kerva le", time: "12:20 pm", fromMe: false, type: "text", date: "2025-06-06" }, { id: 5, text: "aman_Resume.pdf", time: "9:45 am", fromMe: true, type: "file", date: "2025-06-06", file: { name: "aman_Resume.pdf", size: "233 KB", description: "Firefox PDF Document", preview: "https://storage.googleapis.com/a1aa/image/7431d12a-e980-4a45-4fb7-059b75511589.jpg", icon: "https://storage.googleapis.com/a1aa/image/56c2693c-5bd3-411a-09df-d29721bf920b.jpg", downloadUrl: "#" }}, { id: 6, text: "Tu kar va liya??", time: "12:31 pm", fromMe: true, type: "text", date: "2025-06-06" } ], }, { id: 9, name: "BRAND HUT SPORTS WEAR", lastMessage: "~जय जय सिया राम 🚩🚩🚩🚩 Image", lastMessageTime: "06/06/2025", unreadCount: 126, avatar: "https://storage.googleapis.com/a1aa/image/8fccce80-0a65-4b74-052c-2243c197288e.jpg", messages: [ { id: 1, text: "~जय जय सिया राम 🚩🚩🚩🚩 Image", time: "06/06/2025", fromMe: false, type: "text", date: "2025-06-06" } ], }, { id: 10, name: "Chhattisgarh sports 🏅🏅...", lastMessage: "Kasim Bhai Xmxtbbs", lastMessageTime: "06/06/2025", unreadCount: 59, avatar: "https://storage.googleapis.com/a1aa/image/f5828e68-6a4d-4c27-d900-2adcd97bb36e.jpg", messages: [ { id: 1, text: "Kasim Bhai Xmxtbbs", time: "06/06/2025", fromMe: false, type: "text", date: "2025-06-06" } ], }, ]; // State let selectedChatId = null; // Elements const chatListUl = document.getElementById("chatListUl"); const chatHeaderName = document.getElementById("chatHeaderName"); const chatHeaderImg = document.getElementById("chatHeaderImg"); const chatHeaderActions = document.getElementById("chatHeaderActions"); const messagesContainer = document.getElementById("messagesContainer"); const dateLabel = document.getElementById("dateLabel"); const dateLabelText = document.getElementById("dateLabelText"); const messageForm = document.getElementById("messageForm"); const messageInput = document.getElementById("messageInput"); const searchInput = document.getElementById("searchInput"); // Utility: format date label function formatDateLabel(dateStr) { const date = new Date(dateStr); const today = new Date(); const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); if ( date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear() ) { return "Today"; } if ( date.getDate() === yesterday.getDate() && date.getMonth() === yesterday.getMonth() && date.getFullYear() === yesterday.getFullYear() ) { return "Yesterday"; } // Return weekday name return date.toLocaleDateString(undefined, { weekday: "long" }); } // Render chat list function renderChatList(filter = "") { chatListUl.innerHTML = ""; const filteredChats = chats.filter((chat) => chat.name.toLowerCase().includes(filter.toLowerCase()) ); filteredChats.forEach((chat) => { const li = document.createElement("li"); li.className = "flex items-center px-4 py-3 cursor-pointer hover:bg-[#2a2a2a]" + (chat.id === selectedChatId ? " bg-[#2a2a2a] border-l-4 border-[#25d366]" : ""); li.dataset.chatId = chat.id; // Avatar let avatarEl; if (chat.avatar) { avatarEl = document.createElement("img"); avatarEl.src = chat.avatar; avatarEl.alt = `Profile picture of ${chat.name}`; avatarEl.className = "rounded-full w-10 h-10 object-cover"; avatarEl.width = 40; avatarEl.height = 40; } else { avatarEl = document.createElement("div"); avatarEl.className = "flex-shrink-0 rounded-full bg-gray-600 w-10 h-10 flex items-center justify-center text-gray-400 text-sm font-semibold"; avatarEl.textContent = chat.name.charAt(0); } li.appendChild(avatarEl); // Info container const infoDiv = document.createElement("div"); infoDiv.className = "ml-3 flex-1 min-w-0"; // Top row: name and time const topRow = document.createElement("div"); topRow.className = "flex justify-between items-center"; const nameP = document.createElement("p"); nameP.className = "text-white font-semibold text-sm truncate"; if (chat.name.length > 25) { nameP.className = "text-white font-semibold text-xs truncate font-bold"; } nameP.textContent = chat.name; topRow.appendChild(nameP); const timeP = document.createElement("p"); timeP.className = chat.unreadCount > 0 ? "text-[#25d366] text-xs" : "text-gray-400 text-xs"; timeP.textContent = chat.lastMessageTime; topRow.appendChild(timeP); infoDiv.appendChild(topRow); // Bottom row: last message and unread count const bottomRow = document.createElement("div"); bottomRow.className = "flex items-center space-x-1 text-gray-400 text-xs truncate"; // For last message, parse icons and text if (chat.lastMessage.includes("Voice call")) { const icon = document.createElement("i"); icon.className = "fas fa-phone-alt text-xs"; bottomRow.appendChild(icon); const span = document.createElement("span"); span.textContent = " " + chat.lastMessage.replace("Voice call", "").trim(); bottomRow.appendChild(span); } else if (chat.lastMessage.includes("Image")) { if (chat.lastMessage.includes("Image")) { const textPart = chat.lastMessage.replace("Image", "").trim(); if (textPart) { const span = document.createElement("span"); span.textContent = textPart; bottomRow.appendChild(span); } const icon = document.createElement("i"); icon.className = "far fa-image text-xs"; bottomRow.appendChild(icon); const span2 = document.createElement("span"); span2.textContent = " Image"; bottomRow.appendChild(span2); } else { const span = document.createElement("span"); span.textContent = chat.lastMessage; bottomRow.appendChild(span); } } else { const span = document.createElement("span"); span.textContent = chat.lastMessage; bottomRow.appendChild(span); } infoDiv.appendChild(bottomRow); li.appendChild(infoDiv); // Unread count badge if (chat.unreadCount > 0) { const badge = document.createElement("div"); badge.className = "ml-2"; const badgeSpan = document.createElement("span"); badgeSpan.className = "bg-[#25d366] text-black text-xs font-semibold rounded-full px-2 py-0.5"; badgeSpan.textContent = chat.unreadCount; badge.appendChild(badgeSpan); li.appendChild(badge); } li.addEventListener("click", () => { selectChat(chat.id); }); chatListUl.appendChild(li); }); } // Render messages for selected chat function renderMessages(chat) { messagesContainer.innerHTML = ""; if (!chat) return; let lastDate = null; chat.messages.forEach((msg) => { // Show date label if date changes if (msg.date !== lastDate) { lastDate = msg.date; const dateDiv = document.createElement("div"); dateDiv.className = "flex justify-center"; const dateSpan = document.createElement("span"); dateSpan.className = "bg-[#1a1a1a] text-gray-400 text-xs px-2 py-0.5 rounded"; dateSpan.textContent = formatDateLabel(msg.date); dateDiv.appendChild(dateSpan); messagesContainer.appendChild(dateDiv); } // Message container const msgDiv = document.createElement("div"); msgDiv.className = "flex max-w-xs " + (msg.fromMe ? "justify-end ml-auto" : "justify-start"); msgDiv.style.wordBreak = "break-word"; // Message bubble const bubble = document.createElement("div"); bubble.className = (msg.fromMe ? "bg-[#075e54] text-white" : "bg-[#2a2a2a] text-gray-400") + " text-xs rounded px-2 py-1 whitespace-pre-wrap"; if (msg.type === "text") { bubble.textContent = msg.text; } else if (msg.type === "file" && msg.file) { // File message structure bubble.className = (msg.fromMe ? "bg-[#075e54]" : "bg-[#2a2a2a]") + " rounded border-2 border-[#075e54] w-full max-w-[280px]"; bubble.innerHTML = ` <img src="${msg.file.preview}" alt="Blurred preview of a resume document with green border" class="rounded-t w-full max-h-[140px] object-cover" /> <div class="px-3 py-2 border-t border-[#064d40]"> <div class="flex items-center space-x-2 mb-1"> <img src="${msg.file.icon}" alt="PDF icon" class="w-5 h-5 object-contain" /> <div class="flex flex-col text-white text-xs truncate"> <span class="font-semibold truncate">${msg.file.name}</span> <span class="text-gray-300">${msg.file.size}, ${msg.file.description}</span> </div> </div> <button class="w-full bg-[#064d40] hover:bg-[#0a6a4a] text-white text-xs rounded py-1 text-center" type="button" onclick="alert('Downloading ${msg.file.name}')">Download</button> </div> <div class="text-gray-300 text-[10px] text-right px-2 py-1 select-text">${msg.time} <i class="fas fa-check-double"></i></div> `; // Append and return early to avoid adding time again below messagesContainer.appendChild(bubble); return; } // Time and double check icon for sent messages const timeSpan = document.createElement("span"); timeSpan.className = msg.fromMe ? "text-[#25d366] text-[10px] self-end ml-1" : "text-gray-500 text-[10px] self-end ml-1"; timeSpan.style.alignSelf = "flex-end"; timeSpan.style.marginLeft = "0.25rem"; timeSpan.textContent = msg.time; if (msg.fromMe) { const checkIcon = document.createElement("i"); checkIcon.className = "fas fa-check-double ml-1"; timeSpan.appendChild(checkIcon); } msgDiv.appendChild(bubble); msgDiv.appendChild(timeSpan); messagesContainer.appendChild(msgDiv); }); } // Select chat by id function selectChat(id) { selectedChatId = id; renderChatList(searchInput.value); const chat = chats.find((c) => c.id === id); if (!chat) return; chatHeaderName.textContent = chat.name; chatHeaderImg.src = chat.avatar || "https://placehold.co/40x40?text=NA&bg=6b7280&fg=fff"; chatHeaderImg.alt = `Profile picture of ${chat.name}`; chatHeaderActions.style.visibility = "visible"; renderMessages(chat); scrollMessagesToBottom(); messageInput.focus(); } // Scroll messages container to bottom function scrollMessagesToBottom() { const chatMessages = document.getElementById("chatMessages"); chatMessages.scrollTop = chatMessages.scrollHeight; } // Send message handler messageForm.addEventListener("submit", (e) => { e.preventDefault(); const text = messageInput.value.trim(); if (!text || selectedChatId === null) return; const chat = chats.find((c) => c.id === selectedChatId); if (!chat) return; // Add new message const now = new Date(); const timeStr = now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); const dateStr = now.toISOString().split("T")[0]; chat.messages.push({ id: chat.messages.length + 1, text, time: timeStr, fromMe: true, type: "text", date: dateStr, }); chat.lastMessage = text; chat.lastMessageTime = timeStr; chat.unreadCount = 0; messageInput.value = ""; renderChatList(searchInput.value); renderMessages(chat); scrollMessagesToBottom(); }); // Search input handler searchInput.addEventListener("input", () => { renderChatList(searchInput.value); }); // Initial render renderChatList(); // Select default chat (Kunal Punia Gurgaon University) selectChat(8);
PHP Code
NO Need
🧩 Tech Stack Used
-
HTML5 – Semantic structure and layout
-
Tailwind CSS – Utility-first styling for responsiveness
-
JavaScript (Vanilla) – Handling dynamic behavior and DOM updates
📈 Final Thoughts
This WhatsApp Clone project proves how powerful and flexible modern web technologies can be — even without using heavy frameworks. It serves as an excellent starting point for building full-fledged messaging platforms, UI prototypes, or simply sharpening your front-end skills.
If you’re passionate about building real-world UI interfaces that not only look good but also feel interactive — this is the project for you.
❓FAQs
Q1. Is this WhatsApp Clone connected to any backend?
No, this is a front-end UI-only template. However, it's designed to be easily connected to real-time backends like Firebase or Socket.io.
Q2. Is this WhatsApp clone responsive?
Yes, the entire interface is mobile-first and adapts perfectly across devices using Tailwind CSS.
Q3. Can I customize this template?
Absolutely. The code is well-commented and modular, making it beginner-friendly and easy to customize.
Q4. What makes this a good portfolio project?
It demonstrates real-world UI skills, responsive design, event handling, and interactive elements — all essential in modern web development.