Feature spec
IntermediateCategory
communication-social
Build with AI
3–6 hours with Lovable or v0
Custom build
1–2 weeks custom dev
Running cost
$0–$25/mo up to 1K users
Works on
Everything it takes to ship Animated Text Messaging — parts, prompts, and real costs.
Animated text messaging layers Framer Motion bubble animations, emoji reaction bursts, and message effects (shake, loud, invisible ink) onto an existing chat feature. With Lovable or v0 you can ship it in 3–6 hours for $0–$25/month — all animations run client-side. The main cost is Supabase Realtime for the typing presence channel, which stays free up to 200 concurrent connections.
What Animated Text Messaging Actually Is
Animated text messaging is the animation and effects layer that sits on top of a chat feature: message bubbles that pop in from the sender's side, emoji reactions that burst into confetti, typing indicators with bouncing dots, and special message effects — shake, loud (enlarged bold text), invisible ink (blur-reveal on tap). The underlying messages still live in your Supabase `messages` table; this feature adds an `effect` column and a `reactions` jsonb column, then wires Framer Motion to drive the visual layer. The key product decision is accessibility: every animation must fall back to an instant transition when the user has enabled reduced-motion in their OS settings.
What users consider table stakes in 2026
- Message bubbles animate in from the sender's side with a spring easing on first render and do not replay on scroll
- Emoji reaction picker triggers a visible confetti or floating emoji burst immediately, with an optimistic count update before the server confirms
- Typing indicator shows three bouncing dots within 500 ms of the other user typing and clears within 3 seconds of them stopping
- Message effects (shake, loud, invisible ink) are selectable per-message before sending, not applied globally
- All animations respect the OS prefers-reduced-motion setting and degrade to instant transitions
- Exit animations play when a message is soft-deleted so the list does not jump
Anatomy of the Feature
Six components — AI tools handle the Framer Motion bubble and typing indicator well on the first pass. The effect enum, confetti guard, and reduced-motion hook are where builds consistently fall short without explicit prompting.
Message bubble animation
UIA Framer Motion `motion.div` wrapping each message bubble with `initial`, `animate`, and `exit` variants. Outgoing messages slide in from the right with a spring; incoming from the left. `AnimatePresence` handles exit when a message is deleted.
Note: Always key on `message.id` (UUID), never array index — React recycles DOM nodes on index-keyed lists and the exit animation fires on the wrong bubble.
Emoji reaction overlay
UIReaction picker rendered in a Framer Motion `AnimatePresence` popover. On selection, `canvas-confetti` fires a burst from the reaction button position. Reaction counts are stored as a jsonb map `{ '❤️': ['user-id-1', 'user-id-2'] }` in the `messages.reactions` column; UI updates optimistically before the Supabase PATCH returns.
Note: `canvas-confetti` is not in Lovable's default dependencies — request it explicitly in the prompt. Call it inside a `useEffect` with a `justReacted` boolean guard, not directly in the render path.
Typing indicator
UIThree dots animated with a CSS `@keyframes` bounce, staggered by 100 ms per dot. Presence state is driven by Supabase Realtime Presence (`channel.track({ typing: true })`), not polling.
Note: Set a 3-second client-side timeout to clear the indicator if no new Presence event arrives — browser tabs backgrounded stop sending heartbeats and `onLeave` may not fire promptly.
Message effect system
DataThe `messages` table gains an `effect` column typed as text with a CHECK constraint enforcing `'none' | 'shake' | 'loud' | 'invisible_ink'`. Each value maps to a Framer Motion variant or CSS class applied to the bubble on render.
Note: Shake: `keyframes` x-translate oscillation. Loud: font-size 1.25× + font-weight 700. Invisible ink: CSS `filter: blur(12px)` removed on click via React state, with a 0.4 s ease transition.
Invisible ink reveal
UIThe message renders blurred (`filter: blur(12px)`) until the user taps or clicks it. Reveal state lives in `localStorage` keyed by `message.id` so messages stay revealed after page refresh — important because refetching the messages list would otherwise re-blur everything.
Note: Do not store reveal state in React component state — it resets on unmount. `localStorage.setItem('revealed:' + message.id, '1')` is the simplest correct approach.
Accessibility guard
UIFramer Motion's `useReducedMotion()` hook reads the OS `prefers-reduced-motion: reduce` media query. When it returns `true`, all `motion.div` components receive `transition={{ duration: 0 }}` and `canvas-confetti` is skipped entirely.
Note: This is not optional — WCAG 2.1 SC 2.3.3 requires animation to be suppressible. Lovable will not add this without an explicit request in the prompt.
The data model
This feature extends the existing `messages` table rather than creating a new one. Run this in the Supabase SQL editor to add the required columns and update the RLS policies inherited from the parent chat feature:
1-- Add effect and reactions columns to existing messages table2alter table public.messages3 add column if not exists effect text not null default 'none'4 check (effect in ('none', 'shake', 'loud', 'invisible_ink')),5 add column if not exists reactions jsonb not null default '{}';67-- Index for filtering by effect (e.g. show all 'invisible_ink' messages)8create index if not exists messages_effect_idx9 on public.messages (effect)10 where effect <> 'none';1112-- RLS: participants can UPDATE reactions and effect on their own sent messages13-- (assumes conversation_participants table from parent in-app-messaging feature)14create policy "sender can update own message effect"15 on public.messages for update16 using (auth.uid() = sender_id)17 with check (auth.uid() = sender_id);1819-- Allow any conversation participant to update reactions jsonb20create policy "participant can react to messages"21 on public.messages for update22 using (23 auth.uid() in (24 select user_id from public.conversation_participants25 where conversation_id = messages.conversation_id26 )27 );Heads up: If you are building animated messaging on top of the real-time-chat feature (channel-based, not DM), replace `conversation_participants` membership check with `room_members` from that schema.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Best path for builders who already have a Lovable chat project — Framer Motion is pre-installed and Supabase Realtime is auto-wired. The effect system and confetti guard need explicit instructions to be built correctly.
Step by step
- 1Open your existing Lovable chat project and connect Lovable Cloud if not already connected
- 2Paste the prompt below in Agent Mode, scoping it with @messages-component if you have an existing message component
- 3After generation, open the Lovable preview on a real phone — animations are visible in the desktop preview but Presence-based typing needs two browser tabs to test
- 4Publish and test with two accounts to verify typing indicator clears within 3 seconds and reactions update optimistically
Add animated message effects to the existing chat feature using Framer Motion. Wrap each message bubble in a `motion.div` with slide-in animation from the sender side using spring easing (stiffness 300, damping 30). Use `AnimatePresence` with `key={message.id}` (never array index) so deleted messages animate out. Add 4 message effects selectable before sending: 'shake' (CSS x-oscillation keyframes), 'loud' (font-size 1.25rem, font-weight 700), 'invisible_ink' (filter: blur(12px) removed on click, reveal persisted in localStorage by message.id), and 'none' (default). Store the selected effect in a new `effect` text column on the messages table with CHECK constraint for the 4 values. Add an emoji reaction picker using a Framer Motion popover that fires `canvas-confetti` from the picker position when a reaction is selected — install canvas-confetti as a dependency. Store reactions as jsonb `{ emoji: [user_id_array] }` on the messages row and update optimistically before the server confirms. Add a typing indicator with 3 CSS bounce-animated dots driven by Supabase Realtime Presence (not polling); clear the indicator client-side after 3 seconds if no new Presence event arrives. Add `useReducedMotion()` from Framer Motion at the top level: when true, set all motion transitions to duration 0 and skip canvas-confetti. Animations must play only once per message on entry, never loop.Where this path bites
- canvas-confetti is not in Lovable's default package list — the AI may omit the `npm install` step; verify it appears in package.json after generation
- Invisible ink localStorage persistence is sometimes implemented as React state (wrong) — check that refreshing the page keeps messages revealed
- The Lovable desktop preview cannot fully test Presence-based typing indicators — use two browser tabs on the published URL
Third-party services you'll need
All animations run client-side. The only service cost is Supabase Realtime for the typing presence channel:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Realtime Presence for typing indicators + message storage for effect and reactions columns | 200 concurrent Realtime connections, 500 MB DB | Pro $25/mo: 500 connections, 8 GB DB |
| canvas-confetti | Client-side confetti burst on emoji reaction (npm package, no API) | Free, MIT license | Free |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
All animations are CPU on the client device. Supabase free tier (200 concurrent connections) covers a small app. No paid services required.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Confetti fires on every re-render
Symptom: Calling `canvas-confetti()` directly in a React render function or in a `useEffect` without a dependency guard causes the animation to replay every time any parent component re-renders — which in a chat room can be dozens of times per second as messages arrive.
Fix: Gate the confetti call with a `justReacted` boolean state that is set to `true` on reaction click and reset to `false` inside a `setTimeout` of 1000 ms. Put the `canvas-confetti()` call in a `useEffect` that only runs when `justReacted === true`.
AnimatePresence exit animation fires on the wrong message
Symptom: When message components are keyed by array index (`key={index}`), React reuses DOM elements during list updates. If a message in the middle is deleted, the exit animation plays on the last item in the list instead of the deleted one — a visually jarring bug.
Fix: Always set `key={message.id}` where `message.id` is the UUID primary key from Supabase. Never use array index as the key for any animated list.
Invisible ink effect resets on page reload
Symptom: Storing the revealed state in React component state means it resets every time the component unmounts — which happens on page refresh, navigation, or when the messages list re-renders. Every message re-blurs, forcing the user to re-tap each one.
Fix: Persist reveal state in `localStorage` with the key `revealed:${message.id}`. On mount, check `localStorage.getItem('revealed:' + message.id)` to initialise the state. This survives page refreshes per device without touching the server.
Typing indicator stuck on 'typing...'
Symptom: Supabase Realtime Presence relies on heartbeats from the connected client. When a browser tab is backgrounded, throttled, or the user navigates away abruptly, the `onLeave` event may not fire promptly — leaving the typing indicator active indefinitely for other participants.
Fix: Set a client-side 3-second timeout in the receiver: when a `typing: true` Presence event arrives, start a timer. If no follow-up event arrives within 3 seconds, clear the typing state locally regardless of Presence updates.
Animations cause layout shift on mobile
Symptom: Using Framer Motion to animate `width`, `height`, or `margin` properties triggers browser reflow on every animation frame — causing Cumulative Layout Shift (CLS) on mobile devices, particularly during message entry. Slower Android devices drop to below 30fps.
Fix: Only animate `transform`-based properties: `x`, `y`, `scale`, and `opacity`. Never animate `width`, `height`, `padding`, or `margin` in message bubble animations. Use `transform: translateX()` instead of changing left/right positioning.
Best practices
Key every animated message component on its UUID (`key={message.id}`), never on its array index — incorrect keys are the single most common source of wrong-element exit animations
Wrap all animation logic in a `useReducedMotion()` check from Framer Motion and skip canvas-confetti entirely when it returns true — this is a WCAG 2.1 requirement, not a nice-to-have
Guard canvas-confetti calls with a `justReacted` boolean state in a useEffect to prevent animation replay on re-render
Persist invisible ink reveal state in localStorage keyed by message ID so users do not have to re-tap every message after page reload
Clear the typing indicator client-side with a 3-second timeout regardless of Presence events — backgrounded tabs stop sending heartbeats
Animate only transform and opacity properties in message bubbles — animating width, height, or layout properties causes reflow and frame drops on mid-range devices
Set reaction counts optimistically before the Supabase PATCH returns and roll back on error — the 50–200 ms server round-trip is noticeable in a high-frequency reaction scenario
Store the `effect` column as a CHECK-constrained text enum in Postgres, not a free-form string — invalid effects silently render as the default bubble without crashing
When You Need Custom Development
Framer Motion covers everyday chat animation thoroughly. Four scenarios push beyond what AI tools can ship:
- You need server-side animated thumbnails for push notification previews — requires a Puppeteer or Remotion pipeline, not client-side Framer Motion
- You need proprietary AI-generated animation effects per message content type (e.g., weather-aware background animations based on message text)
- You need AR sticker overlays that use the device camera — these require native SDKs (ARKit, ARCore) and cannot run in a web app
- You need WebGL particle effects at above 60fps for a flagship creative tool — requires Three.js or PixiJS, not CSS/Framer Motion
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
How do I add animations to chat messages in a Lovable app?
Framer Motion is already included in Lovable's stack. Wrap your message bubble in a `motion.div` with slide-in variants and use `AnimatePresence` in the parent list. The key requirement is using `key={message.id}` (the UUID) on each message component — using array index causes exit animations to fire on the wrong message. Paste the full prompt from the Build Paths section above to get the complete implementation in one pass.
Can I add emoji reactions with animations to my app?
Yes. Store reactions as a jsonb column on the messages table — a map of emoji to arrays of user IDs. On the UI side, install `canvas-confetti` and fire it from the picker button position when a reaction is selected. Update the UI optimistically before the Supabase PATCH returns so the reaction feels instant. The confetti call must be inside a useEffect with a boolean guard to prevent it replaying on every re-render.
What is the best library to animate message bubbles?
Framer Motion is the standard for React-based chat apps. It provides spring physics (stiffness and damping controls), `AnimatePresence` for smooth enter/exit, and `useReducedMotion()` for accessibility. For Flutter apps, the `animations` package provides equivalent capabilities. Avoid animating CSS `width` or `height` — only animate `transform` and `opacity` to keep frame rates high on mid-range devices.
How do I add an invisible ink effect to messages?
Apply `filter: blur(12px)` via CSS to the message bubble and remove it on click via React state. The critical implementation detail is persisting the revealed state in `localStorage` keyed by the message UUID — not in component state. Without this, every page reload re-blurs all messages and the user has to tap each one again. Add a 0.4 second ease transition so the reveal feels intentional rather than abrupt.
Will animations slow down my app on mid-range devices?
Only if you animate layout properties. Animating `width`, `height`, `padding`, or `margin` triggers browser reflow on every frame and drops below 30fps on mid-range Android. Stick to transform-based animations (`translateX`, `scale`, `opacity`) which run on the GPU compositor thread without reflow. Framer Motion's default spring animations already use transforms — the risk comes from customising them incorrectly.
How do I make animations accessible for users who prefer reduced motion?
Import `useReducedMotion` from Framer Motion and check its return value at the top of your chat component. When it returns `true`, set all motion transitions to `{ duration: 0 }` and skip the canvas-confetti call entirely. This respects the user's OS-level `prefers-reduced-motion: reduce` setting, which is required by WCAG 2.1 Success Criterion 2.3.3.
Can I add a typing indicator to my chat?
Use Supabase Realtime Presence rather than polling. When the user types, call `channel.track({ typing: true, user_id: currentUser.id })`. Other participants receive the Presence event and show the animated dots. Always add a 3-second client-side timeout to clear the indicator if no follow-up event arrives — browser tabs that are backgrounded stop sending heartbeats and the `onLeave` event may not fire promptly.
How do I add a confetti effect when someone reacts to a message?
Install `canvas-confetti` as a project dependency. In the reaction click handler, set a `justReacted` boolean state to `true`. In a `useEffect` that depends on `justReacted`, call `confetti({ origin: { x, y } })` using the button's getBoundingClientRect coordinates, then reset `justReacted` to `false` after 1 second. Never call confetti directly in the render function or it will replay on every re-render.
Need this feature production-ready?
RapidDev builds animated text messaging into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.