404

I have a div that is only 300 pixels big and I want it to when the page loads scroll to the bottom of the content. This div has content dynamically added to it and needs to stay scrolled all the way down. Now if the user decides to scroll up I don't want it to jump back to the bottom until the user scrolls all the way down again.

Is it possible to have a div that will stay scrolled to the bottom unless the user scrolls up and when the user scrolls back to the bottom it needs to keep itself at the bottom even when new dynamic content is added. How would I go about creating this?

TylerH
  • 20,799
  • 66
  • 75
  • 101
Robert E. McIntosh
  • 5,557
  • 4
  • 26
  • 35

20 Answers20

561

I was able to get this working with CSS only.

The trick is to use flex-direction: column-reverse

The browser treats the bottom like its the top. Assuming the browsers you're targeting support flex-box, the only caveat is that the markup has to be in reverse order.

Here is a working example:

.container {
  height: 100px;
  overflow: auto;
  display: flex;
  flex-direction: column-reverse;
}
<div class="container">
  <div class="inner">Bottom</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Hi</div>
  <div class="inner">Top</div>
</div>
miken32
  • 42,008
  • 16
  • 111
  • 154
Jim Hall
  • 6,079
  • 2
  • 16
  • 15
  • 141
    You can get around needing to reverse the content by adding an extra wrapper, and applying `overflow:auto; display:flex; flex-direction:column-reverse;` to the outer wrapper instead of the inner wrapper. – Nathan Arthur Sep 08 '17 at 15:10
  • 10
    This is great, however it doesn't help if you're adding content dynamically that needs to start at the top of the div. The content will always start adding to the bottom of the div. Example: https://codepen.io/anon/pen/GMwyej The case scenario here, I imagine is once the scrollbar is activated on the div, to always stay at the bottom. – Gorgon_Union Oct 17 '17 at 04:25
  • 36
    @NathanArthur In fact, if you just add a div under container to un-reverse everything: https://codepen.io/anon/pen/pdrLEZ – Coo Nov 15 '17 at 05:44
  • 1
    Did anyone notice that using column-reverse on Safari will inverse the scrollTop values? e.g. scrollTop is 0 when you scroll to the bottom instead of the top. – kevinl Dec 05 '17 at 16:21
  • 3
    using this, how to scroll to bottom when adding new content dynamically? when adding content dynamically, this works fine until you manually scroll using mouse – barley Nov 18 '19 at 07:42
  • 2
    Dang, I was excited about this solution but looks like a long-outstanding bug in FF/Edge/IE https://stackoverflow.com/q/34249501/5671022 will force a JS solution. – dras Nov 21 '19 at 18:21
  • @Jim-hall this seems to work great. is there a way to make the chat start at the bottom with this solution rather than have it scroll down? – geoboy Aug 06 '20 at 20:59
  • @Gorgon_Union You can use jquery `prepend` and `append` to control where things go. – Dennis Hackethal Dec 19 '20 at 22:17
  • What if my container is display: flex; flex-wrap: wrap; but direction: row? – YTG Jan 19 '21 at 11:32
  • 2
    Cool approach! Just be aware that this will break accessibility. To see this, take the code snippet and add tabIndex to each element and note how your now tabbing your way up the page. I imagine a screen reader would also have trouble with reading this in the correct order. – Michael Feb 04 '21 at 20:38
  • 2
    Doesn't this solution break text selection? It's selecting from bottom to top. – Artur Carvalho Oct 08 '21 at 09:26
  • 1
    text selection is indeed broken. Anyway, if you add to the top an element with `flex: 1 1 0%;` The text starts also at the top of the container since it will get pushed. Otherwise it starts at the bottom. – The Fool Jan 23 '22 at 22:10
  • I couldn't get @NathanArthur's solution to work, but changing a JS `.append()` for a `.prepend()` is no biggie. – étale-cohomology Jul 01 '22 at 23:02
  • 1
    This seems to unpin from the bottom if you scroll up even once. – mpen Mar 24 '23 at 20:47
  • From an **a11y** perspective, this won't work because the visual order doesn't follow the source order (markup order). – numonium Jul 07 '23 at 23:37
  • The outer wrapper solution by @NathanArthur also has a nice side-effect of working as expected with variable content such as lazy-loaded images in containers with variable heights. – OXiGEN Aug 08 '23 at 03:57
203

This might help you:

var element = document.getElementById("yourDivID");
element.scrollTop = element.scrollHeight;

[EDIT], to match the comment...

function updateScroll(){
    var element = document.getElementById("yourDivID");
    element.scrollTop = element.scrollHeight;
}

whenever content is added, call the function updateScroll(), or set a timer:

//once a second
setInterval(updateScroll,1000);

if you want to update ONLY if the user didn't move:

var scrolled = false;
function updateScroll(){
    if(!scrolled){
        var element = document.getElementById("yourDivID");
        element.scrollTop = element.scrollHeight;
    }
}

