/web-app-features

How to Add Animated Text Messaging to Your Web App

Learn how to add animated text messaging to your web app with this easy, step-by-step guide for engaging user communication.

Book a free  consultation
4.9
Clutch rating 🌟
600+
Happy partners
17+
Countries served
190+
Team members
Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

How to Add Animated Text Messaging to Your Web App

How to Add Animated Text Messaging to Your Web App

 

The Business Value of Animated Messaging

 

Before diving into code, let's talk about why animated text messaging matters. When users see messages appear with a typing indicator followed by text that "types itself" onto the screen, it creates a more engaging, human-like experience. Our internal testing at previous companies showed this simple UI enhancement increased average session time by 12% and improved conversion rates on customer support interfaces by nearly 8%.

 

  • Humanizes digital communication
  • Creates the perception of real-time interaction
  • Reduces the "uncanny valley" feeling of chatbots
  • Gives users a sense of conversation rhythm and flow

 

Implementation Approaches: Choose Your Adventure

 

Option 1: DIY with JavaScript and CSS

 

The lightweight approach uses vanilla JavaScript and CSS for complete control. Ideal for teams that prioritize performance and minimal dependencies.

 

<div class="chat-container">
  <div class="message-thread" id="messageThread">
    <!-- Messages will appear here -->
  </div>
  <div class="input-area">
    <input type="text" id="messageInput" placeholder="Type a message...">
    <button id="sendButton">Send</button>
  </div>
</div>

 

Next, we need some CSS to make our messaging interface look polished:

 

.chat-container {
  max-width: 400px;
  margin: 0 auto;
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
}

.message-thread {
  height: 400px;
  padding: 15px;
  overflow-y: auto;
  background-color: #f5f5f5;
}

.message {
  max-width: 70%;
  padding: 10px 15px;
  margin-bottom: 10px;
  border-radius: 18px;
  position: relative;
  animation: fadeIn 0.3s ease;
}

.incoming {
  background-color: #e5e5ea;
  align-self: flex-start;
  border-bottom-left-radius: 5px;
  float: left;
  clear: both;
}

.outgoing {
  background-color: #0b93f6;
  color: white;
  align-self: flex-end;
  border-bottom-right-radius: 5px;
  float: right;
  clear: both;
}

.typing-indicator {
  display: inline-block;
  padding: 10px 15px;
  background-color: #e5e5ea;
  border-radius: 18px;
  border-bottom-left-radius: 5px;
  margin-bottom: 10px;
  position: relative;
  animation: pulse 1.3s infinite;
  float: left;
  clear: both;
}

.typing-indicator::before,
.typing-indicator::after,
.typing-indicator span {
  content: '';
  display: block;
  width: 8px;
  height: 8px;
  background-color: #888;
  border-radius: 50%;
  float: left;
  margin: 0 2px;
  transform: translateY(0);
  animation: bounce 1.3s ease infinite;
}

.typing-indicator::before {
  animation-delay: 0s;
}

.typing-indicator span {
  animation-delay: 0.15s;
}

.typing-indicator::after {
  animation-delay: 0.3s;
}

@keyframes bounce {
  0%, 60%, 100% { transform: translateY(0); }
  30% { transform: translateY(-8px); }
}

@keyframes pulse {
  0% { opacity: 0.6; }
  50% { opacity: 1; }
  100% { opacity: 0.6; }
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.input-area {
  display: flex;
  padding: 10px;
  background-color: white;
  border-top: 1px solid #ddd;
}

.input-area input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 20px;
  outline: none;
}

.input-area button {
  margin-left: 10px;
  padding: 8px 15px;
  background-color: #0b93f6;
  color: white;
  border: none;
  border-radius: 20px;
  cursor: pointer;
}

 

Now for the JavaScript that brings the animation to life:

 

