Search IOCombats

Search challenges, guides, questions and articles

How should auto-scroll behave while an answer streams, without fighting the user?

Intermediate10 min interview
Skills tested:
Computing whether a scroll container is at the bottomKeeping a stick-to-bottom flag that respects user intentReacting to content growth with ResizeObserverDesigning a Jump to latest affordanceHandling reduced motion and new-message behaviour

Advertisement

🧩 Scenario

Users of your AI chat complain that while a long answer streams, they cannot scroll up to re-read the beginning: the page keeps dragging them back to the bottom. Others complain that when they do scroll up, they lose track of the answer. The interviewer asks you to define and implement the correct behaviour.

Concept Walkthrough

The rule in one line

Follow the stream only if the user is already following it. That is the same rule a terminal or a log viewer uses. Auto-scroll is a convenience for people watching the end of the answer, not a rule that overrides people reading something else.

Three pieces of state

  1. Is the user at the bottom? Compute it from the scroll container: scrollHeight - scrollTop - clientHeight is the distance from the bottom. If it is under a small tolerance, treat it as "at bottom".
  2. Should we stick? A boolean, updated on every scroll event from the position check. If the user scrolls up, it becomes false. If they scroll back down to the bottom, it becomes true again.
  3. Has new content arrived while not stuck? This drives the Jump to latest button.

When we scroll programmatically, the browser fires a scroll event too. That is fine: after our scroll we are at the bottom, so the check keeps stick as true.

When to scroll

Scroll when the content actually grows, not when a piece of state changes. Markdown rendering, code highlighting and images can change height after the text update, so a useEffect that watches the text can fire too early. A ResizeObserver on the inner content element fires whenever its size changes. Inside the callback: if stick is true, set scrollTop to scrollHeight.

Instant, not smooth

While following a stream, jump instantly. A smooth scroll takes time to finish, and new tokens keep arriving, so the animation keeps restarting and lags behind. Keep smooth scrolling for the button click, and skip it when the user has prefers-reduced-motion: reduce.

When sticking turns back on

  • The user scrolls back to the bottom.
  • The user clicks Jump to latest.
  • The user sends a new message (they clearly want to see the reply).

Key Code Explained

import { useCallback, useEffect, useRef, useState } from 'react';

const BOTTOM_TOLERANCE_PX = 24; // tune for your layout

export function useStickToBottom() {
  const containerRef = useRef<HTMLDivElement>(null); // the scrollable element
  const contentRef = useRef<HTMLDivElement>(null); // the element that grows
  const stickRef = useRef(true); // read inside observers without re-renders
  const [isStuck, setIsStuck] = useState(true); // drives the Jump button

  const scrollToBottom = useCallback((smooth = false) => {
    const el = containerRef.current;
    if (!el) return;
    const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    el.scrollTo({ top: el.scrollHeight, behavior: smooth && !reduceMotion ? 'smooth' : 'auto' });
    stickRef.current = true;
    setIsStuck(true);
  }, []);

  useEffect(() => {
    const container = containerRef.current;
    const content = contentRef.current;
    if (!container || !content) return;

    const onScroll = () => {
      const distance = container.scrollHeight - container.scrollTop - container.clientHeight;
      const atBottom = distance <= BOTTOM_TOLERANCE_PX;
      stickRef.current = atBottom;
      setIsStuck(atBottom); // React skips the render when the value is unchanged
    };

    // Follow real size changes: new tokens, highlighted code, loaded images
    const observer = new ResizeObserver(() => {
      if (stickRef.current) {
        container.scrollTop = container.scrollHeight; // instant while streaming
      }
    });

    container.addEventListener('scroll', onScroll, { passive: true });
    observer.observe(content);
    return () => {
      container.removeEventListener('scroll', onScroll);
      observer.disconnect();
    };
  }, []);

  return { containerRef, contentRef, isStuck, scrollToBottom };
}

// Usage
function ChatPanel({ children }: { children: React.ReactNode }) {
  const { containerRef, contentRef, isStuck, scrollToBottom } = useStickToBottom();
  return (
    <div className='chat-panel'>
      <div ref={containerRef} className='chat-scroll'>
        <div ref={contentRef}>{children}</div>
      </div>
      {!isStuck && (
        <button type='button' onClick={() => scrollToBottom(true)}>
          Jump to latest
        </button>
      )}
    </div>
  );
}

stickRef holds the live value so the ResizeObserver callback never reads a stale closure, while isStuck state exists only to show or hide the button. Call scrollToBottom() from your send handler so a new question always brings the user to the reply.


Tradeoffs

ApproachRespects the userHandles late layout changesNotes
scrollIntoView on every chunkNoPartlySimplest, causes the complaint in this scenario
useEffect on text + stick flagYesNoMisses height changes after render
ResizeObserver + stick flagYesYesSmall hook, the approach shown here
Library (for example a stick-to-bottom hook package)UsuallyUsuallyLess code, check its behaviour on your layout

What Interviewers Actually Check

  • Whether you define the behaviour before writing code
  • Whether you stop auto-scrolling when the user scrolls up
  • Whether you use a tolerance instead of an exact bottom check
  • Whether you react to real size changes, not just state changes
  • Whether you offer a way back (Jump to latest) and handle new messages

Follow-Up Questions

  1. How does this change if the message list is virtualized?
  2. How would you handle loading older messages at the top without the view jumping?
  3. Should the Jump button show a count of new messages or just an arrow? Why?
  4. How would you test this behaviour with Playwright?
  5. How does the hook behave on mobile when the on-screen keyboard opens?

Common Candidate Mistakes

  • Treating auto-scroll as always on, which makes reading earlier content impossible
  • Re-enabling sticking on every new chunk, which defeats the user's scroll up
  • Using smooth scrolling for every update, so the view lags behind the stream
  • Observing the scroll container's own size instead of the content inside it
  • Forgetting to disconnect the observer and remove listeners on unmount

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you write the distance-from-bottom calculation from memory?
  • Can you explain the three moments sticking turns back on?
  • Can you explain why ResizeObserver is better than a text-based effect here?
  • Can you explain why instant scroll is better while streaming?
  • Can you design the Jump to latest button and its visibility rule?

Summary

Auto-scroll during streaming should follow the answer only while the user is already at the bottom. Track a stick flag from the scroll position with a small tolerance, turn it off when the user scrolls up and back on when they return to the bottom, click Jump to latest, or send a new message. Follow content growth with a ResizeObserver on the inner element so late layout changes are handled, jump instantly while streaming, and keep smooth scrolling (respecting reduced motion) for the button.

Frequently Asked Questions

Why not just call scrollIntoView on every chunk?

Because it ignores what the user is doing. If they scrolled up to read an earlier part of the answer, every new chunk yanks them back to the bottom. It also runs far more often than needed.

Should the auto-scroll be smooth?

Usually not while following the stream. Smooth scrolling takes time, and new content arrives before the animation ends, so it can feel laggy and the scroll position can end up behind. Use an instant jump while following, and smooth scrolling (respecting prefers-reduced-motion) for the Jump to latest button.

What threshold counts as "at the bottom"?

Use a small tolerance in pixels rather than an exact zero, because fractional scroll values and padding make an exact match unreliable. Tune the number for your layout.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Namaste Dev
Sponsored

Cohort-style courses to go from tutorials to job-ready.

Explore Courses

Advertisement