$("#yourDivID").on('scroll', function(){
    scrolled=true;
});
ctrl-alt-delor
  • 7,506
  • 5
  • 40
  • 52
Mr.Manhattan
  • 5,315
  • 3
  • 22
  • 34
  • 5
    This well scroll it to bottom on page load, but I need it to stay at the bottom when dynamic content is added unless the user has scrolled up. – Robert E. McIntosh Sep 04 '13 at 13:07
  • 14
    As far as I can tell this doesn't re-enable dynamic scrolling when the user scrolls back to the bottom... – TvE Nov 16 '15 at 04:05
  • If you are using angular and are depending on an API, did the following: `angular.element(document).ready(function () {`. Within this function, I set the scrolltop. – Nazariy Sep 25 '17 at 17:34
  • While applying angularJS two-way binding, `setTimeout()` worked better than `setInterval()` for me. – crownlessking Dec 17 '17 at 00:51
  • 14
    Bad solution. There is no reason to add a `setInterval` for this mild a problem. – ethancrist Jun 09 '18 at 07:29
  • where does one reset `scrolled` to false? maybe add `if (scrollTop == scrollHeight) { scrolled = false; }` somewhere? – Jarett Lloyd Oct 19 '18 at 14:32
  • Using `window.onload = function() {}` works nicely. You really don need a setTimeout or setInterval for such a small change. – TKTurbo Apr 23 '21 at 12:33
197

I just implemented this and perhaps you can use my approach.

Say we have the following HTML:

<div id="out" style="overflow:auto"></div>

Then we can check if it scrolled to the bottom with:

var out = document.getElementById("out");
// allow 1px inaccuracy by adding 1
var isScrolledToBottom = out.scrollHeight - out.clientHeight <= out.scrollTop + 1;

scrollHeight gives you the height of the element, including any non visible area due to overflow. clientHeight gives you the CSS height or said in another way, the actual height of the element. Both methods returns the height without margin, so you needn't worry about that. scrollTop gives you the position of the vertical scroll. 0 is top and max is the scrollHeight of the element minus the element height itself. When using the scrollbar it can be difficult (it was in Chrome for me) to get the scrollbar all the way down to the bottom. so I threw in a 1px inaccuracy. So isScrolledToBottom will be true even if the scrollbar is 1px from the bottom. You can set this to whatever feels right to you.

Then it's simply a matter of setting the scrollTop of the element to the bottom.

if(isScrolledToBottom)
    out.scrollTop = out.scrollHeight - out.clientHeight;

I have made a fiddle for you to show the concept: http://jsfiddle.net/dotnetCarpenter/KpM5j/

EDIT: Added code snippet to clarify when isScrolledToBottom is true.

Stick scrollbar to bottom

const out = document.getElementById("out")
let c = 0

setInterval(function() {
    // allow 1px inaccuracy by adding 1
    const isScrolledToBottom = out.scrollHeight - out.clientHeight <= out.scrollTop + 1

    const newElement = document.createElement("div")

    newElement.textContent = format(c++, 'Bottom position:', out.scrollHeight - out.clientHeight,  'Scroll position:', out.scrollTop)

    out.appendChild(newElement)

    // scroll to bottom if isScrolledToBottom is true
    if (isScrolledToBottom) {
      out.scrollTop = out.scrollHeight - out.clientHeight
    }
}, 500)

