Handling scroll behavior for AI Chat Apps: A Simple Guide

Building a great AI chat experience requires more than just handling messages—the scrolling behavior can make or break the user experience. Users expect the chat to stay focused on the latest message, scroll smoothly during conversations, and feel responsive. Here's how to implement perfect scrolling behavior in your AI chat app.

Handling UX for chat scrolling can be challenging

I wanted a simple, scalable way to do this—in my case, I'm using the Vercel AI SDK.

Looking for a simple way to get started with AI chat apps? Check out the AI SDK for a quick start.

Let's solve these problems step by step.

🧠 Step 1: Add Padding to Last Message

The first issue to solve is ensuring there's enough space at the bottom of your chat. Without proper padding, the last message often gets cut off or sits uncomfortably close to the bottom edge. Leveraging CSS can help you prevent react re-renders. Please note the parent has a fixed height and overflow hidden

<div>
  <div className="flex flex-col overflow-hidden w-full bg-accent h-[calc(100vh-50px)] md:h-[calc(100vh-90px)]">
    
    <div ref={messageScrollRef} className="flex-grow overflow-y-auto scroll-smooth scrollbar-hide">
      {messages.map((message, index) => (
        <div
          key={index}
          data-message-role={message.role}
          className={"last:min-h-[calc(100dvh/2)]"}
        >
          {message.text}
        </div>
      ))}
    </div>
  </div>

  {/* Your prompt */}
  <div className="relative bottom-0 left-0 right-0 bg-accent">
    <textarea
      ref={textAreaRef}
      className="w-full h-full bg-accent"
      placeholder="Type your message..."
    />
  </div>
</div>

This approach uses Tailwind's last: modifier to add significant padding (50% of viewport height) only to the last message. This ensures users can comfortably read the entire conversation without content being hidden near the bottom. You can change this to however you want in my case this works for me.

🧠 Step 2: Create a Debounced Scroll Function

A function you can call to scroll to the bottom of the chat smoothly or instantly.

import { useDebounceCallback } from 'usehooks-ts';

// Debounced scroll to bottom
const scrollToBottom = useDebounceCallback((behavior: ScrollBehavior = "smooth") => {
  const scrollEl = messageScrollRef.current;
  if (scrollEl) {
    scrollEl.scrollTo({
      top: scrollEl.scrollHeight,
      behavior,
    });
  }
}, 20);

The behavior parameter allows you to choose between "smooth" animations and "instant" jumps depending on the context.

🧠 Step 3: Scroll on User Submit

When users send a message, they expect the chat to immediately focus on their new input. Trigger the scroll right when they submit:

const customSubmit = useCallback(async () => {

  sendMessage({
    role: "user",
    text: textAreaRef.current?.value || "",
    files: messageAttachments.length > 0 ? messageAttachments : undefined,
    metadata: {
      userId: user?.uid || "",
    },
  });
  
  scrollToBottom("smooth"); 
}, [scrollToBottom, sendMessage, user, messageAttachments]);

By calling scrollToBottom() after sendMessage(), this will scroll within the container to the bottom and leverge the padding to present the tast messaging in a natural location.

🧠 Step 4: First Paint We Scroll to Bottom Instantly

When users first load a chat with existing messages, you want to instantly show the most recent conversation without any scrolling animation:

const hasLoaded = useRef(false);

useEffect(() => {
  if (status === "ready" && messages.length > 0 && !hasLoaded.current) {
    scrollToBottom("instant");
    hasLoaded.current = true;
  }
}, [status, messages, scrollToBottom]);

The hasLoaded ref ensures this optimization only runs once when the chat initially loads. Using "instant" behavior eliminates any jarring scroll animations on first paint.

Wrapping Up

In closing, there is allot of ways to do this, after createing a couple chat UI's here is what I found as a approach I like. I feel its simple and I like the way it works.

The road ahead

Now you see how to scroll with refs, you can also add refs to the message and use the scrollIntoView method to scroll to the message.

<div
  key={message.id}
  ref={el => {
    if (messagesRef.current) {
      messagesRef.current[message.id] = el as HTMLDivElement;
    }
  }}
>
  {message.text}
</div>
const messageRefs = useRef<HTMLDivElement[]>([]);

messageRefs.current[message.id]?.scrollIntoView({ behavior: "smooth" });

This is a simple way to scroll to a specific message.

I hoped this helped you out if you have any questions or feedback please reach out to me on X or LinkedIn.