Model or dataset
stackblitz-labs/use-stick-to-bottom avatar
stackblitz-labs/use-stick-to-bottom

use-stick-to-bottom: a React hook for keeping chat UIs pinned to the newest message

A lightweight React Hook intended mainly for AI chat applications, for smoothly sticking to bottom of messages

777 stars58 forksTypeScriptMIT

At a glance

What is it?
use-stick-to-bottom is a zero-dependency React hook and component that keeps a scroll container pinned to the bottom while streamed content grows, and yields control the moment the user scrolls up. It is a narrow tool, and it is worth adopting only if your problem is exactly that one.
Who is it for?
Adopt use-stick-to-bottom if you are building a React chat interface where assistant output streams in token by token and the container must stay pinned to the newest line without fighting the user. Do not adopt it if your scroll container is not React, if you cannot wrap the message list in a ref, or if you need virtualised rendering of thousands of rows, because the hook measures a content element that must actually exist in the DOM.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 103 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The scroll problem use-stick-to-bottom was written to solve

A chat transcript is a scroll container whose height changes while the user is reading it. An assistant streams tokens, a code block finishes rendering, an image finishes loading, and the content below the viewport grows. If nothing compensates, the newest line drifts below the fold and the user has to chase it. The browser's own answer is CSS scroll anchoring, exposed through overflow-anchor, but the README points out that Safari does not support it, so a cross-browser chat UI cannot rely on it alone.

The audience is narrow and clearly stated: the package description says it is intended mainly for AI chat applications, and the README opens with "Designed with AI chat bots in mind". If you are building a message list that streams, this is the target case. If you are building a document viewer, a log tail, or an infinite feed, the same primitives may apply, but the design decisions (velocity-based animation, cancellation on user scroll) were made for streaming text.

How the hook decides to stick, and when it lets go

The mechanism is ref-based. The hook returns scrollRef, which you attach to the element with overflow: auto, and contentRef, which you attach to the child that holds the messages. The hook observes that content element with ResizeObserver, so it reacts to the content box changing size rather than to scroll events alone. The README notes this also covers content shrinking, not just growing taller.

Stickiness is a state, not a permanent mode. When the user scrolls up, the hook stops pinning. The README describes the detection as logic that distinguishes the user's own scrolling from the animation scroll events the hook itself generates, explicitly without debouncing, on the grounds that debouncing could drop events. That is the part worth scrutinising in your own app: any programmatic scroll that fires events the hook cannot attribute to itself risks being read as a user gesture. The README claims mobile works well with this logic, but it does not document the attribution rule in detail.

The animation is a custom smooth-scroll implementation using velocity-based spring parameters rather than an easing function with a fixed duration. The README's argument is that duration-based easing behaves badly when new content arrives with variable sizing, which is the normal case for streamed model output. scrollToBottom returns a Promise<boolean>: true when the scroll completed, false when it was cancelled.

Installing use-stick-to-bottom and wiring a first chat container

Installation is a single npm command as shown in the README. There is no build step, no peer package beyond React, and no stylesheet to import; the package ships as ESM with types, and the README describes it as zero-dependency.

bash
npm install use-stick-to-bottom

The README gives two integration styles. The component form handles the refs for you and provides context, so descendants can read isAtBottom and call scrollToBottom without prop drilling. This is the shape the README demonstrates, including resize and initial props set to "smooth":

jsx
import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';

function Chat() {
  return (
    <StickToBottom className="h-[50vh] relative" resize="smooth" initial="smooth">
      <StickToBottom.Content className="flex flex-col gap-4">
        {messages.map((message) => (
          <Message key={message.id} message={message} />
        ))}
      </StickToBottom.Content>
      <ScrollToBottom />
    </StickToBottom>
  );
}

The second form is the bare hook, for when you already have your own container markup and only want the behaviour. You attach the two refs yourself and render nothing extra:

jsx
import { useStickToBottom } from 'use-stick-to-bottom';

function Component() {
  const { scrollRef, contentRef } = useStickToBottom();

  return (
    <div style={{ overflow: 'auto' }} ref={scrollRef}>
      <div ref={contentRef}>
        {messages.map((message) => (
          <Message key={message.id} message={message} />
        ))}
      </div>
    </div>
  );
}

What you should see: with the refs placed as above and new items appended to the array, the container stays pinned to the last message. Scroll up mid-stream and the pin releases; the README's example renders a scroll-to-bottom button only while isAtBottom is false, and clicking it calls scrollToBottom().

Where use-stick-to-bottom is the wrong tool

The ref contract is the main failure mode. scrollRef must land on the scrolling element and contentRef on the child whose size changes. Put contentRef on the same node as scrollRef, or on a wrapper that never resizes, and ResizeObserver has nothing meaningful to report. The README does not document a warning or a development-mode check for this misconfiguration, so the symptom is silent: the list simply stops following new messages, and it looks like a bug in your streaming code.