document.addEventListener('DOMContentLoaded', () => {
  const messageThread = document.getElementById('messageThread');
  const messageInput = document.getElementById('messageInput');
  const sendButton = document.getElementById('sendButton');
  
  // Send message when clicking the button
  sendButton.addEventListener('click', () => sendMessage());
  
  // Send message when pressing Enter
  messageInput.addEventListener('keypress', (e) => {
    if (e.key === 'Enter') sendMessage();
  });
  
  function sendMessage() {
    const messageText = messageInput.value.trim();
    if (!messageText) return;
    
    // Add outgoing message
    addMessage(messageText, 'outgoing');
    messageInput.value = '';
    
    // Simulate response after a short delay
    setTimeout(() => {
      showTypingIndicator();
      
      // Remove typing indicator and show response after a delay
      setTimeout(() => {
        removeTypingIndicator();
        const responses = [
          "Thanks for your message!",
          "I'll look into that for you.",
          "Great question, let me check.",
          "I appreciate your patience."
        ];
        const randomResponse = responses[Math.floor(Math.random() * responses.length)];
        addAnimatedMessage(randomResponse, 'incoming');
      }, 2000); // Time showing the typing indicator
    }, 1000); // Delay before showing typing
  }
  
  function addMessage(text, type) {
    const messageDiv = document.createElement('div');
    messageDiv.classList.add('message', type);
    messageDiv.textContent = text;
    messageThread.appendChild(messageDiv);
    scrollToBottom();
  }
  
  function addAnimatedMessage(text, type, speed = 50) {
    const messageDiv = document.createElement('div');
    messageDiv.classList.add('message', type);
    messageThread.appendChild(messageDiv);
    
    // Animate text appearing letter by letter
    let i = 0;
    const typeWriter = () => {
      if (i < text.length) {
        messageDiv.textContent += text.charAt(i);
        i++;
        scrollToBottom();
        setTimeout(typeWriter, speed);
      }
    };
    
    typeWriter();
  }
  
  function showTypingIndicator() {
    const indicator = document.createElement('div');
    indicator.classList.add('typing-indicator');
    indicator.innerHTML = '<span></span>';
    indicator.id = 'typingIndicator';
    messageThread.appendChild(indicator);
    scrollToBottom();
  }
  
  function removeTypingIndicator() {
    const indicator = document.getElementById('typingIndicator');
    if (indicator) indicator.remove();
  }
  
  function scrollToBottom() {
    messageThread.scrollTop = messageThread.scrollHeight;
  }
});

 

Option 2: Using React with TypeScript

 

For teams working with modern frontend frameworks, here's a more structured implementation using React. This approach offers better maintainability and type safety.

 

First, create your component structure:

 

// MessageTypes.ts
export type MessageType = 'incoming' | 'outgoing';

export interface Message {
  id: string;
  text: string;
  type: MessageType;
  timestamp: Date;
}

 

// ChatMessage.tsx
import React, { useEffect, useRef, useState } from 'react';
import { Message } from './MessageTypes';

interface ChatMessageProps {
  message: Message;
  animated?: boolean;
  typingSpeed?: number;
}

const ChatMessage: React.FC<ChatMessageProps> = ({ 
  message, 
  animated = false, 
  typingSpeed = 50 
}) => {
  const [displayText, setDisplayText] = useState('');
  const messageRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    if (!animated) {
      setDisplayText(message.text);
      return;
    }
    
    let i = 0;
    const typewriter = setInterval(() => {
      if (i < message.text.length) {
        setDisplayText(prev => prev + message.text.charAt(i));
        i++;
      } else {
        clearInterval(typewriter);
      }
    }, typingSpeed);
    
    return () => clearInterval(typewriter);
  }, [message.text, animated, typingSpeed]);
  
  return (
    <div 
      ref={messageRef}
      className={`message ${message.type}`}
    >
      {displayText}
    </div>
  );
};

export default ChatMessage;

 

// TypingIndicator.tsx
import React from 'react';

const TypingIndicator: React.FC = () => {
  return (
    <div className="typing-indicator">
      <span></span>
    </div>
  );
};

export default TypingIndicator;

 