function format () {
  return Array.prototype.slice.call(arguments).join(' ')
}
#out {
    height: 100px;
}
<div id="out" style="overflow:auto"></div>
<p>To be clear: We want the scrollbar to stick to the bottom if we have scrolled all the way down. If we scroll up, then we don't want the content to move.
</p>
dotnetCarpenter
  • 10,019
  • 6
  • 32
  • 54
  • 27
    This is actually the only solution that accomplishes the stated question. And should have been marked as the correct answer. – preezzzy Sep 04 '15 at 00:02
  • Good... simple. And you would just add the new content between declaring isScrolledToBottom, and the final if statement. – Andrew Jun 22 '16 at 16:55
  • 3
    @dotnetCarpenter: It looks to me as if you need `if(!isScrolledToBottom)`: the test looks wrong to me (and didn't work in _my_ code until I fixed it). – luskwater Sep 06 '17 at 13:31
  • 1
    @luskwater can you provide a fsfiddle with your fix? I don't understand the issue with `out.scrollHeight - out.clientHeight <= out.scrollTop + 1`. Are you using padding in your CSS? – dotnetCarpenter Sep 19 '17 at 13:06
  • 2
    This is what I was looking for. And, this is the answer to the question OP asked. Thanks dotnetCarperter. – Luis Menjivar Oct 10 '17 at 22:27
  • 2
    Maybe I'm confused, but shouldn't it be "if (!isScrolledToBottom)," with a not? Adding the not seems to make it do what the OP asked for. –  Jun 23 '18 at 18:07
  • @BenCrowell sorry but you're confused ;) The OP is asking the overflow div to be scrolled to the bottom if `isScrolledToBottom` is true. That is, if the div is scrolled down, then **keep it scrolled down**. That is what the next line of code does. If `isScrolledToBottom` is false then we should do nothing, which of course is not necessary to spell out in code. – dotnetCarpenter Jun 25 '18 at 12:05
  • 1
    @BenCrowell Actually your code is not working for me, I needed to change <= to >= or adding ! to the if() condition, only then it works for me. Also, I think the logic behind my changed code is correct IMO. BTW, your answer is awesome! – Bharath Kumar R Jul 03 '18 at 20:13
  • 1
    If anyone is getting 0 when they call scrollTop, I suggest using `document.getScrollingElement` as suggested here: https://stackoverflow.com/questions/36227559/scrolltop-always-returns-0/36227646 – Dane Jordan May 05 '19 at 17:13
  • Hello my friends I am using this example on a chat using `jquery.load` to load messages but it's not working , can you guys help me on this ?https://stackoverflow.com/questions/57367962/chat-do-not-scroll-to-bottom-if-i-use-jquery-load-method –  Aug 06 '19 at 02:01
  • This works only with a fixed height. Does not works if the height is unset – Protagonist Apr 28 '21 at 15:46
64

You can use CSS Scroll Snap; check Can I Use for support.

This demo will snap the last element if visible, scroll to bottom to see the effect.

.container {
  overflow-y: scroll;
  overscroll-behavior-y: contain;
  scroll-snap-type: y proximity;
}

.container > div > div:last-child {
  scroll-snap-align: end;
}

.container > div > div {
  background: lightgray;
  height: 3rem;
  font-size: 1.5rem;
}
.container > div > div:nth-child(2n) {
  background: gray;
}
<div class="container" style="height:6rem">
  <div>
    <div>1</div>
    <div>2</div>
    <div>3</div>
    <div>4</div>
    <div>5</div>
  </div>
</div>
TylerH
  • 20,799
  • 66
  • 75
  • 101
wener
  • 7,191
  • 6
  • 54
  • 78
  • It works in Firefox 79 but only if you scroll with the scrollbar, if you mousewheel it doesn't trigger the snap back. – Bill Kervaski Jul 23 '20 at 18:04
  • 3
    Proximity range depends on scrollable height. If your chat view is taller than 2 or 3 chat bubbles, when you scroll up as much as 2 or 3 messages, it will again snap back to the end (unless you scroll hundreds of pixels) which makes this method unusable. The reason the shared snippet works is that the height of view is so small. – Mohebifar Aug 25 '20 at 23:45
  • 2
    @Mohebifar Agreed that this is hard to use with purely CSS. However, if you toggle the CSS snap on or off based on user scrolling in Javascript, this turns into a very pleasant solution. You don't get the jerkiness of managing the scroll with Javascript while you also keep control over how sensitive the snap is. – Etheryte Jun 30 '22 at 16:34
  • I liked this a lot but it breaks the wheel scrolling on Firefox. I ended up going for a full JS solution :( – simlmx Oct 29 '22 at 04:47
  • Can you edit your answer to include what the full solution looks like with your recent changes? I don't understand how your old demo solves the problem, and it sounds like it's out of date anyway. – mikemaccana Jan 19 '23 at 23:12
37
$('#yourDiv').scrollTop($('#yourDiv')[0].scrollHeight);

Live demo: http://jsfiddle.net/KGfG2/

Ankur Soni
  • 5,725
  • 5
  • 50
  • 81
Alvaro
  • 40,778
  • 30
  • 164
  • 336
17
$('#div1').scrollTop($('#div1')[0].scrollHeight);

Or animated:

$("#div1").animate({ scrollTop: $('#div1')[0].scrollHeight}, 1000);
Sasidharan
  • 3,676
  • 3
  • 19
  • 37
  • 1
    This well scroll it to bottom on page load, but I need it to stay at the bottom when dynamic content is added unless the user has scrolled up. – Robert E. McIntosh Sep 04 '13 at 13:06
  • This does not work when div grows a lot in first shot. for example in Angular-js, react js, Meteor Blaze template loading, this will not work. – Ankur Soni May 08 '18 at 14:02
  • This worked for me. I think it works fine if the div in question is going to stay a consistent height. Haven't tested it with dynamic heights. – doij790 Mar 26 '20 at 03:05
14

.cont {
  height: 100px;
  overflow-x: hidden;
  overflow-y: auto;
  transform: rotate(180deg);
  direction: rtl;
  text-align: left;
}
ul {
  overflow: hidden;
  transform: rotate(180deg);
}
<div class="cont"> 
 <ul>
   <li>0</li>
   <li>1</li>
   <li>2</li>
   <li>3</li>
   <li>4</li>
   <li>5</li>
   <li>6</li>
   <li>7</li>
   <li>8</li>
   <li>9</li>
   <li>10</li>  
 </ul>
</div>

https://jsfiddle.net/Yeshen/xm2yLksu/3/

How it works: By default overflow scrolling is from top to bottom. transform: rotate(180deg) reverses this, so that scrolling or loading dynamic blocks is from bottom to top.

Original idea: https://blog.csdn.net/yeshennet/article/details/88880252

TylerH
  • 20,799
  • 66
  • 75
  • 101
yisheng wu
  • 157
  • 1
  • 4
  • 9
    Very creative solution. However, it reverses the normal operation of my mouse's scroll wheel. To go up, I have to scroll "down," and vice versa. – mfluehr Jul 10 '19 at 13:10
  • @mfluehr I am on Firefox on Mac, it's not reversing my scroll wheel direction. What setup do you have? I'd like to test that setup to avoid issues. – IcyIcicle Jul 05 '23 at 19:20
  • @mfluehr Interesting... Just tried it on Chrome and it reversed it. Same with Safari. Looks like Firefox is the only one that doesn't reverse the direction. – IcyIcicle Jul 05 '23 at 19:22
13

Based on Jim Halls solution and comments. https://stackoverflow.com/a/44051405/9208887.

I added additionally an element with flex 1 1 0% to ensure the text starts at the top of the container when it's not full.

// just to add some numbers, so we can see the effect
// the actual solution requires no javascript
let num = 1001;
const container = document.getElementById("scroll-container");
document.getElementById("adder").onclick = () =>
  container.append(
    Object.assign(document.createElement("div"), {
      textContent: num++
    })
  );
.scroll-wrapper {
  height: 100px;
  overflow: auto;
  display: flex;
  flex-direction: column-reverse;
  border: 1px solid black;
}

.scroll-start-at-top {
  flex: 1 1 0%;
}
<div class="scroll-wrapper">
  <span class="scroll-start-at-top"></span>
  <div id="scroll-container">
    <div>1000</div>
  </div>
</div>

<button id="adder">Add Text</button>
The Fool
  • 16,715
  • 5
  • 52
  • 86
6

Here's a solution based on a blog post by Ryan Hunt. It depends on the overflow-anchor CSS property, which pins the scrolling position to an element at the bottom of the scrolled content.

function addMessage() {
  const $message = document.createElement('div');
  $message.className = 'message';
  $message.innerText = `Random number = ${Math.ceil(Math.random() * 1000)}`;
  $messages.insertBefore($message, $anchor);

  // Trigger the scroll pinning when the scroller overflows
  if (!overflowing) {
    overflowing = isOverflowing($scroller);
    $scroller.scrollTop = $scroller.scrollHeight;
  }
}

function isOverflowing($el) {
  return $el.scrollHeight > $el.clientHeight;
}

const $scroller = document.querySelector('.scroller');
const $messages = document.querySelector('.messages');
const $anchor = document.querySelector('.anchor');
let overflowing = false;

setInterval(addMessage, 1000);
.scroller {
  overflow: auto;
  height: 90vh;
  max-height: 11em;
  background: #555;
}

.messages > * {
  overflow-anchor: none;
}

.anchor {
  overflow-anchor: auto;
  height: 1px;
}

.message {
  margin: .3em;
  padding: .5em;
  background: #eee;
}
<section class="scroller">
  <div class="messages">
    <div class="anchor"></div>
  </div>
</section>

Note that overflow-anchor doesn't currently work in Safari.

mfluehr
  • 2,832
  • 2
  • 23
  • 31
4

I couldn't get the top two answers to work, and none of the other answers were helpful to me. So here are 3 solutions:

Solution 1:

$(function(){
  var scrolled = false;
  var lastScroll = 0;
  var count = 0;
  $("#chatscreen").on("scroll", function() {
    var nextScroll = $(this).scrollTop();

    if (nextScroll <= lastScroll) {
      scrolled = true;
    }
    lastScroll = nextScroll;

    console.log(nextScroll, $("#inner").height())
    if ((nextScroll + 100) == $("#inner").height()) {
      scrolled = false;
    }
  });

  function updateScroll(){
      if(!scrolled){
          var element = document.getElementById("chatscreen");
          var inner = document.getElementById("inner");
          element.scrollTop = inner.scrollHeight;
      }
  }

  // Now let's load our messages
  function load_messages(){
      $( "#inner" ).append( "Test" + count + "<br/>" );
      count = count + 1;
      updateScroll();
  }

  setInterval(load_messages,300); 
});
#chatscreen {
  width: 300px;
  overflow-y: scroll;
  max-height:100px;
}
<div id="chatscreen">
  <div id="inner">

  </div>
</div>

Solution 2:

$(function(){
    var isScrolledToBottom = false;
    // Now let's load our messages
    function load_messages(){
        $( "#chatscreen" ).append( "<br>Test" );
        updateScr();
    }
    
    var out = document.getElementById("chatscreen");
    var c = 0;
    
    $("#chatscreen").on('scroll', function(){
            console.log(out.scrollHeight);
        isScrolledToBottom = out.scrollHeight - out.clientHeight <= out.scrollTop + 10;
    });
    
    function updateScr() {
            // allow 1px inaccuracy by adding 1
        //console.log(out.scrollHeight - out.clientHeight,  out.scrollTop + 1);
        var newElement = document.createElement("div");
    
        newElement.innerHTML = c++;
        out.appendChild(newElement);
        
        console.log(isScrolledToBottom);
    
        // scroll to bottom if isScrolledToBotto
        if(isScrolledToBottom) {out.scrollTop = out.scrollHeight - out.clientHeight; }
    }
    
    var add = setInterval(updateScr, 1000);
    
    setInterval(load_messages,300); // change to 300 to show the latest message you sent after pressing enter // comment this line and it works, uncomment and it fails
                                    // leaving it on 1000 shows the second to last message
    setInterval(updateScroll,30);
});
#chatscreen {
  height: 300px;
  border: 1px solid purple;
  overflow: scroll;
}
<div id="chatscreen">

