100

I have a website with different sections. I am using segment.io to track different actions on the page. How can I detect if a user has scrolled to the bottom of a div? I have tried the following but it seems to be triggered as soon as I scroll on the page and not when I reached the bottom of the div.

componentDidMount() {
  document.addEventListener('scroll', this.trackScrolling);
}

trackScrolling = () => {
  const wrappedElement = document.getElementById('header');
  if (wrappedElement.scrollHeight - wrappedElement.scrollTop === wrappedElement.clientHeight) {
    console.log('header bottom reached');
    document.removeEventListener('scroll', this.trackScrolling);
  }
};
Pardeep Dhingra
  • 3,916
  • 7
  • 30
  • 56

16 Answers16

173

An even simpler way to do it is with scrollHeight, scrollTop, and clientHeight.

Subtract the scrolled height from the total scrollable height. If this is equal to the visible area, you've reached the bottom!

element.scrollHeight - element.scrollTop === element.clientHeight

In react, just add an onScroll listener to the scrollable element, and use event.target in the callback.

class Scrollable extends Component {

  handleScroll = (e) => {
    const bottom = e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
    if (bottom) { ... }
  }

  render() {
    return (
      <ScrollableElement onScroll={this.handleScroll}>
        <OverflowingContent />
      </ScrollableElement>
    );
  }
}

I found this to be more intuitive because it deals with the scrollable element itself, not the window, and it follows the normal React way of doing things (not using ids, ignoring DOM nodes).

You can also manipulate the equation to trigger higher up the page (lazy loading content/infinite scroll, for example).

Brendan McGill
  • 6,130
  • 4
  • 21
  • 31
  • 16
    From the `scrollTop` web doc: `On systems using display scaling, scrollTop may give you a decimal value.` If you want to account for varying levels of zoom in a browser window you might want to get the ceiling of scrollTop to compare these values. `node.scrollHeight - Math.ceil(node.scrollTop) === node.clientHeight` – janDro May 03 '19 at 15:16
  • For some reason my `scrollTop` comes out in "low" decimal values. Like 34.22233 for example. For this reason I use `Math.floor(scrollTop)`. This seems a bit "hacky". Will I find cases where i should implement ceil instead of floor and the other way around? Is there a better solution? Maybe `Math.Round` ? – Kulio Sep 29 '20 at 06:28
  • 5
    What about `Math.abs(scrollHeight - (scrollTop + clientHeight) <= 1`. Works fine for me so far. – Yuki Jul 14 '21 at 15:52
  • Thank you! You have helped me a lot. Better solution than event listener! – Sebastian Korotkiewicz Aug 04 '21 at 23:42
  • This is actually not a good solution if you want to implement something like infinite scrolling. If you load more elements e.g. below, your scroll event will trigger again causing the `bottom` value to be `true` again. If you have something like an API call here: `if (bottom) { ... }`, it will be called twice. – Branislav Lazic Mar 21 '23 at 13:32
79

you can use el.getBoundingClientRect().bottom to check if the bottom has been viewed

isBottom(el) {
  return el.getBoundingClientRect().bottom <= window.innerHeight;
}

componentDidMount() {
  document.addEventListener('scroll', this.trackScrolling);
}

componentWillUnmount() {
  document.removeEventListener('scroll', this.trackScrolling);
}

trackScrolling = () => {
  const wrappedElement = document.getElementById('header');
  if (this.isBottom(wrappedElement)) {
    console.log('header bottom reached');
    document.removeEventListener('scroll', this.trackScrolling);
  }
};
Twitter khuong291
  • 11,328
  • 15
  • 80
  • 116
ewwink
  • 18,382
  • 2
  • 44
  • 54
44

Here's a solution using React Hooks and ES6:

import React, { useRef, useEffect } from "react";

const List = () => {
  const listInnerRef = useRef();

  const onScroll = () => {
    if (listInnerRef.current) {
      const { scrollTop, scrollHeight, clientHeight } = listInnerRef.current;
      const isNearBottom = scrollTop + clientHeight >= scrollHeight;

      if (isNearBottom) {
        console.log("Reached bottom");
        // DO SOMETHING HERE
      }
    }
  };

  useEffect(() => {
    const listInnerElement = listInnerRef.current;

    if (listInnerElement) {
      listInnerElement.addEventListener("scroll", onScroll);

      // Clean-up
      return () => {
        listInnerElement.removeEventListener("scroll", onScroll);
      };
    }
  }, []);

  return (
    <div className="list">
      <div className="list-inner" ref={listInnerRef}>
        {/* List items */}
      </div>
    </div>
  );
};

export default List;
Allan of Sydney
  • 1,410
  • 14
  • 23
  • And to do it without hooks you just have to call onScroll and use the event param like this: `if ((event.target.clientHeight + event.target.scrollTop) >= event.target.scrollHeight) {...` – Chema Sep 04 '22 at 18:22