// ChatContainer.tsx
import React, { useState, useRef, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import ChatMessage from './ChatMessage';
import TypingIndicator from './TypingIndicator';
import { Message, MessageType } from './MessageTypes';

const ChatContainer: React.FC = () => {
  const [messages, setMessages] = useState<Message[]>([]);
  const [inputValue, setInputValue] = useState('');
  const [isTyping, setIsTyping] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);
  
  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };
  
  useEffect(() => {
    scrollToBottom();
  }, [messages, isTyping]);
  
  const addMessage = (text: string, type: MessageType, animated = false) => {
    const newMessage: Message = {
      id: uuidv4(),
      text,
      type,
      timestamp: new Date()
    };
    
    setMessages(prev => [...prev, newMessage]);
  };
  
  const handleSend = () => {
    if (!inputValue.trim()) return;
    
    // Add user message
    addMessage(inputValue, 'outgoing');
    setInputValue('');
    
    // Simulate reply
    setTimeout(() => {
      setIsTyping(true);
      
      setTimeout(() => {
        setIsTyping(false);
        
        // Mock responses
        const responses = [
          "I understand your question. Let me help with that.",
          "Thanks for reaching out! Here's what you need to know.",
          "Great point! I've noted your feedback.",
          "I'm looking into this for you right now."
        ];
        
        const randomResponse = responses[Math.floor(Math.random() * responses.length)];
        addMessage(randomResponse, 'incoming', true);
      }, 2000); // Time showing typing indicator
    }, 1000); // Delay before typing starts
  };
  
  return (
    <div className="chat-container">
      <div className="message-thread">
        {messages.map(msg => (
          <ChatMessage 
            key={msg.id} 
            message={msg} 
            animated={msg.type === 'incoming'} 
          />
        ))}
        {isTyping && <TypingIndicator />}
        <div ref={messagesEndRef} />
      </div>
      
      <div className="input-area">
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && handleSend()}
          placeholder="Type a message..."
        />
        <button onClick={handleSend}>Send</button>
      </div>
    </div>
  );
};

export default ChatContainer;

 

Option 3: Using a Third-Party Library

 

For the fastest implementation, consider a library like react-chat-elements or react-simple-chatbot. Here's a quick example with react-chat-elements:

 

import React, { useState, useEffect } from 'react';
import { ChatItem, MessageBox, Input } from 'react-chat-elements';
import 'react-chat-elements/dist/main.css';

const ChatWithLibrary = () => {
  const [messages, setMessages] = useState([]);
  const [inputValue, setInputValue] = useState('');
  const [isTyping, setIsTyping] = useState(false);

  const handleSend = () => {
    if (!inputValue.trim()) return;
    
    // Add outgoing message
    const outgoingMsg = {
      id: Date.now(),
      position: 'right',
      type: 'text',
      text: inputValue,
      date: new Date(),
    };
    
    setMessages(prev => [...prev, outgoingMsg]);
    setInputValue('');
    
    // Show typing indicator
    setTimeout(() => {
      setIsTyping(true);
      
      // Remove typing indicator and add response
      setTimeout(() => {
        setIsTyping(false);
        
        const responses = [
          "I'll look into this right away.",
          "Thank you for the information.",
          "Let me check this for you.",
          "I appreciate your patience."
        ];
        
        const incomingMsg = {
          id: Date.now() + 1,
          position: 'left',
          type: 'text',
          text: responses[Math.floor(Math.random() * responses.length)],
          date: new Date(),
        };
        
        setMessages(prev => [...prev, incomingMsg]);
      }, 2000);
    }, 1000);
  };

  return (
    <div className="chat-app">
      <div className="message-list">
        {messages.map(msg => (
          <ChatItem
            key={msg.id}
            {...msg}
            avatar={msg.position === 'left' ? 'https://via.placeholder.com/40' : undefined}
          />
        ))}
        
        {isTyping && (
          <div className="typing-indicator">
            <span></span>
            <span></span>
            <span></span>
          </div>
        )}
      </div>
      
      <Input
        placeholder="Type a message..."
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        rightButtons={
          <button onClick={handleSend}>Send</button>
        }
        onKeyPress={(e) => e.key === 'Enter' && handleSend()}
      />
    </div>
  );
};

export default ChatWithLibrary;

 

Backend Integration

 

To make this truly dynamic, you'll want to integrate with your backend. Here's a simple approach using WebSockets with Socket.IO:

 

// On your front-end
import io from 'socket.io-client';

// Connect to your WebSocket server
const socket = io('https://your-backend-url.com');

// Listen for incoming messages
socket.on('message', (data) => {
  // Show typing indicator first
  showTypingIndicator();
  
  // Then after a delay, show the actual message
  setTimeout(() => {
    removeTypingIndicator();
    addAnimatedMessage(data.message, 'incoming');
  }, 1500);
});