</div>

Solution 3:

$(function(){
    
    // Now let's load our messages
    function load_messages(){
        $( "#chatscreen" ).append( "<br>Test" );
    }
    
    var out = document.getElementById("chatscreen");
    var c = 0;
    var add = setInterval(function() {
        // allow 1px inaccuracy by adding 1
        var isScrolledToBottom = out.scrollHeight - out.clientHeight <= out.scrollTop + 1;
        load_messages();
    
        // scroll to bottom if isScrolledToBotto
        if(isScrolledToBottom) {out.scrollTop = out.scrollHeight - out.clientHeight; }
    }, 1000);
    setInterval(updateScroll,30);
});
#chatscreen {
  height: 100px;
  overflow: scroll;
  border: 1px solid #000;
}
<div id="chatscreen"></div>
miken32
  • 42,008
  • 16
  • 111
  • 154
desbest
  • 4,746
  • 11
  • 51
  • 84
  • @miken32 It is moderation policy on Stack Overflow to give credit within answers, for code that the user did not create, _including_ if it came from external websites from people who don't have a Stack Overfow account. Not giving credit, undermines the reputation system for upvotes, whilst also damaging Stack Overflow's website for being associated with low quality inaccurate, outdated, incomplete, shoddy and irrelevant answers like on _other_ websites. Also over 10 years ago, there's been issues with _"reputation junkies"_ who gained high reputation from 100% plagarism, causing lots of drama. – desbest Apr 07 '23 at 19:36
  • The jsfiddle links are unnecessary when we have snippets; the remaining links were promotional and not appropriate for Stack Overflow. As an active curator I'm well aware of the rules around plagiarism; but since the solutions were original work-for-hire creations there is no need – or ability – to cite a previous work. – miken32 Apr 07 '23 at 21:26
  • If you have a link to your original request with the responses on Reddit, that would be an appropriate citation source to link to. A portfolio of someone's work or a commercial freelancer site are absolutely not. – miken32 Apr 07 '23 at 21:29
  • The problem with what you're suggesting is that it doesn't explain _who exactly_ wrote the code, hence again, causing more moderation problems, following onward from your logic, if everyone else was to do the same thing as you was suggesting. https://imgur.com/a/m37vl4Q https://www.troddit.com/u/desbest/r/forhire/comments/6jj9qs/hiring_online_40_for_helping_me_with_javascript – desbest Apr 24 '23 at 21:28