19

This answer belongs to Brendan, let's make it functional

export default () => {
   const handleScroll = (e) => {
       const bottom = e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
       if (bottom) { 
           console.log("bottom")
       }
    }

  return (
     <div onScroll={handleScroll}  style={{overflowY: 'scroll', maxHeight: '400px'}}  >
        //overflowing elements here
   </div>
  )
}

If the first div is not scrollable it won't work and onScroll didn't work for me in a child element like div after the first div so onScroll should be at the first HTML tag that has an overflow

Chukwuemeka Maduekwe
  • 6,687
  • 5
  • 44
  • 67
10

We can also detect div's scroll end by using ref.

import React, { Component } from 'react';
import {withRouter} from 'react-router-dom';
import styles from 'style.scss';

class Gallery extends Component{ 

  paneDidMount = (node) => {    
    if(node) {      
      node.addEventListener("scroll", this.handleScroll.bind(this));      
    }
  }

  handleScroll = (event) => {    
    var node = event.target;
    const bottom = node.scrollHeight - node.scrollTop === node.clientHeight;
    if (bottom) {      
      console.log("BOTTOM REACHED:",bottom); 
    }    
  }

  render() {
    var that = this;        
    return(<div className={styles.gallery}>
      <div ref={that.paneDidMount} className={styles.galleryContainer}>
        ...
      </div>

    </div>);   
  }
}

export default withRouter(Gallery);
Chandresh
  • 277
  • 2
  • 14
7

Extending chandresh's answer to use react hooks and ref I would do it like this;

import React, {useState, useEffect} from 'react';

export default function Scrollable() {
    const [referenceNode, setReferenceNode] = useState();
    const [listItems] = useState(Array.from(Array(30).keys(), (n) => n + 1));

    useEffect(() => {
        return () => referenceNode.removeEventListener('scroll', handleScroll);
    }, []);

    function handleScroll(event) {
        var node = event.target;
        const bottom = node.scrollHeight - node.scrollTop === node.clientHeight;
        if (bottom) {
            console.log('BOTTOM REACHED:', bottom);
        }
    }

    const paneDidMount = (node) => {
        if (node) {
            node.addEventListener('scroll', handleScroll);
            setReferenceNode(node);
        }
    };

    return (
        <div
            ref={paneDidMount}
            style={{overflowY: 'scroll', maxHeight: '400px'}}
        >
            <ul>
                {listItems.map((listItem) => <li>List Item {listItem}</li>)}
            </ul>
        </div>
    );
}
Allan Guwatudde
  • 533
  • 4
  • 8
5

Add following functions in your React.Component and you're done :]

  componentDidMount() {
    window.addEventListener("scroll", this.onScroll, false);
  }

  componentWillUnmount() {
    window.removeEventListener("scroll", this.onScroll, false);
  }

  onScroll = () => {
    if (this.hasReachedBottom()) {
      this.props.onScrollToBottom();
    }
  };

  hasReachedBottom() {
    return (
      document.body.offsetHeight + document.body.scrollTop ===
      document.body.scrollHeight
    );
  }
4

I know this has already been answered but, I think another good solution is to use what's already available out in the open source community instead of DIY. React Waypoints is a library that exists to solve this very problem. (Though don't ask me why the this problem space of determining if a person scrolls past an HTML element is called "waypoints," haha)

I think it's very well designed with its props contract and definitely encourage you to check it out.

Danchez
  • 1,266
  • 2
  • 13
  • 28
1

I used follow in my code

.modify-table-wrap {
    padding-top: 50px;
    height: 100%;
    overflow-y: scroll;
}

And add code in target js

    handleScroll = (event) => {
        const { limit, offset } = this.state
        const target = event.target
        if (target.scrollHeight - target.scrollTop === target.clientHeight) {
            this.setState({ offset: offset + limit }, this.fetchAPI)
        }
    }
    return (
            <div className="modify-table-wrap" onScroll={this.handleScroll}>
               ...
            <div>
            )

許雅婷
  • 154
  • 1
  • 6
1

To evaluate whether my browser has scrolled to the bottom of a div, I settled with this solution:

const el = document.querySelector('.your-element');
const atBottom = Math.ceil(el.scrollTop + el.offsetHeight) === el.scrollHeight;
hngr18
  • 817
  • 10
  • 13
1

Put a div with 0 height after your scrolling div. then use this custom hooks to detect if this div is visible.

  const bottomRef = useRef();
  const reachedBottom = useCustomHooks(bottomRef);

  return(
  <div>
   {search resault}
  </div>
  <div ref={bottomRef}/> )

reachedBottom will toggle to true if you reach bottom

Dabir Rahamni
  • 89
  • 1
  • 5
1

The solution below works fine on most of browsers but has problem with some of them.

element.scrollHeight - element.scrollTop === element.clientHeight

