Skip to main content
WebSockets Tutorial
CHAPTER 08 Beginner

Building a Real-Time Chat Application

Updated: May 14, 2026
25 min read

# CHAPTER 8

Building a Real-Time Chat Application

1. Introduction

It is time to put theory into practice. The classic "Hello World" of real-time web development is the chat application. In this chapter, we will design and build a modern, sleek Chat UI using TailwindCSS for styling and Alpine.js for reactive state management. We will connect this frontend to our WebSocket server, allowing users to type, send, and instantly view messages.

2. Learning Objectives

By the end of this chapter, you will be able to:
  • Design a responsive chat interface using TailwindCSS.
  • Use Alpine.js to manage WebSocket state (connected, disconnected).
  • Bind an Alpine component to incoming WebSocket messages.
  • Format complex JSON payloads for chat messages.

3. Beginner-Friendly Explanation

Building a chat app requires two main pieces: the visual interface (what you see) and the brain (the logic).
  • TailwindCSS gives us the paint and the layout—it makes the chat bubbles look like an iPhone messaging app.
  • Alpine.js is the brain. It holds a list of all messages. Whenever a new WebSocket message arrives, Alpine instantly notices the new data and automatically updates the screen, so we don't have to write messy document.getElementById code anymore.

4. Real-World Examples

The architecture we are building here is a simplified version of the logic powering applications like Discord, Slack, and Twitch chat. They all rely on a frontend state manager (like React, Vue, or Alpine) reacting to incoming WebSocket frames.

5. Step-by-Step Tutorial

Let's build the application step by step.

Step 1: Include TailwindCSS and Alpine.js via CDN in the <head>. Step 2: Create the HTML layout for the chat window (Header, Message List, Input Bar). Step 3: Define an Alpine component x-data="chatApp()" to hold our messages array and our socket. Step 4: Initialize the WebSocket inside Alpine's init() function. Step 5: Write the sendMessage function to push new texts over the socket and clear the input.

6. The Complete Chat Application

Here is the full code. Save it as chat.html and open it in your browser. It points to a public echo server so you can test it immediately.

7. HTML, Tailwind & Alpine.js Example

