All hooks / Elements

useStickToBottom

589 B minified and brotliedSourceMarkdown✓ Works in all modern browsers

Keeps a scrollable element scrolled to the bottom as content is added or grows, like a chat or a log, until the user scrolls up to read. Scrolling back to the bottom, or calling scrollToBottom, follows again.

Follows content that grows with no scroll event, such as an AI reply streamed into the last message. Starts at the bottom. Only scrolling up stops following (wheel, touch, keys, or scrollbar), and it stops as soon as the user starts, so it never cancels their scroll. Its own scrolling never counts as the user's.

Import

import { useStickToBottom } from '@vincent.ri/react-tils'
// or only this hook
import { useStickToBottom } from '@vincent.ri/react-tils/useStickToBottom'

Usage

const [ref, { atBottom, scrollToBottom }] = useStickToBottom<HTMLDivElement>()
return (
  <>
    <div ref={ref} style={{ overflowY: 'auto', height: 500 }}>
      {messages.map((m) => <Message key={m.id} {...m} />)}
    </div>
    {!atBottom && <button onClick={() => scrollToBottom()}>Jump to latest</button>}
  </>
)

Live demo

Stream a reply, then scroll up while it streams.

Hi!

Ask me anything.

following: yes

When to use it

A chat, AI chat, terminal output, or live log.

When not to

New items appear at the top (a feed). Loading older messages above doesn't need it either, but Safari has no scroll anchoring, so there the view jumps unless you restore the position yourself: after prepending, add the change in scrollHeight to scrollTop. For only reading the scroll position, use useElementScroll.

API

function useStickToBottom<T extends HTMLElement = HTMLElement>(): [(element: T | null) => void, StickToBottom]

Returns [ref, { atBottom, scrollToBottom }]. Pass ref to the scrollable element (with overflow: auto and a fixed or max height).

Types

export type StickToBottom = {
  /** Following new content. `false` after the user scrolls up; `true` again at the bottom. */
  atBottom: boolean;
  /** Scroll to the bottom and follow new content again. Default behavior `'smooth'`. */
  scrollToBottom: (behavior?: ScrollBehavior) => void;
};

Browser support

UsesChromeEdgeFirefoxSafariChrome AndroidSafari iOS
Touch eventsoptionalLimited availability22+12+52+✕25+≤3+
wheel eventoptionalLimited availability31+12+17+7+31+✕
ResizeObserverWidely available64+79+69+13.1+64+13.4+
MutationObserverWidely available26+12+14+7+26+7+

Optional features are used when present; without them the hook still works.

First version with support, from MDN's compatibility data (web-features 3.42.0, @mdn/browser-compat-data 8.1.5).

Coming from another library?

Other libraries call this useAutoScroll, useChatScroll.