I'm building a chat interface for a website. When it first loads, the most recent messages are displayed, with the newest at the bottom, as is mon with most chat apps.
As the user scrolls up, more messages are loaded via our API, and inserted above the existing messages.
As you might expect, the chat bubbles are styled <div>
s inside a container <div>
that has overflow-y: auto
and a set height.
Currently what I'm doing is noting the top message <div>
, loading the older messages above it, and then repositioning the view to try and put the user back to almost where they left off, but it's tricky, especially when the chat bubbles contain dynamically loading elements (embedded images, etc). (I do not know the height of these bubbles before the browser renders them.)
In an ideal world, I would like to find a way to insert the messages above, without causing the scrolled position to move at all, so that the user doesn't lose track of where they are in the stream of messages.
I've never heard of such a thing, but it would be cool if there was a way to tell the div to stretch itself in the upward direction, rather than causing it to push the existing messages down further.
Any advice would be appreciated.
Here is a JSFiddle that shows the basic problem. It simulates my chat interface, by loading 30 chat messages when the page loads, and then puts you at the bottom of the message list. As you scroll to the top, another 30 are loaded, but you'll see that this is jarring to the UX because you instantly lose your place.
UPDATE: Thanks to Richard's answer, with a technique based on requestAnimationFrame
to re-align the viewport. (For my use of it, I utilized the scrollTop
of my scrollable div, rather than window.scrollTo
as shown in his example.)
I'm building a chat interface for a website. When it first loads, the most recent messages are displayed, with the newest at the bottom, as is mon with most chat apps.
As the user scrolls up, more messages are loaded via our API, and inserted above the existing messages.
As you might expect, the chat bubbles are styled <div>
s inside a container <div>
that has overflow-y: auto
and a set height.
Currently what I'm doing is noting the top message <div>
, loading the older messages above it, and then repositioning the view to try and put the user back to almost where they left off, but it's tricky, especially when the chat bubbles contain dynamically loading elements (embedded images, etc). (I do not know the height of these bubbles before the browser renders them.)
In an ideal world, I would like to find a way to insert the messages above, without causing the scrolled position to move at all, so that the user doesn't lose track of where they are in the stream of messages.
I've never heard of such a thing, but it would be cool if there was a way to tell the div to stretch itself in the upward direction, rather than causing it to push the existing messages down further.
Any advice would be appreciated.
Here is a JSFiddle that shows the basic problem. It simulates my chat interface, by loading 30 chat messages when the page loads, and then puts you at the bottom of the message list. As you scroll to the top, another 30 are loaded, but you'll see that this is jarring to the UX because you instantly lose your place.
UPDATE: Thanks to Richard's answer, with a technique based on requestAnimationFrame
to re-align the viewport. (For my use of it, I utilized the scrollTop
of my scrollable div, rather than window.scrollTo
as shown in his example.)
- Could you provide a minimal runnable snippet of your currently working code? – Richard Commented Mar 14, 2020 at 3:37
- I guess you just need to take note of scroll position, add the loaded messages and scroll to new loaded messages height + old scroll positon. about images loading: you could send the height/width before loading the image, that way you can size the box beforehand. – ariel Commented Mar 14, 2020 at 3:45
1 Answer
Reset to default 11Fortunately, there is. The CSS property is called overflow-anchor
. When you set the overflow-anchor
property of an element to auto
, it will turn on scroll anchoring; which is an attempt of the browser to minimize the problem of content jumping (such as a large image loaded above your scroll position causing your content to scroll further down); and set said element as the potential anchor when adjusting scroll position.
That being said, overflow-anchor
is automatically set to auto
and you needn't set it, so I'm not sure how setting it manually helped you. From MDN:
Scroll anchoring behavior is enabled by default in any browser that supports it. Therefore, changing the value of this property is typically only required if you are experiencing problems with scroll anchoring in a document or part of a document and need to turn the behavior off.
Here's a simple demonstration. At first, the window
is already scrolled by 200px. Every two seconds, a new div
is inserted on top of the container. Note that the visible area of the window
doesn't change even when a new div
is inserted on top the container.
const container = document.querySelector('#container')
window.scrollBy(0, 200)
let counter = 9
setInterval(() => {
let newDiv = document.createElement('div')
newDiv.classList.add('messages')
newDiv.innerText = counter++;
container.prepend(newDiv)
}, 2000)
* {
box-sizing: border-box;
font-family: Helvetica;
}
html,
body {
margin: 0;
}
#container {
width: 100%;
height: 100%;
padding: 0 20px;
scroll-behavior: smooth;
overflow-anchor: auto;
}
.messages {
width: 100%;
padding: 20px;
text-align: center;
background: #121212;
color: white;
border-radius: 5px;
margin: 20px 0px;
}
<div id="container">
<div class="messages">8</div>
<div class="messages">7</div>
<div class="messages">6</div>
<div class="messages">5</div>
<div class="messages">4</div>
<div class="messages">3</div>
<div class="messages">2</div>
<div class="messages">1</div>
</div>
Update
As setting overflow-anchor
manually did help you with adjusting scroll position upon height increase above the viewport, but Safari does not support overflow-anchor
, here's a simple alternative mechanism (using requestAnimationFrame
) to adjust the viewport so that it always stays in its current position. This code below also adjusts the scroll position when you are at the top of the container and older messages show up (an issue you mentioned).
Note that I've set the overflow-anchor
to none
, thus disabling the scroll-anchoring browsers usually enable automatically, to show that this code works (I'm running it in Firefox). Also, the code is adjusted such that it does not scroll down when new messages
are appended to the container
. Do consider the scenario when the user is reading the last message and a new message gets sent; you might want to scroll down in that case. Furthermore, I am calling getBoundingClientRect()
every frame. If not used properly, this can cause layout thrashing (also read here for the list of JS properties/methods that can cause this).
One last note: I actually wanted to use ResizeObserver
, but as Safari does not support that API, I used rAQ
instead to observe container's height changes. There are already ResizeObserver
polyfills, which I have not tried out yet, but may be more efficient than using rAQ
to observe height changes. Here's the page that contains a lot of links to ResizeObserver
polyfills.
const container = document.querySelector('#container')
let counter = 9
setInterval(() => {
let newDiv = document.createElement('div')
newDiv.classList.add('messages')
newDiv.innerText = counter++;
container.prepend(newDiv)
}, 2000)
window.scrollBy(0, 200)
// Mimicking scroll anchoring behaviour from browser
let previousLastChild = container.lastElementChild
let previousBoundingRect = container.getBoundingClientRect()
function scrollAdjustment() {
let boundingRect = container.getBoundingClientRect()
let isHeightIncreased = boundingRect.height !== previousBoundingRect.height
let isAppend = container.lastElementChild !== previousLastChild // Is height increase caused by new messages being appended?
if (isHeightIncreased && !isAppend) { // If new messages are appended, don't scroll down as people are reading the upper messages
let newScrollYPosition = window.scrollY + boundingRect.height - previousBoundingRect.height
previousBoundingRect = boundingRect
window.scrollTo(0, newScrollYPosition)
}
requestAnimationFrame(scrollAdjustment)
}
requestAnimationFrame(scrollAdjustment)
* {
box-sizing: border-box;
font-family: Helvetica;
}
html,
body {
margin: 0;
}
#container {
width: 100%;
height: 100%;
padding: 0 20px;
scroll-behavior: smooth;
overflow-anchor: none; /* Note I've set it to none */
}
.messages {
width: 100%;
padding: 20px;
text-align: center;
background: #121212;
color: white;
border-radius: 5px;
margin: 20px 0px;
}
<div id="container">
<div class="messages">8</div>
<div class="messages">7</div>
<div class="messages">6</div>
<div class="messages">5</div>
<div class="messages">4</div>
<div class="messages">3</div>
<div class="messages">2</div>
<div class="messages">1</div>
</div>