react-stay-scrolled

React component to keep content always scrolled down

Usage no npm install needed!

<script type="module">
  import reactStayScrolled from 'https://cdn.skypack.dev/react-stay-scrolled';
</script>

README

react-stay-scrolled

Build Status npm package Coverage Status Dependency Status devDependency Status

Keep your component, such as message boxes, scrolled down

Live demo

You can see the simplest demo here: Live demo

Install

$ npm install --save react-stay-scrolled

Examples

Run examples:

$ npm ci
$ cd examples
$ npm ci
$ npm start

Usage

import { useRef, useLayoutEffect } from 'react';
import PropTypes from 'prop-types';
import useStayScrolled from 'react-stay-scrolled';

const Messages = ({ messages }) => {
  const listRef = useRef();
  const { stayScrolled/*, scrollBottom*/ } = useStayScrolled(listRef);

  // Typically you will want to use stayScrolled or scrollBottom inside
  // useLayoutEffect, because it measures and changes DOM attributes (scrollTop) directly
  useLayoutEffect(() => {
    stayScrolled();
  }, [messages.length])

  return (
    <ul ref={listRef}>
      {messages.map((message, i) => <Message key={i} text={message} />)}
    </ul>
  );
};

Messages.propTypes = {
  messages = PropTypes.array,
}

Another use case is notifying users when there is a new message down the window that they haven't read:

// messages.jsx
import { useState, useRef, useCallback, useLayoutEffect } from 'react';
import PropTypes from 'prop-types';
import useStayScrolled from 'react-stay-scrolled';
import Message from './message.jsx';

const Messages = ({ messages }) => {
  const [notifyNewMessage, setNotifyNewMessage] = useState(false);
  const ref = useRef();

  const { stayScrolled, isScrolled } = useStayScrolled(ref);

    // The element just scrolled down - remove new messages notification, if any
  const onScroll = useCallback(() => {
    if (isScrolled()) setNotifyNewMessage(false);
  }, []);

  useLayoutEffect(() => {
    // Tell the user to scroll down to see the newest messages if the element wasn't scrolled down
    setNotifyNewMessage(!stayScrolled());
  }, [messages.length])

  return (
    <div ref={ref} onScroll={onScroll}>
      {messages.map((message, i) => <Message key={i} text={message} />)}
      {notifyNewMessage && <div>Scroll down to new message</div>}
    </div>
  );
};

You can use react-spring to animate the scroll:

import { useRef, useCallback, useLayoutEffect } from 'react';
import useStayScrolled from 'react-stay-scrolled';
import { useSpring, animated } from '@react-spring/web';

const SpringStayScrolled = ({
  provideControllers,
  onScroll,
  getRunScroll,
}) => {
  const ref = useRef(null);
  const [{ scrollTop }, animateScroll] = useSpring(() => ({ scrollTop: 0 }), []);
  const runScroll = useCallback(offset => animateScroll.start({
    scrollTop: offset,
    from: { scrollTop: ref.current ? ref.current.scrollTop : 0 },
  }), [animateScroll])

  const { scrollBottom } = useStayScrolled(ref, { runScroll });

  useLayoutEffect(() => { scrollBottom(); }, []);

  return (
    <animated.div ref={ref} scrollTop={scrollTop}>
      {/* ... */}
    </animated.div>
  );
};

Arguments

ref

Type: a React ref, required

A ref to the DOM element whose scroll position you want to control

options

Type: object, default:

{
  initialScroll: null,
  inaccuracy: 0,
  runScroll: defaultRunScroll,
}

options.initialScroll

Type: number, default: null

If provided, the scrolling element will mount scrolled with the provided value. If Infinity is provided, it will mount scrolled to the bottom.

options.inaccuracy

Type: number, default: 0

Defines an error margin, in pixels, under which stayScrolled will still scroll to the bottom

options.runScroll

Type: function: (offset) => undefined, default: (offset) => { ref.current.scrollTop = offset; } where ref is the first value

Used for animating dom scrolling. You can use dynamic.js, Velocity, jQuery, or your favorite animation library. Here are examples of possible, tested runScroll values:

const easing = 'linear';
const duration = 100;

const dynamicsRunScroll = (domRef) => (offset) => {
  dynamics.animate(domRef.current, {
    scrollTop: offset,
  }, {
    type: dynamics[easing],
    duration,
  });
};

const jqueryRunScroll = (domRef) => (offset) => {
  jQuery(domRef.current).animate({ scrollTop: offset }, duration, easing);
};

const velocityRunScroll = (domRef) => (offset) => {
  Velocity(
    domRef.current.firstChild,
    'scroll',
    {
      container: domRef.current,
      easing,
      duration,
      offset,
    }
  );
};

Return value

Type: object, shape: { stayScrolled, scrollBottom, scroll, isScrolled }

Four functions used for controlling scroll behavior.

stayScrolled

Type: function: () => bool

Scrolls down the element if it was already scrolled down - useful for when a user is reading previous messages, and you don't want to interrupt. Returns true if it performed a scrolled down, false otherwise.

scroll

Type: function: (position: Integer) => void

Scrolls down to the desired position. If given Infinity, it scrolls to the bottom

scrollDown

Type: function: () => void

Scrolls down the wrapper element, regardless of current position. Equivalent to () => scroll(Infinity).

isScrolled

Type: function: () => bool

Returns true if the dom element is scrolled all the way down (within the inaccuracy provided).

License

See the LICENSE file for license rights and limitations (MIT).