The problem
ResizeObserver API can be used to solve "how to keep chat window scrolled to the bottom" problem.
By default, browsers preserve scroll position from the top. This is because important content is at the top.
In chat window, important content is at the bottom: the last message. Instead of preserving top scroll position, we need to keep user pinned to bottom scroll position when user has scrolled all the way down. We can do this by scrolling to the bottom on every resize.
How to scroll to bottom on resize?
This is easily implemented with ResizeObserver.
The simplest implementation is:
.chat { overflow: scroll; } <div class="chat"> <!-- chat has the scrollbar --> <div class="chat-text"> <!-- chat-text contains chat text --> <div>jack: hi </div> <div>jill: hi </div> </div> </div let ro = new ResizeObserver( entries => { for (let e of entries) { let chat = e.target.parentNode; chat.scrollTop = chat.scrollHeight - chat.clientHeight; } }); ro.observe(document.querySelector('.chat-text'))
chat
has the scrollbar, and chat-text
contains the chat text. ResizeObserver observes chat-text
's size,
and scrolls chat
to bottom on every resize. Example 1 implements this.
You can try it out by in live demo at the bottom of this page by clicking
on Chat/Chatbot buttons.
Appending new messages to chat-text
will also trigger a resize,
and scroll to the bottom, which is great.
But, there is a problem. What happens if user has scrolled higher up trying to read older messages? Scrolling to the bottom would make user lose text they were reading. Our chat window implementation has a new constraint:
How to not lose user's scroll position?
We can use scroll
event to detect when user has scrolled.
There is no API to distinguish whether scroll event was initiated by user, or
javascript. This is problematic, because we only want to save user-initated scroll.
The hacky solution is to use flags to distinguish between user and programatic scroll.
It is implemented as an Example 2.
let ro = new ResizeObserver(); function initScrollPositionChat(chat) { let chatText = chat.firstElementChild; chatText.resizeHandler = entry => { let chat = entry.target.parentNode; // Scroll to bottom, unless user has scrolled. if (!chat.saveUserScroll) { chat.isResizeScrollEvent = true; chat.scrollTop = chat.scrollHeight - chat.clientHeight; } } ro.observe(chatText); chat.addEventListener('scroll', ev => { // Ignore scrolls generated by ResizeObserver if (chat.isResizeScrollEvent) { delete chat.isResizeScrollEvent; return; } // Save user's position unless scrolled to the bottom, if (chat.scrollTop != chat.scrollHeight - chat.clientHeight) { chat.saveUserScroll = true; } else { chat.saveUserScroll = false; } }); }
Example 3 extends Example 2 with smooth scrolling. Unfortunatelly, it has
to implement smooth scrolling from scratch, instead of using
scrollIntoView(behavior:'smooth')
. scrollIntoView fires scroll
events that cannot be distinguished from user initiated events.
For completeness, examples also implemented chat pruning to prevent chat window from getting too big.