DeltaKitDeltaKit
OpenAI Agents

Scroll to Bottom

Recipe for showing a "new messages" button when users scroll up during streaming.

This recipe shows how to show a scroll-to-bottom button when the user has scrolled away from the latest messages. You'll use the isAtBottom state from useAutoScroll to conditionally render a button that jumps to new content.

Problem

When users scroll up to read earlier messages, new streaming content appears below the visible area. You need to:

  • Detect when the user is not at the bottom
  • Show a button to quickly jump to new messages
  • Maintain scroll position during streaming
  • Provide clear visual feedback

Solution

Use useAutoScroll's isAtBottom return value combined with a positioned button:

import { useStreamChat, useAutoScroll } from "@deltakit/react";

function Chat() {
  const { messages, sendMessage } = useStreamChat({ api: "/api/chat" });
  const { ref, scrollToBottom, isAtBottom } = useAutoScroll([messages]);

  return (
    <div className="relative flex h-screen flex-col">
      {/* Scrollable message area */}
      <div ref={ref} className="flex-1 overflow-y-auto p-4">
        {messages.map((msg) => (
          <div key={msg.id} className="mb-4">
            <strong className="text-sm text-neutral-500">{msg.role}:</strong>
            <div className="mt-1">
              {msg.parts
                .filter((p) => p.type === "text")
                .map((p) => p.text)
                .join("")}
            </div>
          </div>
        ))}
      </div>

      {/* Scroll to bottom indicator */}
      {!isAtBottom && (
        <button
          onClick={scrollToBottom}
          className="absolute bottom-20 left-1/2 -translate-x-1/2 rounded-full bg-neutral-800 px-4 py-2 text-sm text-neutral-200 shadow-lg hover:bg-neutral-700"
        >
          <span className="flex items-center gap-2">
            <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
            </svg>
            New messages below
          </span>
        </button>
      )}

      {/* Input form */}
      <form
        className="border-t border-neutral-800 p-4"
        onSubmit={(e) => {
          e.preventDefault();
          const input = e.currentTarget.elements.namedItem("message") as HTMLInputElement;
          if (!input.value.trim()) return;
          sendMessage(input.value);
          input.value = "";
        }}
      >
        <div className="flex gap-2">
          <input
            name="message"
            placeholder="Type a message..."
            className="flex-1 rounded border border-neutral-700 bg-neutral-800 px-3 py-2"
          />
          <button type="submit" className="rounded bg-neutral-100 px-4 py-2 text-neutral-900">
            Send
          </button>
        </div>
      </form>
    </div>
  );
}

How It Works

  1. useAutoScroll tracks position — The hook monitors scroll position and sets isAtBottom to false when the user scrolls up
  2. Conditional rendering — When isAtBottom is false, show the indicator button
  3. Jump to bottom — Clicking the button calls scrollToBottom() to re-pin the view
  4. Re-pins automatically — If the user scrolls back near the bottom, isAtBottom becomes true and the button hides

Customization

Badge with Message Count

Show how many new messages arrived while scrolled up:

const [lastSeenCount, setLastSeenCount] = useState(0);
const newMessageCount = messages.length - lastSeenCount;

// Update when scrolling to bottom
const handleScrollToBottom = () => {
  scrollToBottom();
  setLastSeenCount(messages.length);
};

// In render
{!isAtBottom && newMessageCount > 0 && (
  <button onClick={handleScrollToBottom}>
    {newMessageCount} new message{newMessageCount > 1 ? 's' : ''}
  </button>
)}

Animated Appearance

Add a smooth fade-in animation:

<button
  className={`absolute bottom-20 left-1/2 -translate-x-1/2 transition-all duration-300 ${
    isAtBottom ? 'opacity-0 translate-y-2 pointer-events-none' : 'opacity-100 translate-y-0'
  }`}
>
  New messages below
</button>

Position Variants

Place the indicator in different locations:

// Bottom-right corner
<button className="absolute bottom-20 right-4 ...">

// Centered with full width
<button className="absolute bottom-20 left-4 right-4 ...">

// Floating action button style
<button className="absolute bottom-20 right-4 rounded-full w-12 h-12 ...">
  <ArrowDownIcon />
</button>

Best Practices

  1. Position carefully — Place above the input area so it doesn't overlap typing
  2. Clear labeling — Use "New messages" or show the count, not just an arrow
  3. Auto-dismiss — Hide immediately when user scrolls to bottom manually
  4. Respect user intent — Don't auto-scroll if the user has deliberately scrolled up to read

On this page