All hooks / Elements
useStickToBottom
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.
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
| Uses | Chrome | Edge | Firefox | Safari | Chrome Android | Safari iOS |
|---|---|---|---|---|---|---|
| Touch eventsoptionalLimited availability | 22+ | 12+ | 52+ | ✕ | 25+ | ≤3+ |
| wheel eventoptionalLimited availability | 31+ | 12+ | 17+ | 7+ | 31+ | ✕ |
| ResizeObserverWidely available | 64+ | 79+ | 69+ | 13.1+ | 64+ | 13.4+ |
| MutationObserverWidely available | 26+ | 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.