ChatGPT解决这个技术问题 Extra ChatGPT

Keep overflow div scrolled to bottom unless user scrolls up

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 bout creating this.

use css position top keep it at the bottom {position : relative; bottom:0;}. Remove the css property once the user has scroll.
since you didn't accept an answer, i want to ask: did it work for you?
Sounds like a chatbox you want to accomplish
For new to this question, should try css snap in 2020.
Here's a css-only method that works in many cases.

J
Jim Hall

I was able to get this working with CSS only.

The trick is to use display: flex; and 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. https://codepen.io/jimbol/pen/YVJzBg


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.
probably the best solution because it doesn't need javascript.
@NathanArthur In fact, if you just add a div under container to un-reverse everything: codepen.io/anon/pen/pdrLEZ
Unfortunately this solution doesn't seem to work in Firefox :(
It's October, 2020. And I've checked it's working on Firefox as well. Thanks for the quick solution.
c
ctrl-alt-delor

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;
});

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.
As far as I can tell this doesn't re-enable dynamic scrolling when the user scrolls back to the bottom...
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.
While applying angularJS two-way binding, setTimeout() worked better than setInterval() for me.
Bad solution. There is no reason to add a setInterval for this mild a problem.
d
dotnetCarpenter

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; }

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.


This is actually the only solution that accomplishes the stated question. And should have been marked as the correct answer.
@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 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?
This is what I was looking for. And, this is the answer to the question OP asked. Thanks dotnetCarperter.
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.
w
wener

In 2020, you can use css snap, but before Chrome 81 the layout change will not trigger re-snap, a pure css chat ui works on Chrome 81, also you can check Can I use CSS snap.

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; }

1
2
3
4
5

https://i.stack.imgur.com/Vrhja.gif

EDIT

use scroll-snap-type: y proximity;, scroll up easier.


this deserves a medal. exactly what i was looking for :)
It works in Firefox 79 but only if you scroll with the scrollbar, if you mousewheel it doesn't trigger the snap back.
@aatifshaikh I updated the answer, now you can scroll up easier.
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.
This works better than the highest upvoted answer for my use case as changing the display to flex tends to cause issues with my layouts.
A
Ankur Soni
$('#yourDiv').scrollTop($('#yourDiv')[0].scrollHeight);

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


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.
This works perfectly. I'm updating with dynamic content and the scroll is always at the bottom.
This works only for a div having a fixed height. Won't work for a variable div height
S
Sasidharan
$('#div1').scrollTop($('#div1')[0].scrollHeight);

Or animated:

$("#div1").animate({ scrollTop: $('#div1')[0].scrollHeight}, 1000);

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.
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.
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.
y
yisheng wu

.cont{ height: 100px; overflow-x: hidden; overflow-y: auto; transform: rotate(180deg); direction:rtl; text-align:left; } ul{ overflow: hidden; transform: rotate(180deg); }

  • 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Run code snippet to see the effect. (PS: If Run code snippet is not working, try this: https://jsfiddle.net/Yeshen/xm2yLksu/3/ ) How it work:

Default overflow is scroll from top to bottom.

transform: rotate(180deg) can make it scroll or load dynamic block from bottom to top.

Original idea:

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


Please add more explanation on how your code solves the OP's question
1, has added an additional introduction. 2, Please Run code snippet , directly see the effect.
The run code snippet button doesn't appear for me. Check your formatting
That's great. Please include the link in your answer
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.
T
The Fool

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%; }

1000


love it man! thanks a bunch :)
This is exactly what I was looking for. Content pushed to top, but scrolling is reversed (just like a terminal!)
m
mfluehr

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; }

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


d
desbest

I couldn't get the top two answers to work, and none of the other answers were helpful to me. So I paid three people $30 from Reddit r/forhire and Upwork and got some really good answers. This answer should save you $90.

Justin Hundley / The Site Bros' solution

HTML

<div id="chatscreen">
  <div id="inner">
  
  </div>
</div>

CSS

#chatscreen {
  width: 300px;
  overflow-y: scroll;
  max-height:100px;
}

Javascript

$(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); 
});

Preview the site bros' solution

portfolio

Lermex / Sviatoslav Chumakov's solution

HTML

<div id="chatscreen">

</div>

CSS

#chatscreen {
  height: 300px;
  border: 1px solid purple;
  overflow: scroll;
}

Javascript

$(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);
});

Preview Sviatoslav's solution

portfolio

Igor Rusinov's Solution

HTML

<div id="chatscreen"></div>

CSS

#chatscreen {
  height: 100px;
  overflow: scroll;
  border: 1px solid #000;
}

Javascript

$(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);
});

Preview Igor's solution

portfolio


A
Ankur Soni
$('#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.


K
KingOfDaNorth
//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..


p
phil294

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.


Y
Yashesh Chauhan

You can use something like this,

var element = document.getElementById("yourDivID");
window.scrollTo(0,element.offsetHeight);

Explain it please!
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.
J
Jam Roll

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

<html>
<head>
  <script>
    // 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++;
      }
    }
  </script>

  <style>
    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;
    }
  </style>
</head>
<body>
<div id="container" class="bordered inline-block centered">
  <div class="inline-block">My Chat</div>

  <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>
</body>
</html>

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

And a link to my own working example, hosted on my server: https://night-stand.ca/jaretts_tests/chat_scroll.html


M
Marco

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);
}

A
AndSmith

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 });
    }

T
Thomas Guyot-Sionnest

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:

2nd Level Scrolling Example

Normal scroll list, grow on top

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ut
  • tortor eu ex tincidunt pretium non eu nisl. Ut eu libero ac velit
  • ultricies dapibus. Donec id augue scelerisque, gravida est ut,
  • commodo sapien. Interdum et malesuada fames ac ante ipsum primis
  • in faucibus. Suspendisse volutpat fermentum finibus. Cras egestas
  • tempor tempor. Suspendisse potenti. Mauris ac tellus ultrices lectus
  • accumsan pellentesque. Nullam semper, nisi nec euismod ultrices, leo
  • sem bibendum sapien, in rutrum sapien massa id mi.

Reverse scroll list, grow on bottom

  • sem bibendum sapien, in rutrum sapien massa id mi.
  • accumsan pellentesque. Nullam semper, nisi nec euismod ultrices, leo
  • tempor tempor. Suspendisse potenti. Mauris ac tellus ultrices lectus
  • in faucibus. Suspendisse volutpat fermentum finibus. Cras egestas
  • commodo sapien. Interdum et malesuada fames ac ante ipsum primis
  • ultricies dapibus. Donec id augue scelerisque, gravida est ut,
  • tortor eu ex tincidunt pretium non eu nisl. Ut eu libero ac velit
  • Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ut

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.


E
Etheryte

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; }

  1. item
  2. item
  3. item
  4. item
  5. item
  6. item
  7. item
  8. item
  9. item
  10. item
  11. item
  12. item
  13. item
  14. item
  15. item
  16. item
  17. item
  18. item
  19. item
  20. item
  21. item
  22. item
  23. item
  24. item
  25. item
  26. item
  27. item
  28. item
  29. item
  30. item
  31. item
  32. item
  33. item
  34. item
  35. item
  36. item
  37. item
  38. item
  39. item
  40. item