3
$('#yourDivID').animate({ scrollTop: $(document).height() }, "slow");
return false;

This will calculate the ScrollTop Position from the height of #yourDivID using the $(document).height() property so that even if dynamic contents are added to the div the scroller will always be at the bottom position. Hope this helps. But it also has a small bug even if we scroll up and leaves the mouse pointer from the scroller it will automatically come to the bottom position. If somebody could correct that also it will be nice.

Ankur Soni
  • 5,725
  • 5
  • 50
  • 81
Krishnadas PC
  • 5,981
  • 2
  • 53
  • 54
3
//Make sure message list is scrolled to the bottom
var container = $('#MessageWindowContent')[0];
var containerHeight = container.clientHeight;
var contentHeight = container.scrollHeight;

container.scrollTop = contentHeight - containerHeight;

Here is my version based on dotnetCarpenter's answer. My approach is a pure jQuery and I named the variables to make things a bit clearer.. What is happening is if the content height is greater then the container we scroll the extra distance down to achieve the desired result.

Works in IE and chrome..

KingOfDaNorth
  • 91
  • 1
  • 3
3

Jim Hall's answer is preferrable because while it indeed does not scroll to the bottom when you're scrolled up, it is also pure CSS.

Very much unfortunately however, this is not a stable solution: In chrome (possibly due to the 1-px-issue described by dotnetCarpenter above), scrollTop behaves inaccurately by 1 pixel, even without user interaction (upon element add). You can set scrollTop = scrollHeight - clientHeight, but that will keep the div in position when another element is added, aka the "keep itself at bottom" feature is not working anymore.

So, in short, adding a small amount of Javascript (sigh) will fix this and fulfill all requirements:

Something like https://codepen.io/anon/pen/pdrLEZ this (example by Coo), and after adding an element to the list, also the following:

container = ...
if(container.scrollHeight - container.clientHeight - container.scrollTop <= 29) {
    container.scrollTop = container.scrollHeight - container.clientHeight;
}

where 29 is the height of one line.

So, when the user scrolls up half a line (if that is even possible?), the Javascript will ignore it and scroll to the bottom. But I guess this is neglectible. And, it fixes the Chrome 1 px thingy.

phil294
  • 10,038
  • 8
  • 65
  • 98
3

You can use something like this,

var element = document.getElementById("yourDivID");
window.scrollTo(0,element.offsetHeight);
  • 2
    Explain it please! – Szabolcs Páll Nov 27 '19 at 09:06
  • 1.scrollTo is a method that scrolls the whole window to particular coordinates.2.offsetHeight will give the height of your element so the second line of the above code keeps scrolling the window down while you are assigning something. – Yashesh Chauhan Nov 27 '19 at 09:37
2

The solution I've found most user-friendly is combining the scroll-snap-align approach with a little bit of Javascript. The problem with the former solution by itself is that the snap is too strong and you have to scroll far to get out of it.

Instead of that, we can use the snapping dynamic while the container is scrolled to the bottom and then disable it when the user scrolls up past a certain threshold.

This solution has the added benefit that it's a progressive enhancement: if the user has Javascript disabled, it will fall back to the CSS-only approach.

const container = document.getElementById("container");
const snap = document.getElementById("snap");

// Scroll the view to the bottom once initially
container.scrollTop = container.scrollHeight;

container.addEventListener("scroll", (event) => {
  const target = event.currentTarget;
  const scroll = target.scrollTop;
  const maxScroll = target.scrollHeight - target.clientHeight;
  const threshold = 50; // px
  isScrollBottomedOut = maxScroll - scroll < threshold;
  // If the user scrolls up more than the threshold, disable snapping
  // If the user scrolls down again, reenable snapping
  snap.style.display = isScrollBottomedOut ? "block" : "none";
});
#container {
  width: 200px;
  height: 500px;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  -ms-scroll-chaining: none;
  overscroll-behavior: contain;
  -ms-scroll-snap-type: y proximity;
  scroll-snap-type: y proximity;
  border: 2px solid black;
}

#snap {
  scroll-snap-align: end;
}
<div id="container">
  <ol>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
  </ol>
  <!-- This is the snapping target, if visible -->
  <div id="snap"></div>
</div>
Etheryte
  • 24,589
  • 11
  • 71
  • 116
1