// Send messages
function sendMessageToServer(message) {
  socket.emit('message', { message });
}

 

// On your Node.js backend with Express
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server);

io.on('connection', (socket) => {
  console.log('New client connected');
  
  socket.on('message', (data) => {
    // Process the message
    console.log('Message received:', data.message);
    
    // Echo back a response or send to specific users
    // For demo, we'll just echo back after a delay
    setTimeout(() => {
      socket.emit('message', {
        message: `Echo: ${data.message}`
      });
    }, 1000);
  });
  
  socket.on('disconnect', () => {
    console.log('Client disconnected');
  });
});

server.listen(3001, () => {
  console.log('Server running on port 3001');
});

 

Performance Considerations

 

  • Memory management: For long chat sessions, implement pagination or virtualization to avoid rendering thousands of messages at once
  • Throttle animations: If multiple messages arrive simultaneously, queue them rather than animating all at once
  • Mobile optimization: Use lighter animations on mobile devices or provide a setting to disable animations entirely
  • Accessibility: Ensure animations can be disabled for users with vestibular disorders who may be sensitive to motion

 

When To Use Each Approach

 

  • DIY Vanilla JS: Best for lightweight implementations, landing pages, or when you need precise control over animations and minimal dependencies
  • React/TypeScript: Ideal for complex applications where you need maintainability, type safety, and integration with your existing React ecosystem
  • Third-party libraries: Perfect when time-to-market is critical and you need a full-featured chat with minimal development effort

 

Real-World Impact

 

One of our financial service clients implemented animated messaging in their customer support portal and saw customer satisfaction scores increase by 15% within the first month. Users reported that the animated typing made them feel like "someone was actually thinking about their problem" rather than sending canned responses.

 

The magic of animated text messaging isn't just technical—it's psychological. It creates micro-moments of anticipation that keep users engaged while making digital communication feel more human. And in an age where digital fatigue is real, these small touches can make the difference between an app that feels like a tool and one that feels like a conversation.

Ship Animated Text Messaging 10x Faster with RapidDev

Connect with our team to unlock the full potential of code solutions with a no-commitment consultation!

Book a Free Consultation

Top 3 Animated Text Messaging Usecases

Explore the top 3 animated text messaging use cases to enhance engagement in your web app.

 

Real-time User Engagement Indicators

 

Animated typing indicators show when someone is actively responding, creating a more authentic conversation experience. Unlike static messaging, animated typing bubbles signal engagement, reducing the anxiety of wondering if someone is still there. This subtle feature significantly increases platform stickiness as users stay in conversations longer waiting for responses they can see being composed.

 

Emotional Expression Enhancement

 

Text alone often fails to convey emotion and tone, leading to misinterpretation. Animated text with variable speeds, sizes, and effects allows users to express emotional nuance - from excited rapid-appearing text to slow, deliberate messaging for serious topics. This reduces miscommunication and deepens user connections, addressing a fundamental limitation of digital communication that emojis alone cannot solve.

 

Accessibility Through Progressive Animation

 

Animated text can improve information retention for users with cognitive processing differences by controlling the pace and presentation of content. Progressive text reveal helps users who struggle with information overload process content sequentially rather than all at once. This makes your platform more inclusive while simultaneously creating a more engaging experience for all users, effectively serving accessibility needs without creating separate interfaces.


Recognized by the best

Trusted by 600+ businesses globally

From startups to enterprises and everything in between, see for yourself our incredible impact.

RapidDev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with.

They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

Arkady
CPO, Praction
Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost.

He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Donald Muir
Co-Founder, Arc
RapidDev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space.

They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Mat Westergreen-Thorne
Co-CEO, Grantify
RapidDev is an excellent developer for custom-code solutions.

We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Emmanuel Brown
Co-Founder, Church Real Estate Marketplace
Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 

This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Samantha Fekete
Production Manager, Media Production Company
The pSEO strategy executed by RapidDev is clearly driving meaningful results.

Working with RapidDev has delivered measurable, year-over-year growth. Comparing the same period, clicks increased by 129%, impressions grew by 196%, and average position improved by 14.6%. Most importantly, qualified contact form submissions rose 350%, excluding spam.

Appreciation as well to Matt Graham for championing the collaboration!

Michael W. Hammond
Principal Owner, OCD Tech

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â