html
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Chat</title>
    <!-- TailwindCSS for Styling -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- Alpine.js for Logic -->
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body class="bg-gray-200 h-screen flex items-center justify-center font-sans">

    <!-- Alpine Component Scope -->
    <div x-data="chatApp()" class="w-full max-w-md bg-white rounded-lg shadow-xl overflow-hidden flex flex-col h-[600px]">
        
        <!-- Header -->
        <div class="bg-blue-600 text-white p-4 flex justify-between items-center shadow">
            <h1 class="font-bold text-lg">Global Chat</h1>
            <!-- Status Indicator -->
            <div class="flex items-center gap-2 text-sm">
                <span class="h-3 w-3 rounded-full" 
                      :class="connected ? &#039;bg-green-400' : 'bg-red-400'"></span>
                <span x-text="connected ? &#039;Online' : 'Offline'"></span>
            </div>
        </div>

        <!-- Message List (Scrollable) -->
        <div class="flex-1 p-4 overflow-y-auto bg-gray-50 flex flex-col gap-3" id="message-container">
            <!-- Loop through messages array -->
            <template x-for="(msg, index) in messages" :key="index">
                <div class="flex" :class="msg.isSelf ? &#039;justify-end' : 'justify-start'">
                    <div class="max-w-[75%] p-3 rounded-lg shadow-sm"
                         :class="msg.isSelf ? &#039;bg-blue-500 text-white rounded-br-none' : 'bg-white border rounded-bl-none'">
                        <p class="text-sm break-words" x-text="msg.text"></p>
                    </div>
                </div>
            </template>
        </div>

        <!-- Input Area -->
        <div class="p-4 bg-white border-t flex gap-2">
            <input type="text" 
                   x-model="newMessage" 
                   @keydown.enter="sendMessage"
                   placeholder="Type your message..." 
                   class="flex-1 border border-gray-300 rounded-full px-4 py-2 focus:outline-none focus:border-blue-500"
                   :disabled="!connected">
            
            <button @click="sendMessage" 
                    class="bg-blue-600 text-white rounded-full p-2 w-10 h-10 flex items-center justify-center hover:bg-blue-700 disabled:opacity-50"
                    :disabled="!connected || newMessage.trim() === &#039;'">
                ➤
            </button>
        </div>

    </div>

    <!-- Alpine Logic -->
    <script>
        function chatApp() {
            return {
                socket: null,
                connected: false,
                newMessage: &#039;',
                messages: [
                    // Initial welcome message
                    { text: "Welcome to the chat! Connect to begin.", isSelf: false }
                ],

                init() {
                    // Connect on load
                    this.socket = new WebSocket("wss://echo.websocket.events");

                    this.socket.onopen = () => {
                        this.connected = true;
                    };

                    this.socket.onmessage = (event) => {
                        // The echo server returns exactly what we send.
                        // In a real app, we'd parse JSON here.
                        this.messages.push({
                            text: event.data,
                            isSelf: false // It came from the server
                        });
                        this.scrollToBottom();
                    };

                    this.socket.onclose = () => {
                        this.connected = false;
                    };
                },

                sendMessage() {
                    if (this.newMessage.trim() === &#039;') return;

                    // Send to server
                    this.socket.send(this.newMessage);

                    // Add our own message to the UI instantly
                    this.messages.push({
                        text: this.newMessage,
                        isSelf: true
                    });

                    // Clear input
                    this.newMessage = &#039;';
                    this.scrollToBottom();
                },

                scrollToBottom() {
                    // Give Alpine a tick to render the new DOM element, then scroll
                    setTimeout(() => {
                        const container = document.getElementById(&#039;message-container');
                        container.scrollTop = container.scrollHeight;
                    }, 50);
                }
            }
        }
    </script>
</body>
</html>

8. JSON Message Structures

In a real application, you wouldn't just send plain text. You would send a structured JSON payload so the server knows who the message is from and what room they are in.
json
123456
{
  "action": "broadcast_message",
  "room_id": 42,
  "user_name": "Alice",
  "content": "Does anyone know Alpine.js?"
}

9. Alpine.js Reactivity

Why use Alpine.js? Without it, every time a message arrives, you would have to write document.createElement('div'), add 5 different Tailwind classes, format the text, and appendChild(). Alpine allows us to simply say this.messages.push(newMsg) and it automatically updates the HTML.

10. Best Practices

  • Optimistic UI Updates: Notice in the sendMessage function, we push the message to our own this.messages array *immediately*, without waiting for the server to echo it back. This makes the app feel infinitely fast to the user.
  • Auto-Scrolling: A chat app is useless if you have to manually scroll down to read new messages. Always implement an auto-scroll function when rendering a new message.

11. Common Mistakes

  • Cross-Site Scripting (XSS): If you use pure JavaScript and element.innerHTML = event.data, a malicious user can send a message containing <script>alert('Hacked')</script>, which will execute on everyone's computer. Alpine's x-text directive automatically escapes HTML, keeping you safe.

12. Mini Exercises

  1. 1. Look at the Alpine template x-for loop in Section 7.
  1. 2. Notice how we use a ternary operator msg.isSelf ? 'bg-blue-500' : 'bg-white' to style the chat bubbles differently based on who sent them. Change the blue color to bg-purple-500 and see how it looks.

13. Coding Challenges

Challenge 1: Modify the Alpine component so that when the user presses "Enter" while the newMessage field is completely empty, it does not send an empty message. (Hint: check for .trim()).

14. MCQs with Answers

Question 1

Why is Alpine's x-text safer than injecting raw HTML when displaying chat messages?

Question 2

What is "Optimistic UI Update"?

15. Interview Questions

  • Q: Explain why state management libraries (like Alpine, React, or Vue) pair so well with WebSockets.
  • Q: How do you handle auto-scrolling to the bottom of a chat div dynamically as new messages arrive?

16. FAQs

Q: Can I use jQuery instead of Alpine.js? A: Yes, you can use vanilla JS or jQuery, but DOM manipulation becomes very messy and error-prone as your chat app gets more complex (e.g., adding user avatars, timestamps, and read receipts).

17. Summary

In Chapter 8, we elevated our basic WebSocket script into a professional-grade user interface. By combining TailwindCSS for layout styling and Alpine.js for reactive data handling, we created a chat interface that cleanly handles sending messages, receiving data, auto-scrolling, and indicating connection status.

18. Next Chapter Recommendation

Our client interface is beautiful, but right now we are just talking to an "echo" server. To make a real chat app, we need to send a message to the server, and the server needs to send it to *everyone else*. Proceed to Chapter 9: Broadcasting Messages to Multiple Clients to understand backend routing.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·