The following does what you need (I did my best, with loads of google searches along the way):

    // no jquery, or other craziness. just
    // straight up vanilla javascript functions
    // to scroll a div's content to the bottom
    // if the user has not scrolled up.  Includes
    // a clickable "alert" for when "content" is
    // changed.

    // this should work for any kind of content
    // be it images, or links, or plain text
    // simply "append" the new element to the
    // div, and this will handle the rest as
    // proscribed.

    let scrolled = false; // at bottom?
    let scrolling = false; // scrolling in next msg?
    let listener = false; // does element have content changed listener?
    let contentChanged = false; // kind of obvious
    let alerted = false; // less obvious

    function innerHTMLChanged() {
      // this is here in case we want to
      // customize what goes on in here.
      // for now, just:
      contentChanged = true;
    }

    function scrollToBottom(id) {
      if (!id) { id = "scrollable_element"; }
      let DEBUG = 0; // change to 1 and open console
      let dstr = "";

      let e = document.getElementById(id);
      if (e) {
        if (!listener) {
          dstr += "content changed listener not active\n";
          e.addEventListener("DOMSubtreeModified", innerHTMLChanged);
          listener = true;
        } else {
          dstr += "content changed listener active\n";
        }
        let height = (e.scrollHeight - e.offsetHeight); // this isn't perfect
        let offset = (e.offsetHeight - e.clientHeight); // and does this fix it? seems to...
        let scrollMax = height + offset;

        dstr += "offsetHeight: " + e.offsetHeight + "\n";
        dstr += "clientHeight: " + e.clientHeight + "\n";
        dstr += "scrollHeight: " + e.scrollHeight + "\n";
        dstr += "scrollTop: " + e.scrollTop + "\n";
        dstr += "scrollMax: " + scrollMax + "\n";
        dstr += "offset: " + offset + "\n";
        dstr += "height: " + height + "\n";
        dstr += "contentChanged: " + contentChanged + "\n";

        if (!scrolled && !scrolling) {
          dstr += "user has not scrolled\n";
          if (e.scrollTop != scrollMax) {
            dstr += "scroll not at bottom\n";
            e.scroll({
              top: scrollMax,
              left: 0,
              behavior: "auto"
            })
            e.scrollTop = scrollMax;
            scrolling = true;
          } else {
            if (alerted) {
              dstr += "alert exists\n";
            } else {
              dstr += "alert does not exist\n";
            }
            if (contentChanged) { contentChanged = false; }
          }
        } else {
          dstr += "user scrolled away from bottom\n";
          if (!scrolling) {
            dstr += "not auto-scrolling\n";

            if (e.scrollTop >= scrollMax) {
              dstr += "scroll at bottom\n";
              scrolled = false;

              if (alerted) {
                dstr += "alert exists\n";
                let n = document.getElementById("alert");
                n.remove();
                alerted = false;
                contentChanged = false;
                scrolled = false;
              }
            } else {
              dstr += "scroll not at bottom\n";
              if (contentChanged) {
                dstr += "content changed\n";
                if (!alerted) {
                  dstr += "alert not displaying\n";
                  let n = document.createElement("div");
                  e.append(n);
                  n.id = "alert";
                  n.style.position = "absolute";
                  n.classList.add("normal-panel");
                  n.classList.add("clickable");
                  n.classList.add("blink");
                  n.innerHTML = "new content!";

                  let nposy = parseFloat(getComputedStyle(e).height) + 18;
                  let nposx = 18 + (parseFloat(getComputedStyle(e).width) / 2) - (parseFloat(getComputedStyle(n).width) / 2);
                  dstr += "nposx: " + nposx + "\n";
                  dstr += "nposy: " + nposy + "\n";
                  n.style.left = nposx;
                  n.style.top = nposy;

                  n.addEventListener("click", () => {
                    dstr += "clearing alert\n";
                    scrolled = false;
                    alerted = false;
                    contentChanged = false;
                    n.remove();
                  });

                  alerted = true;
                } else {
                  dstr += "alert already displayed\n";
                }
              } else {
                alerted = false;
              }
            }
          } else {
            dstr += "auto-scrolling\n";
            if (e.scrollTop >= scrollMax) {
              dstr += "done scrolling";
              scrolling = false;
              scrolled = false;
            } else {
              dstr += "still scrolling...\n";
            }
          }
        }
      }

      if (DEBUG && dstr) console.log("stb:\n" + dstr);

      setTimeout(() => { scrollToBottom(id); }, 50);
    }

    function scrollMessages(id) {
      if (!id) { id = "scrollable_element"; }
      let DEBUG = 1;
      let dstr = "";

      if (scrolled) {
        dstr += "already scrolled";
      } else {
        dstr += "got scrolled";
        scrolled = true;
      }
      dstr += "\n";

      if (contentChanged && alerted) {
        dstr += "content changed, and alerted\n";
        let n = document.getElementById("alert");
        if (n) {
          dstr += "alert div exists\n";
          let e = document.getElementById(id);
          let nposy = parseFloat(getComputedStyle(e).height) + 18;
          dstr += "nposy: " + nposy + "\n";
          n.style.top = nposy;
        } else {
          dstr += "alert div does not exist!\n";
        }
      } else {
        dstr += "content NOT changed, and not alerted";
      }

      if (DEBUG && dstr) console.log("sm: " + dstr);
    }

    setTimeout(() => { scrollToBottom("messages"); }, 1000);

    /////////////////////
    // HELPER FUNCTION
    //   simulates adding dynamic content to "chat" div
    let count = 0;
    function addContent() {
      let e = document.getElementById("messages");
      if (e) {
        let br = document.createElement("br");
        e.append("test " + count);
        e.append(br);
        count++;
      }
    }
button {
  border-radius: 5px;
}

#container {
  padding: 5px;
}

#messages {
  background-color: blue;
  border: 1px inset black;
  border-radius: 3px;
  color: white;
  padding: 5px;
  overflow-x: none;
  overflow-y: auto;
  max-height: 100px;
  width: 100px;
  margin-bottom: 5px;
  text-align: left;
}

.bordered {
  border: 1px solid black;
  border-radius: 5px;
}

.inline-block {
  display: inline-block;
}

.centered {
  text-align: center;
}

.normal-panel {
  background-color: #888888;
  border: 1px solid black;
  border-radius: 5px;
  padding: 2px;
}

.clickable {
  cursor: pointer;
}
<div id="container" class="bordered inline-block centered">
  <div id="messages" onscroll="scrollMessages('messages')">
    test<br>
    test<br>
    test<br>
    test<br>
    test<br>
    test<br>
    test<br>
    test<br>
    test<br>
    test<br>
  </div>
  <button onclick="addContent();">Add Content</button>
</div>

Note: You may have to adjust the alert position (nposx and nposy) in both scrollToBottom and scrollMessages to match your needs...

miken32
  • 42,008
  • 16
  • 111
  • 154
Jam Roll
  • 13
  • 3
1

There is native support for this problem.

There is a method called *.scrollIntoView. After running this method once, it keeps the container scroll at the bottom. Even after adding new content to the container, it scrolls to the bottom.

import {
  AfterViewInit,
  Directive,
  ElementRef,
} from '@angular/core';

