Scroll to bottom of an overflowing div in React

I have a ul with a max-height and overflow-y: auto.

When the user enters enough li elements, the ul starts to scroll, but I want the last li with the form in it to always be present and viewable to the user.

I've tried implementing a scrollToBottom function that looks like this:

scrollToBottom() {
    this.formLi.scrollIntoView({ behavior: 'smooth' });
}

But that just makes the ul jump to the top of the screen and show the li with the form in it as the only visible thing.

Is there a better way to accomplish this? Answers I find tend to be a bit older and use ReactDOM. Thanks!

CSS:

.prompt-box .options-holder {
    list-style: none;
    padding: 0;
    width: 95%;
    margin: 12px auto;
    max-height: 600px;
    overflow-y: auto;
}

HTML:

<ul className='options-holder'>
    {
        this.state.items.map((item, index) => (
            <li key={item.id} className={`option ${index === 0 ? 'first' : ''}`}>
                <div className='circle' onClick={this.removeItem} />

                <p className='item-text'>{ item.text }</p>
            </li>
        ))
    }

    <li key={0} className={`option form ${this.state.items.length === 0 ? 'only' : ''}`} ref={el => (this.formLi = el)}>
        <div className='circle form' />

        <form className='new-item-form' onSubmit={this.handleSubmit}>
            <input 
                autoFocus 
                className='new-item-input' 
                placeholder='Type something and press return...' 
                onChange={this.handleChange} 
                value={this.state.text}
                ref={(input) => (this.formInput = input)} />
        </form>
    </li> 
</ul>

Solution 1:

I have a container that fills the height of the page. This container has display flex and flex direction column. Then I have a Messages component which is a ul with one li per message plus a special AlwaysScrollToBottom component as the last child that, with the help of useEffect, scrolls to the bottom of the list.

const AlwaysScrollToBottom = () => {
  const elementRef = useRef();
  useEffect(() => elementRef.current.scrollIntoView());
  return <div ref={elementRef} />;
};

const Messages = ({ items }) => (
  <ul>
    {items.map(({id, text}) => <li key={id}>{text}</li>)}
    <AlwaysScrollToBottom />
  </ul>
)

const App = () => (
  <div className="container">
    <Messages items={[...]}>
    <MessageComposer />
  </div>
)

The CSS is

.container {
  display: flex;
  height: 100vh;
  flex-direction: column;
}

ul {
  flex-grow: 1;
  overflow-y: auto;
}

MessageComposer has been omitted for brevity, it contains the form, the input field, etc. to send messages.

Solution 2:

Use react-scroll library. However you will have to explicitly set ID of your ul.

import { animateScroll } from "react-scroll";

scrollToBottom() {
    animateScroll.scrollToBottom({
      containerId: "options-holder"
    });
}

You can call scrollToBottom on setState callback.

For example

this.setState({ items: newItems}, this.scrollToBottom);

Or by using setTimeout.

Or you can even set Element from react-scroll and scroll to a certain li.

Solution 3:

I had a similar issue when it came to rendering an array of components that came from a user import. I ended up using the componentDidUpdate() function to get mine to work.

componentDidUpdate() {
        // I was not using an li but may work to keep your div scrolled to the bottom as li's are getting pushed to the div
        const objDiv = document.getElementById('div');
        objDiv.scrollTop = objDiv.scrollHeight;
      }

Solution 4:

Seems like you're building a chat-like interface. You can try wrapping the <ul> and the div.circle-form in a separate div like below:

ul{
  border:1px solid red;
  height:13em;
  overflow-y:auto;
  position:relative;
}
ul li{
  border-bottom:1px solid #ddd;
  list-style:none;
  margin-left:0;
  padding:6px;
}

.wrapper{
  background:#eee;
  height:15em;
  position:relative;
}
.circle-form{
  background:#ddd;
  height:2em;
  padding:3px;
  position:absolute;
  bottom:0;
  left:0;
  right:0;
  z-index:2;
}
.circle-form input[type=text]{
  padding:8px;
  width:50%;
}
<div class="wrapper">
   <ul id="list">
       <li>item</li>
       <li>item</li>
       <li>item</li>
       <li>item</li>
       <li>item</li>
       <li>item</li>
       <li>item</li>
       <li>item</li>
    </ul>
    <div class="circle-form">
        <input type="text" name="type_here" placeholder="Enter something..." autofocus/>
    </div>
</div>

EDIT

And to scroll to the bottom of the list with javascript

var list = document.getElementById("list");
list.scrollTop = list.offsetHeight;