Virtualised lists are a second boundary. If you render only the visible window of a long transcript, the content element's measured height does not correspond to the full transcript, and the assumption that the content box grows as messages arrive breaks down. The README does not claim compatibility with virtualisation libraries, and nothing in the repository layout suggests a virtualised example; the demo directory contains Demo.tsx, index.css, index.tsx and useFakeMessages.tsx, which points at a straightforward append-only list.

Finally, this is a React package. The hook needs a React render cycle and the peer range is ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0. If your chat UI is Vue, Svelte, or plain DOM, there is nothing here for you beyond reading the spring-scroll approach and reimplementing it.

How it differs from CSS overflow-anchor and from generic scroll libraries

The nearest built-in alternative is CSS scroll anchoring via overflow-anchor. The difference is support and control. Scroll anchoring is a browser behaviour: you get it or you do not, and the README states that Safari does not support it. use-stick-to-bottom implements the behaviour in JavaScript on top of ResizeObserver, so the same code path runs in every browser that supports the observer. The trade-off is that you now own the logic, including the user-versus-animation scroll attribution, which the browser would otherwise handle internally.

The second comparison is against scroll-animation libraries built on easing functions with fixed durations. The README draws that line itself: duration-based easing does not work well when content streams in with variable sizing. A spring parameterised by velocity can retarget mid-flight as the content box keeps changing. That is a real architectural difference, not a cosmetic one, and it is the reason the hook fits streaming output better than a generic smooth-scroll helper whose job is to move a fixed distance over a fixed time.

Maintenance, versioning and licence cost

The repository is not archived, and the last push was on 2026-06-04, which is recent enough that the project has not gone quiet. There are no releases retrieved in the repository listing, so version history has to be read from package.json, which declares version 1.1.6. Upgrades are cheap in the ordinary case: the package is ESM, ships a dist folder and type declarations, and the only peer dependency is React. There is no runtime dependency tree to audit and no CSS to keep in sync.

The licence is MIT, declared in package.json and shipped as LICENSE.txt at the repository root. That permits commercial use and modification; it also means the package comes with no warranty, which matters if the silent-ref-misconfiguration failure mode above reaches production. The README asks for sponsorship via GitHub Sponsors and the package.json carries a funding entry, but nothing in the repository suggests a paid tier or a support contract, so treat upstream help as best-effort. If you need a guaranteed response on a scroll bug, budget for reading the source under src/ yourself.

Editorial conclusion

Adopt use-stick-to-bottom if you are building a React chat interface where assistant output streams in token by token and the container must stay pinned to the newest line without fighting the user. Do not adopt it if your scroll container is not React, if you cannot wrap the message list in a ref, or if you need virtualised rendering of thousands of rows, because the hook measures a content element that must actually exist in the DOM. Before committing, verify two things in your own app: that the container is the element receiving overflow: auto and that contentRef sits on the direct child holding the messages, and that your React version satisfies the peer range ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0. If either check fails, the hook will appear to do nothing, and no amount of tuning resize or initial will fix it.

Frequently asked questions

What does use-stick-to-bottom do that CSS scroll anchoring does not?

It implements the behaviour in JavaScript using ResizeObserver instead of relying on the browser's overflow-anchor support, which the README notes Safari does not provide. That means the same code path runs across browsers, at the cost of you owning the stickiness logic.

Does use-stick-to-bottom work on mobile?

The README states that mobile devices work well with the logic that distinguishes user scrolling from the hook's own animation scroll events, and that this is done without debouncing. It does not document separate mobile configuration, so no extra setup is described.

Why does use-stick-to-bottom use spring animations instead of easing?

The README argues that easing functions with fixed durations do not work well when new content streams in with variable sizing, which is common in AI chatbot output. The hook instead uses velocity-based spring animations with configurable parameters.

What React versions does use-stick-to-bottom support?

The peer dependency range in package.json is ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0. React is the only peer dependency, and the package is described as zero-dependency.

How do I scroll to the bottom programmatically with use-stick-to-bottom?

Call scrollToBottom, either from useStickToBottomContext inside a StickToBottom tree or from the hook's return value. The README states it returns a Promise<boolean> that resolves true when the scroll succeeded and false when it was cancelled.

What licence is use-stick-to-bottom released under?

MIT, as declared in package.json and included as LICENSE.txt at the repository root. The README also asks users to sponsor the author on GitHub, and package.json carries a funding entry.

Official sources

  1. Issues
  2. License: MIT
  3. Project website
  4. README
  5. stackblitz-labs/use-stick-to-bottom on GitHub
Community notes

Community notes