@Directive({
  selector: '[aeScrollIntoView]',
})
export class ScrollIntoViewDirective implements AfterViewInit {
  constructor(private readonly el: ElementRef<HTMLDivElement>) {}
  ngAfterViewInit(): void {
    this.el.nativeElement.scrollIntoView({ behavior: 'smooth' });
  }
}

<div aeScrollIntoView>
 Your long and dynamic content. 
 Whenever new content is added to this container, it scrolls to the bottom.
<div>

Ahmet Emrebas
  • 566
  • 6
  • 10
0

Here is how I approached it. My div height is 650px. I decided that if the scroll height is within 150px of the bottom then auto scroll it. Else, leave it for the user.

if (container_block.scrollHeight - container_block.scrollTop < 800) {
                    container_block.scrollTo(0, container_block.scrollHeight);
}
Marco
  • 31
  • 3
0

I managed to get this working. The trick is to calculate: (a) current div user scroll position and (b) div scroll height, both BEFORE appending the new element.

If a === b, we know the user is at the bottom before appending the new element.

    let div = document.querySelector('div.scrollableBox');

    let span = document.createElement('span');
    span.textContent = 'Hello';

    let divCurrentUserScrollPosition = div.scrollTop + div.offsetHeight;
    let divScrollHeight = div.scrollHeight;

    // We have the current scroll positions saved in
    // variables, so now we can append the new element.
    div.append(span);

    
    if ((divScrollHeight === divCurrentUserScrollPosition)) {
        // Scroll to bottom of div
        div.scrollTo({ left: 0, top: div.scrollHeight });
    }
AndSmith
  • 741
  • 1
  • 6
  • 12
-1

I was trying to to the same with Bootstrap 5. The page I'm writing is a single-window html tool and I wanted two columns to have scrollable contents, and one needs to be in reverse as it's a log (the other is unlikely to scroll unless done on purpose). The lists and their headers are also bottom-anchored and I was having difficulty getting the header to remain right on top of a flex scrollable list.

Thanks to the examples above I could figure out what I was missing and get the right class types to make it work.

Here is my full example. In my actual app there is a 3rd column left of the other two with class mh-100 col overflow-auto and no need for an inner row/column as there is no title to stick on top (it will just scroll normally if viewport is too small). The lists have an ID I use to select and prepend to them or remove the top element (which is the bottom <li> item on the reversed list).

A smaller version is provided here:

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<div class="vh-100 w-75 container-fluid">
  <h1>2nd Level Scrolling Example</h1>
  <div class="h-75 row align-items-end">
    <div class="mh-100 col d-flex flex-column">
      <div class="row align-items-end">
        <div class="col"><h3>Normal scroll list, grow on top</h3></div>
      </div>
      <div class="row align-items-end overflow-auto">
        <div class="mh-100 col">
          <ul class="list-group">
            <li>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ut</li>
            <li>tortor eu ex tincidunt pretium non eu nisl. Ut eu libero ac velit</li>
            <li>ultricies dapibus. Donec id augue scelerisque, gravida est ut,</li>
            <li>commodo sapien. Interdum et malesuada fames ac ante ipsum primis</li>
            <li>in faucibus. Suspendisse volutpat fermentum finibus. Cras egestas</li>
            <li>tempor tempor. Suspendisse potenti. Mauris ac tellus ultrices lectus</li>
            <li>accumsan pellentesque. Nullam semper, nisi nec euismod ultrices, leo</li>
            <li>sem bibendum sapien, in rutrum sapien massa id mi.</li>
          </ul>
        </div>
      </div>
    </div>
    <div class="mh-100 col d-flex flex-column">
      <div class="row align-items-end">
        <div class="col"><h3>Reverse scroll list, grow on bottom</h3></div>
      </div>
      <div class="row align-items-end d-flex flex-column-reverse overflow-auto">
        <div class="mh-100 col">
          <ul class="list-group">
            <li>sem bibendum sapien, in rutrum sapien massa id mi.</li>
            <li>accumsan pellentesque. Nullam semper, nisi nec euismod ultrices, leo</li>
            <li>tempor tempor. Suspendisse potenti. Mauris ac tellus ultrices lectus</li>
            <li>in faucibus. Suspendisse volutpat fermentum finibus. Cras egestas</li>
            <li>commodo sapien. Interdum et malesuada fames ac ante ipsum primis</li>
            <li>ultricies dapibus. Donec id augue scelerisque, gravida est ut,</li>
            <li>tortor eu ex tincidunt pretium non eu nisl. Ut eu libero ac velit</li>
            <li>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ut</li>
          </ul>
        </div>
      </div>
    </div>
  </div>
</div>

If your viewport height is less than the overall content, the title should sit on top of the list, and everything on the bottom of the page (actually 75% of the viewport height, but in this example the title isn't taking the space it was designed for).

NB: I'm not really a web dev, just writing some handy html-based tools for day to day work, so comments are very welcome.

Thomas Guyot-Sionnest
  • 2,251
  • 22
  • 17