Real-Time Chat UI
Live message stream with optimistic sends and auto-scroll.
How it works
A live chat room: messages from others appear the moment they're sent, yours show up before the server confirms them, and the view only follows new messages while you're reading the bottom. The hard parts are invisible in the happy path: dropped connections, duplicate or missing messages, and scrolling that jumps under the reader.
- One order for everyone: the server numbers every message (
seq); every tab sorts and de-duplicates by it. - Optimistic, but honest: your message appears instantly, and a failed send stays visible with Retry.
- Resume, don't restart: after a drop the client asks for “everything after my last message”.
- Scroll that respects the reader: follow new messages only at the bottom; older history loads without a jump.
Read moreShow less
Where you'd use it
- Team and customer-support chat, comment threads that update live.
- Activity feeds and logs that stream in while someone reads them.
- Any “live” screen on a flaky network: mobile users switch networks and close laptops all the time.
- Collaborative tools that need everyone to see the same sequence of events.
How we built it
Messages arrive over one long-lived Server-Sent Events response, and are sent with normal POST requests. The stream is read by hand with fetch and a small parser instead of EventSource, so reconnecting is our code: delays that double from 0.5 s up to 15 s with random jitter, a pause while offline, and a server ping every 15 s so a silently dead stream is noticed after 35 s. Every reconnect asks for after=<last seq>; the server replays what was missed, or sends a reset with the latest page if you were away longer than its 500-message window.
Each message you send gets an id from your browser first. It shows right away, and whichever arrives first, the server's echo on the stream or the POST response, turns it into the real message in place. Sends go out one at a time so your messages keep the order you typed them, and Retry reuses the same id, so the server never stores a message twice. Unsent messages are kept in localStorage and come back after a reload.
The message list tracks whether you're within 48 px of the bottom. Only then do new messages scroll it, before paint. When older messages load above, the message you were reading is put back exactly where it was. New messages are announced through one screen-reader status region, batched once a second.
Best practices
Do: Give every message a server sequence number and de-duplicate by it.
Avoid: Trusting arrival order or timestamps: replays, retries and echoes will duplicate or reorder messages.
Do: Make sends idempotent with a client-generated id, and keep failed messages on screen with Retry and Discard.
Avoid: Retrying with a new id (duplicates after a lost response) or silently dropping text the user typed.
Do: Reconnect with growing, randomized delays, stop while offline, and resume from the last message you have.
Avoid: Reconnecting in a tight loop, or reloading the whole conversation after every drop.
Do: Auto-scroll only when the reader is at the bottom, and keep their place when content is added above.
Avoid: Yanking the view to the newest message while someone is reading older ones.
Do: Announce new messages through one polite region, summarized during bursts.
Avoid: Making the whole list a live region: a burst becomes 50 interruptions.
Try it with the demo controls
- Latency
- Set 5000 ms and send: your message appears instantly as “Sending…”, then turns “Delivered” in place, never twice.
- Send / history failure
- At 100%, some sends show “Not sent” with Retry and Discard; others still succeed because the server stored them but the response was lost. Retry at 0% delivers each exactly once.
- Simulated people
- Normal: bots type, post, reply to you and mark your messages “Seen”. Off: a quiet room for testing on your own.
- Burst (50 messages)
- Scroll up first: the view doesn't move, an “N new messages” button appears, and the “New” divider marks where unread starts.
- Drop connection
- The badge goes Reconnecting, then Live; Stream stats count the messages replayed while you were away. Press it several times quickly to see the delay grow.
- Go offline
- Sends fail at once with “You're offline”; after 10 s a “Connection lost” toast appears. Switch it back: missed messages fill in and a “Reconnected” toast shows.
- Clear conversation
- Empties the room for everyone. Every tab shows “Conversation cleared by …” and a toast, including a tab that was offline when it happened.
- Open another tab
- Put the tabs side by side: messages, typing and “Seen” cross between them, and each tab shows only its own messages as “You”.
- Scroll to the top
- Older messages load in pages of 30 without moving what you're reading, until “Beginning of conversation”.
- Stream stats
- Live counters for messages received, replayed after a reconnect, duplicates dropped and resets: the numbers behind each experiment.
Trade-offs and limits
- One room, plain text only: no direct messages, editing, reactions, attachments or sign-in.
- The room lives in server memory and resets when the server restarts.
- No list virtualization (that's practice 7); a long session keeps every message in the page.
- Unsent messages are kept per browser, so two tabs both restore them; a tab that missed two clears gets one notification.
Demo controls
# general
Connecting…Connection: Connecting…You are
- Loading messages…
Stream stats
- Messages loaded
- 0
- Waiting to send
- 0
- Received live
- 0
- Replayed on reconnect
- 0
- Duplicates dropped
- 0
- Resets (gap too old)
- 0
- Reconnect attempt
- 0