The better and most accurate is to use the code below which works on all browsers.

Math.abs(e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop) < 1

So the final code should be something like this

const App = () => {
   const handleScroll = (e) => {
     const bottom = Math.abs(e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop) < 1;
     if (bottom) { ... }
   }
   return(
     <div onScroll={handleScroll}>
       ...
     </div>
   )
}
0

This answer belongs to Brendan, but I am able to use that code in this way.

window.addEventListener("scroll", (e) => {
        const bottom =
            e.target.scrollingElement.scrollHeight -
                e.target.scrollingElement.scrollTop ===
            e.target.scrollingElement.clientHeight;
        console.log(e);
        console.log(bottom);
        if (bottom) {
            console.log("Reached bottom");
        }
    });

While others are able to access directly inside target by e.target.scrollHeight,
I am able to achieve same by e.target.scrollingElement.scrollHeight

Tirath Sharma
  • 93
  • 1
  • 8
0

I made this example for chat in my project it works

This function uses the scrollHeight and clientHeight properties of the container element to calculate the difference and set the scrollTop property accordingly.

  import  { useEffect } from "react";
  const chatContainerRef = useRef(null);

  function scrollToBottom() {
      if (chatContainerRef.current) {
       const { scrollHeight, clientHeight } = chatContainerRef.current;
       chatContainerRef.current.scrollTop = scrollHeight - clientHeight;
      }
   }

  function Chat(props) {
      const { messages } = props;

      useEffect(() => {
       scrollToBottom();
      }, [messages]);

    return (
      <div className="chat-container" ref={chatContainerRef}>
      {/* render chat messages here */}
      </div>
    );
   }
0

Custom hook to know if the scroll is complete

Usecase: useIsScrollComplete hook returns a variable isScrollComplete which is a boolean with an initial value of false and set to true if the scroll is complete.

Note: The below snippet is not runnable. Check this Stackblitz for runnable code.

function useIsScrollComplete<TElement extends HTMLElement | null>({
  ref,
  querySelector,
  markAsComplete = true,
}: IUseIsScrollComplete<TElement>) {
  const [isScrollComplete, setIsScrollComplete] = useState(false);

  const onScroll: EventListener = useCallback(({ target }) => {
    const { scrollHeight, clientHeight, scrollTop } = target as Element;

    if (Math.abs(scrollHeight - clientHeight - scrollTop) < THRESHOLD) {
      setIsScrollComplete(true);
    } else {
      setIsScrollComplete(false);
    }
  }, []);

  useEffect(() => {
    const element = ref.current;
    const targetElement = querySelector
      ? element?.querySelector(querySelector)
      : element;

    if (targetElement) {
      const { scrollHeight, clientHeight } = targetElement;

      if (scrollHeight === clientHeight) {
        // set scroll is complete if there is no scroll
        setIsScrollComplete(true);
      }

      targetElement.addEventListener("scroll", onScroll);

      if (isScrollComplete && markAsComplete) {
        targetElement.removeEventListener("scroll", onScroll);
      }

      return () => {
        targetElement.removeEventListener("scroll", onScroll);
      };
    }
  }, [isScrollComplete, markAsComplete, onScroll, querySelector, ref]);

  return { isScrollComplete };
}

Usage:

  const divRef = useRef<HTMLDivElement | null>(null);
  const { isScrollComplete } = useIsScrollComplete({ ref: divRef });

  return (
    <div>
      <div ref={divRef}>
           <p>Scrollable Content</p>
      </div>

      {isScrollComplete && (
        <p>Scroll is Complete ✅</p>
      )}
    </div>
  );

Other use-cases:

  1. You can use querySelector to target a child of an element that you don't have direct access to.
  2. markAsComplete prop -> specifies whether to mark the scroll as complete. Defaults to true. If set to false, the scroll is observed even after the scroll is complete. i.e if you move back from the bottom to the top, isScrollComplete will be false. ( Ex: When you want to show pagination of the table only when the scroll is at the bottom of the table and should hide when the scroll is anywhere else )
  3. If the container does not have scroll, the value is set to true by default.

Open Code in Stackblitz

PS: The custom hook is maintained and updated here for more use cases.

Anjan Talatam
  • 2,212
  • 1
  • 12
  • 26
0

Without js, I achieved the enforcement to scroll to bottom by including the event element (e.g button) inside the ScrollView, see example below.

<View style={styles.rootContainer}>
    <ScrollView 
        style={{paddingTop: 16, paddingBottom: 16}} 
        adding persistentScrollbar={true} 
    >
        <Text>
            ..... multipage paragraphs....
        </Text>        
        <View style={styles.buttonContainer}>
            <PrimaryButton onPress={props.Submit}>Submit</PrimaryButton>
        </View>
    </ScrollView>
</View>
Olu
  • 63
  • 1
  • 9