Repository: patternfly/chatbot
Pull request #550: Implement Smart Scroll Feature in MessageBox Component
Author: login: karthikjeeyar
Labels: name: released on @prerelease
Created: 2025-05-08T18:54:18Z | Merged: 2025-05-19T19:19:04Z

## Fixes:

https://github.com/patternfly/chatbot/issues/549

## Description:

This PR introduces the optional `enableSmartScroll` feature in the MessageBox component, which provides a smoother and more user-friendly experience when interacting with dynamically updated content. The feature enhances scrolling behavior in the following ways:


- *Pause and Resume Auto-Scroll*: Automatically pauses scrolling when the user scrolls up, and resumes once the user scrolls back down or clicks the "Back to Bottom" button.

- *Imperative Handle Methods*: Exposes imperative methods (`scrollToTop`, `scrollToBottom`, `isSmartScrollActive`) via ref to provide programmatic control over the scrolling behavior.

- *Controlled Auto-Scroll*: The component will not scroll automatically unless the user scrolls to the bottom or uses the exposed methods. The user has full control over the scroll position.


###  Example Usage


```tsx
const ref = useRef<MessageBoxHandle>(null);

React.useEffect(() => {
  // Scroll to bottom programmatically
  ref.current?.scrollToBottom();
}, [messages])

const onNewMessage = () => { 
 ...other logic
 // resumes the smart scroll and scrolls to bottom.
 ref.currrent?.scrollToBottom({resumeSmartScroll: true});
}

<MessageBox ref={ref} enableSmartScroll>
{/* message content */}
</MessageBox>

```

## Screenshots/Videos


https://github.com/user-attachments/assets/1fec1549-4383-41a5-8e71-4242d190d37d

## Demo

https://chatbot-pr-chatbot-550.surge.sh/patternfly-ai/chatbot/messages/demo#message-auto-scrolling


## Unit tests

```
  MessageBox
    ✓ should call user defined onWheel, onTouchMove and onTouchEnd handlers (5 ms)
    ✓ should scroll to the bottom when the method is called  (4 ms)
    ✓ should scroll to the top when the method is called  (4 ms)
    ✓ should pause automatic scrolling when user scrolls up  (8 ms)
    ✓ should resume automatic scrolling when user scrolls down to the bottom using scroll event (9 ms)
    ✓ should resume automatic scrolling when scrollToBottom method is used (7 ms)
    ✓ should resume automatic scrolling when mouse wheel event is used (7 ms)
    ✓ should resume automatic scrolling when user swipes up in touch screen (7 ms)
 ```



Diff:
diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md
index 95e2a3d07..da1d47e63 100644
--- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md
+++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md
@@ -45,6 +45,8 @@ import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg';
 import userAvatar from '../Messages/user_avatar.svg';
 import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
 import { FunctionComponent, useState, useRef, isValidElement, cloneElement, Children, ReactNode, MouseEvent } from 'react';
+import { getTrackingProviders } from "@patternfly/chatbot/dist/dynamic/tracking";
+import ChatbotConversationHistoryNav from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
 
 ## Demos
 
@@ -62,6 +64,40 @@ It is also important to announce when new content appears onscreen for accessibi
 
 ```
 
+### Message auto-scrolling
+
+This demo shows auto-scrolling functionality, which automatically scrolls to the bottom of the active chat.
+
+To enable auto-scroll behavior pass the `enableSmartScroll` prop to the [`<MessageBox>`](/patternfly-ai/chatbot/ui#message-box) component.
+
+When enabled:
+
+- Scroll position is automatically managed based on user interaction.
+- Scrolling is _not_ forced to the bottom when new messages arrive, unless explicitly triggered via the `scrollToBottom()` method.
+- If the user scrolls up or interacts with UI controls like "Back to top" or "Back to bottom", the component pauses auto-scroll to respect user intent.
+- Auto-scroll resumes only when the user scrolls back down manually or programmatically via the `scrollToBottom({resumeSmartScroll: true})` method.
+
+#### Imperative methods via `ref`
+
+When using `ref`, the `<MessageBox>` component exposes the following methods:
+
+- `scrollToBottom()`: Scrolls to the bottom of the message container.
+- `scrollToTop()`: Scrolls to the top of the message container.
+- `isSmartScrollActive()`: Returns `true` if smart auto-scroll is currently active.
+- Native `HTMLDivElement` methods like `scrollTo()`.
+
+This demo includes broader ChatBot features, including:
+
+1. A [`<ChatbotToggle>`](/patternfly-ai/chatbot/ui#toggle) that controls the [`<Chatbot>`](/patternfly-ai/chatbot/ui#container) container.
+2. A `<ChatbotContent>` and [`<MessageBox>`](/patternfly-ai/chatbot/ui#content-and-message-box) with:
+   - A `<ChatbotWelcomePrompt>`
+   - An initial user message and initial bot message
+3. A [`<ChatbotFooter>`](/patternfly-ai/chatbot/ui#footer) with a [`<ChatbotFootnote>`](/patternfly-ai/chatbot/ui#footnote-with-popover) and a `<MessageBar>`
+
+```js file="./ChatbotScrolling.tsx" isFullscreen
+
+```
+
 ### Attach via upload button in message bar
 
 This demo displays unique attachment features, including:
diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotScrolling.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotScrolling.tsx
new file mode 100644
index 000000000..ad4d2bc5a
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotScrolling.tsx
@@ -0,0 +1,536 @@
+import React from 'react';
+
+import { Bullseye, Brand, DropdownList, DropdownItem, DropdownGroup, SkipToContent } from '@patternfly/react-core';
+
+import ChatbotToggle from '@patternfly/chatbot/dist/dynamic/ChatbotToggle';
+import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
+import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent';
+import ChatbotWelcomePrompt from '@patternfly/chatbot/dist/dynamic/ChatbotWelcomePrompt';
+import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter';
+import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar';
+import MessageBox, { MessageBoxHandle } from '@patternfly/chatbot/dist/dynamic/MessageBox';
+import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message';
+import ChatbotConversationHistoryNav, {
+  Conversation
+} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
+import ChatbotHeader, {
+  ChatbotHeaderMenu,
+  ChatbotHeaderMain,
+  ChatbotHeaderTitle,
+  ChatbotHeaderActions,
+  ChatbotHeaderSelectorDropdown,
+  ChatbotHeaderOptionsDropdown
+} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader';
+
+import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
+import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon';
+import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon';
+
+import PFHorizontalLogoColor from '../UI/PF-HorizontalLogo-Color.svg';
+import PFHorizontalLogoReverse from '../UI/PF-HorizontalLogo-Reverse.svg';
+import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg';
+import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg';
+import userAvatar from '../Messages/user_avatar.svg';
+import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
+import '@patternfly/react-core/dist/styles/base.css';
+import '@patternfly/chatbot/dist/css/main.css';
+
+const footnoteProps = {
+  label: 'ChatBot uses AI. Check for mistakes.',
+  popover: {
+    title: 'Verify information',
+    description: `While ChatBot strives for accuracy, AI is experimental and can make mistakes. We cannot guarantee that all information provided by ChatBot is up to date or without error. You should always verify responses using reliable sources, especially for crucial information and decision making.`,
+    bannerImage: {
+      src: 'https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif',
+      alt: 'Example image for footnote popover'
+    },
+    cta: {
+      label: 'Dismiss',
+      onClick: () => {
+        alert('Do something!');
+      }
+    },
+    link: {
+      label: 'View AI policy',
+      url: 'https://www.redhat.com/'
+    }
+  }
+};
+
+const markdown = `A paragraph with *emphasis* and **strong importance**.
+
+> A block quote with ~strikethrough~ and a URL: https://reactjs.org.
+
+Here is an inline code - \`() => void\`
+
+Here is some YAML code:
+
+~~~yaml
+apiVersion: helm.openshift.io/v1beta1/
+kind: HelmChartRepository
+metadata:
+  name: azure-sample-repo0oooo00ooo
+spec:
+  connectionConfig:
+  url: https://raw.githubusercontent.com/Azure-Samples/helm-charts/master/docs
+~~~
+
+Here is some JavaScript code:
+
+~~~js
+import React from 'react';
+
+const MessageLoading = () => (
+  <div className="pf-chatbot__message-loading">
+    <span className="pf-chatbot__message-loading-dots">
+      <span className="pf-v6-screen-reader">Loading message</span>
+    </span>
+  </div>
+);
+
+export default MessageLoading;
+
+~~~
+`;
+
+// It's important to set a date and timestamp prop since the Message components re-render.
+// The timestamps re-render with them.
+const date = new Date();
+
+const initialMessages: MessageProps[] = [
+  {
+    id: '1',
+    role: 'user',
+    content: 'Hello, can you give me an example of what you can do?',
+    name: 'User',
+    avatar: userAvatar,
+    timestamp: date.toLocaleString(),
+    avatarProps: { isBordered: true }
+  },
+  {
+    id: '2',
+    role: 'bot',
+    content: markdown,
+    name: 'Bot',
+    avatar: patternflyAvatar,
+    timestamp: date.toLocaleString(),
+    actions: {
+      // eslint-disable-next-line no-console
+      positive: { onClick: () => console.log('Good response') },
+      // eslint-disable-next-line no-console
+      negative: { onClick: () => console.log('Bad response') },
+      // eslint-disable-next-line no-console
+      copy: { onClick: () => console.log('Copy') },
+      // eslint-disable-next-line no-console
+      share: { onClick: () => console.log('Share') },
+      // eslint-disable-next-line no-console
+      listen: { onClick: () => console.log('Listen') }
+    }
+  }
+];
+
+const welcomePrompts = [
+  {
+    title: 'Set up account',
+    message: 'Choose the necessary settings and preferences for your account.'
+  },
+  {
+    title: 'Troubleshoot issue',
+    message: 'Find documentation and instructions to resolve your issue.'
+  }
+];
+
+const initialConversations = {
+  Today: [{ id: '1', text: 'Hello, can you give me an example of what you can do?' }],
+  'This month': [
+    {
+      id: '2',
+      text: 'Enterprise Linux installation and setup'
+    },
+    { id: '3', text: 'Troubleshoot system crash' }
+  ],
+  March: [
+    { id: '4', text: 'Ansible security and updates' },
+    { id: '5', text: 'Red Hat certification' },
+    { id: '6', text: 'Lightspeed user documentation' }
+  ],
+  February: [
+    { id: '7', text: 'Crashing pod assistance' },
+    { id: '8', text: 'OpenShift AI pipelines' },
+    { id: '9', text: 'Updating subscription plan' },
+    { id: '10', text: 'Red Hat licensing options' }
+  ],
+  January: [
+    { id: '11', text: 'RHEL system performance' },
+    { id: '12', text: 'Manage user accounts' }
+  ]
+};
+
+export const ChatbotScrollingDemo: React.FunctionComponent = () => {
+  const [chatbotVisible, setChatbotVisible] = React.useState<boolean>(true);
+  const [displayMode, setDisplayMode] = React.useState<ChatbotDisplayMode>(ChatbotDisplayMode.default);
+  const [messages, setMessages] = React.useState<MessageProps[]>(initialMessages);
+  const [selectedModel, setSelectedModel] = React.useState('Granite 7B');
+  const [isSendButtonDisabled, setIsSendButtonDisabled] = React.useState(false);
+  const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);
+  const [conversations, setConversations] = React.useState<Conversation[] | { [key: string]: Conversation[] }>(
+    initialConversations
+  );
+  const [announcement, setAnnouncement] = React.useState<string>();
+  const toggleRef = React.useRef<HTMLButtonElement>(null);
+  const chatbotRef = React.useRef<HTMLDivElement>(null);
+  const historyRef = React.useRef<HTMLButtonElement>(null);
+  const messageBoxRef = React.useRef<MessageBoxHandle>(null); // ref to MessageBox will have isSmartScrollActive, scrollToTop and scrollToBottom
+  const scrollQueued = React.useRef(false);
+
+  // Auto-scrolls to the latest message
+  React.useLayoutEffect(() => {
+    if (!messageBoxRef.current?.isSmartScrollActive() || scrollQueued.current) {
+      return undefined;
+    }
+
+    let rafId = 0;
+    // don't scroll the first load - in this demo, we know we start with two messages
+    if (messages.length > 2) {
+      scrollQueued.current = true;
+
+      rafId = requestAnimationFrame(() => {
+        messageBoxRef.current?.scrollToBottom();
+        scrollQueued.current = false;
+      });
+    }
+
+    return () => {
+      cancelAnimationFrame(rafId);
+      scrollQueued.current = false;
+    };
+    // To scroll to the top of latest message just once, Use messages.length as a dependency instead of messages.
+  }, [messages, messageBoxRef]);
+
+  const onSelectModel = (
+    _event: React.MouseEvent<Element, MouseEvent> | undefined,
+    value: string | number | undefined
+  ) => {
+    setSelectedModel(value as string);
+  };
+
+  const onSelectDisplayMode = (
+    _event: React.MouseEvent<Element, MouseEvent> | undefined,
+    value: string | number | undefined
+  ) => {
+    setDisplayMode(value as ChatbotDisplayMode);
+  };
+
+  // you will likely want to come up with your own unique id function; this is for demo purposes only
+  const generateId = () => {
+    const id = Date.now() + Math.random();
+    return id.toString();
+  };
+
+  const handleSend = (message) => {
+    setIsSendButtonDisabled(true);
+    const newMessages: MessageProps[] = [];
+    // We can't use structuredClone since messages contains functions, but we can't mutate
+    // items that are going into state or the UI won't update correctly
+    messages.forEach((message) => newMessages.push(message));
+    // It's important to set a timestamp prop since the Message components re-render.
+    // The timestamps re-render with them.
+    const date = new Date();
+    newMessages.push({
+      id: generateId(),
+      role: 'user',
+      content: message,
+      name: 'User',
+      avatar: userAvatar,
+      timestamp: date.toLocaleString(),
+      avatarProps: { isBordered: true }
+    });
+    newMessages.push({
+      id: generateId(),
+      role: 'bot',
+      content: 'API response goes here',
+      name: 'Bot',
+      isLoading: true,
+      avatar: patternflyAvatar,
+      timestamp: date.toLocaleString()
+    });
+    setMessages(newMessages);
+    messageBoxRef.current?.scrollToBottom();
+    // make announcement to assistive devices that new messages have been added
+    setAnnouncement(`Message from User: ${message}. Message from Bot is loading.`);
+
+    // this is for demo purposes only; in a real situation, there would be an API response we would wait for
+    setTimeout(() => {
+      const loadedMessages: MessageProps[] = [];
+      // We can't use structuredClone since messages contains functions, but we can't mutate
+      // items that are going into state or the UI won't update correctly
+      newMessages.forEach((message) => loadedMessages.push(message));
+      const messageIndex = loadedMessages.length - 1;
+      loadedMessages.pop();
+      loadedMessages.push({
+        id: generateId(),
+        role: 'bot',
+        content: 'API response goes here.',
+        name: 'Bot',
+        isLoading: false,
+        avatar: patternflyAvatar,
+        timestamp: date.toLocaleString(),
+        actions: {
+          // eslint-disable-next-line no-console
+          positive: { onClick: () => console.log('Good response') },
+          // eslint-disable-next-line no-console
+          negative: { onClick: () => console.log('Bad response') },
+          // eslint-disable-next-line no-console
+          copy: { onClick: () => console.log('Copy') },
+          // eslint-disable-next-line no-console
+          share: { onClick: () => console.log('Share') },
+          // eslint-disable-next-line no-console
+          listen: { onClick: () => console.log('Listen') }
+        }
+      });
+
+      const longMessage =
+        `It includes a long message. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
+          magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+          consequat. Duis aute irure dolor in reprehenderit in voluptatevelit esse cillum dolore eu fugiat nulla
+          pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
+          est laborum.`.repeat(2);
+
+      const contentChunks = longMessage.split(' ');
+      let chunkIndex = 0;
+
+      setMessages(loadedMessages);
+
+      let accumulatedContent = '';
+      let lastUpdateTime = 0;
+      const interval = setInterval(() => {
+        if (chunkIndex < contentChunks.length) {
+          accumulatedContent += ` ${contentChunks[chunkIndex]}`;
+          chunkIndex++;
+
+          // Only update every 100 ms or every 10 chunks
+          const now = Date.now();
+          const shouldUpdate = now - lastUpdateTime > 100 || chunkIndex % 10 === 0;
+
+          if (shouldUpdate) {
+            const updatedMessages = loadedMessages.map((msg, i) => {
+              if (i === messageIndex) {
+                return {
+                  ...msg,
+                  content: msg.content + accumulatedContent
+                };
+              }
+              return msg;
+            });
+
+            setMessages(updatedMessages);
+            lastUpdateTime = now;
+          }
+        } else {
+          setIsSendButtonDisabled(false);
+          clearInterval(interval);
+        }
+      }, 50);
+
+      // Scrolls to the bottom of the message box and resumes auto-scrolling
+      messageBoxRef.current?.scrollToBottom({ resumeSmartScroll: true });
+      // make announcement to assistive devices that new message has loaded
+      setAnnouncement(`Message from Bot: API response goes here`);
+    }, 100);
+  };
+
+  const findMatchingItems = (targetValue: string) => {
+    let filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => {
+      const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase()));
+      if (filteredItems.length > 0) {
+        acc[key] = filteredItems;
+      }
+      return acc;
+    }, {});
+
+    // append message if no items are found
+    if (Object.keys(filteredConversations).length === 0) {
+      filteredConversations = [{ id: '13', noIcon: true, text: 'No results found' }];
+    }
+    return filteredConversations;
+  };
+
+  const horizontalLogo = (
+    <Bullseye>
+      <Brand className="show-light" src={PFHorizontalLogoColor} alt="PatternFly" />
+      <Brand className="show-dark" src={PFHorizontalLogoReverse} alt="PatternFly" />
+    </Bullseye>
+  );
+
+  const iconLogo = (
+    <>
+      <Brand className="show-light" src={PFIconLogoColor} alt="PatternFly" />
+      <Brand className="show-dark" src={PFIconLogoReverse} alt="PatternFly" />
+    </>
+  );
+
+  const handleSkipToContent = (e) => {
+    e.preventDefault();
+    /* eslint-disable indent */
+    switch (displayMode) {
+      case ChatbotDisplayMode.default:
+        if (!chatbotVisible && toggleRef.current) {
+          toggleRef.current.focus();
+        }
+        if (chatbotVisible && chatbotRef.current) {
+          chatbotRef.current.focus();
+        }
+        break;
+
+      case ChatbotDisplayMode.docked:
+        if (chatbotRef.current) {
+          chatbotRef.current.focus();
+        }
+        break;
+      default:
+        if (historyRef.current) {
+          historyRef.current.focus();
+        }
+        break;
+    }
+    /* eslint-enable indent */
+  };
+
+  return (
+    <>
+      <SkipToContent onClick={handleSkipToContent} href="#">
+        Skip to chatbot
+      </SkipToContent>
+      <ChatbotToggle
+        tooltipLabel="Chatbot"
+        isChatbotVisible={chatbotVisible}
+        onToggleChatbot={function () {
+          setChatbotVisible(!chatbotVisible);
+        }}
+        id="chatbot-toggle"
+        ref={toggleRef}
+      />
+      <Chatbot isVisible={chatbotVisible} displayMode={displayMode} ref={chatbotRef}>
+        <ChatbotConversationHistoryNav
+          displayMode={displayMode}
+          onDrawerToggle={() => {
+            setIsDrawerOpen(!isDrawerOpen);
+            setConversations(initialConversations);
+          }}
+          isDrawerOpen={isDrawerOpen}
+          setIsDrawerOpen={setIsDrawerOpen}
+          activeItemId="1"
+          // eslint-disable-next-line no-console
+          onSelectActiveItem={(e, selectedItem) => console.log(`Selected history item with id ${selectedItem}`)}
+          conversations={conversations}
+          onNewChat={() => {
+            setIsDrawerOpen(!isDrawerOpen);
+            setMessages([]);
+            setConversations(initialConversations);
+          }}
+          handleTextInputChange={(value: string) => {
+            if (value === '') {
+              setConversations(initialConversations);
+            }
+            // this is where you would perform search on the items in the drawer
+            // and update the state
+            const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value);
+            setConversations(newConversations);
+          }}
+          drawerContent={
+            <>
+              <ChatbotHeader>
+                <ChatbotHeaderMain>
+                  <ChatbotHeaderMenu
+                    ref={historyRef}
+                    aria-expanded={isDrawerOpen}
+                    onMenuToggle={() => setIsDrawerOpen(!isDrawerOpen)}
+                  />
+                  <ChatbotHeaderTitle
+                    displayMode={displayMode}
+                    showOnFullScreen={horizontalLogo}
+                    showOnDefault={iconLogo}
+                  ></ChatbotHeaderTitle>
+                </ChatbotHeaderMain>
+                <ChatbotHeaderActions>
+                  <ChatbotHeaderSelectorDropdown value={selectedModel} onSelect={onSelectModel}>
+                    <DropdownList>
+                      <DropdownItem value="Granite 7B" key="granite">
+                        Granite 7B
+                      </DropdownItem>
+                      <DropdownItem value="Llama 3.0" key="llama">
+                        Llama 3.0
+                      </DropdownItem>
+                      <DropdownItem value="Mistral 3B" key="mistral">
+                        Mistral 3B
+                      </DropdownItem>
+                    </DropdownList>
+                  </ChatbotHeaderSelectorDropdown>
+                  <ChatbotHeaderOptionsDropdown onSelect={onSelectDisplayMode}>
+                    <DropdownGroup label="Display mode">
+                      <DropdownList>
+                        <DropdownItem
+                          value={ChatbotDisplayMode.default}
+                          key="switchDisplayOverlay"
+                          icon={<OutlinedWindowRestoreIcon aria-hidden />}
+                          isSelected={displayMode === ChatbotDisplayMode.default}
+                        >
+                          <span>Overlay</span>
+                        </DropdownItem>
+                        <DropdownItem
+                          value={ChatbotDisplayMode.docked}
+                          key="switchDisplayDock"
+                          icon={<OpenDrawerRightIcon aria-hidden />}
+                          isSelected={displayMode === ChatbotDisplayMode.docked}
+                        >
+                          <span>Dock to window</span>
+                        </DropdownItem>
+                        <DropdownItem
+                          value={ChatbotDisplayMode.fullscreen}
+                          key="switchDisplayFullscreen"
+                          icon={<ExpandIcon aria-hidden />}
+                          isSelected={displayMode === ChatbotDisplayMode.fullscreen}
+                        >
+                          <span>Fullscreen</span>
+                        </DropdownItem>
+                      </DropdownList>
+                    </DropdownGroup>
+                  </ChatbotHeaderOptionsDropdown>
+                </ChatbotHeaderActions>
+              </ChatbotHeader>
+              <ChatbotContent>
+                {/* Update the announcement prop on MessageBox whenever a new message is sent
+                 so that users of assistive devices receive sufficient context  */}
+                <MessageBox announcement={announcement} enableSmartScroll ref={messageBoxRef}>
+                  <ChatbotWelcomePrompt
+                    title="Hi, ChatBot User!"
+                    description="How can I help you today?"
+                    prompts={welcomePrompts}
+                  />
+                  {messages.map((message, index) => {
+                    if (index === messages.length - 1) {
+                      return (
+                        <>
+                          <Message key={message.id} {...message} />
+                        </>
+                      );
+                    }
+                    return <Message key={message.id} {...message} />;
+                  })}
+                </MessageBox>
+              </ChatbotContent>
+              <ChatbotFooter>
+                <MessageBar
+                  onSendMessage={handleSend}
+                  hasMicrophoneButton
+                  isSendButtonDisabled={isSendButtonDisabled}
+                />
+                <ChatbotFootnote {...footnoteProps} />
+              </ChatbotFooter>
+            </>
+          }
+        ></ChatbotConversationHistoryNav>
+      </Chatbot>
+    </>
+  );
+};
diff --git a/packages/module/patternfly-docs/generated/patternfly-ai/chatbot/messages/demo/message-auto-scrolling.png b/packages/module/patternfly-docs/generated/patternfly-ai/chatbot/messages/demo/message-auto-scrolling.png
new file mode 100644
index 000000000..960105de2
Binary files /dev/null and b/packages/module/patternfly-docs/generated/patternfly-ai/chatbot/messages/demo/message-auto-scrolling.png differ
diff --git a/packages/module/src/MessageBox/MessageBox.test.tsx b/packages/module/src/MessageBox/MessageBox.test.tsx
index 5eded4934..6d4374274 100644
--- a/packages/module/src/MessageBox/MessageBox.test.tsx
+++ b/packages/module/src/MessageBox/MessageBox.test.tsx
@@ -1,9 +1,20 @@
 import { createRef } from 'react';
-import { render, screen, waitFor } from '@testing-library/react';
-import { MessageBox } from './MessageBox';
+import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { MessageBox, MessageBoxHandle } from './MessageBox';
 import userEvent from '@testing-library/user-event';
 
 describe('MessageBox', () => {
+  beforeEach(() => {
+    jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
+      cb(0); // Immediately call the callback
+      return 0;
+    });
+  });
+
+  afterEach(() => {
+    jest.restoreAllMocks();
+  });
+
   it('should render Message box', () => {
     render(
       <MessageBox>
@@ -14,17 +25,25 @@ describe('MessageBox', () => {
   });
 
   it('should assign ref to Message box', () => {
-    const ref = createRef<HTMLDivElement>();
+    const ref = createRef<MessageBoxHandle>();
     render(
-      <MessageBox ref={ref}>
+      <MessageBox data-testid="message-box" ref={ref}>
         <div>Test message content</div>
       </MessageBox>
     );
 
+    screen.getByText('Test message content');
+
     expect(ref.current).not.toBeNull();
+    // should contain custom methods exposed by the ref
+    expect(typeof ref.current?.isSmartScrollActive).toBe('function');
+    expect(typeof ref.current?.scrollToTop).toBe('function');
+    expect(typeof ref.current?.scrollToBottom).toBe('function');
+    expect(ref.current?.isSmartScrollActive()).toBe(false);
+
     expect(ref.current).toBeInstanceOf(HTMLDivElement);
   });
-  it('should call onScrollToBottomClick when scroll to top button is clicked', async () => {
+  it('should call onScrollToBottomClick when scroll to bottom button is clicked', async () => {
     const spy = jest.fn();
     render(
       <MessageBox onScrollToBottomClick={spy}>
@@ -37,7 +56,9 @@ describe('MessageBox', () => {
     Object.defineProperty(region, 'scrollHeight', { configurable: true, value: 1000 });
     Object.defineProperty(region, 'clientHeight', { configurable: true, value: 500 });
     Object.defineProperty(region, 'scrollTop', { configurable: true, value: 0 });
-    region.dispatchEvent(new Event('scroll'));
+    act(() => {
+      region.dispatchEvent(new Event('scroll'));
+    });
 
     await waitFor(() => {
       userEvent.click(screen.getByRole('button', { name: /Jump bottom/i }));
@@ -60,11 +81,236 @@ describe('MessageBox', () => {
       configurable: true,
       value: 500
     });
-    region.dispatchEvent(new Event('scroll'));
-
+    act(() => {
+      region.dispatchEvent(new Event('scroll'));
+    });
     await waitFor(() => {
       userEvent.click(screen.getByRole('button', { name: /Jump top/i }));
       expect(spy).toHaveBeenCalled();
     });
   });
+
+  it('should call user defined onWheel, onTouchMove and onTouchEnd handlers', async () => {
+    const ref = createRef<MessageBoxHandle>();
+    const onWheel = jest.fn();
+    const onTouchMove = jest.fn();
+    const onTouchEnd = jest.fn();
+
+    render(
+      <MessageBox ref={ref} enableSmartScroll onWheel={onWheel} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd}>
+        <div>Test message content</div>
+      </MessageBox>
+    );
+
+    const element = ref.current!;
+    act(() => {
+      fireEvent.wheel(element, { deltaY: 10 });
+      fireEvent.touchMove(element, { touches: [{ clientY: 700 }] });
+      fireEvent.touchEnd(element);
+    });
+
+    expect(onWheel).toHaveBeenCalled();
+    expect(onTouchMove).toHaveBeenCalled();
+    expect(onTouchEnd).toHaveBeenCalled();
+  });
+
+  it('should scroll to the bottom when the method is called ', async () => {
+    const ref = createRef<MessageBoxHandle>();
+
+    render(
+      <MessageBox ref={ref} enableSmartScroll>
+        <div>Test message content</div>
+      </MessageBox>
+    );
+    const element = ref.current!;
+    const scrollSpy = jest.spyOn(element, 'scrollTo');
+
+    act(() => {
+      ref.current?.scrollToBottom();
+      ref.current?.scrollToBottom();
+      ref.current?.scrollToBottom();
+    });
+
+    expect(scrollSpy).toHaveBeenCalledWith({ top: element.scrollHeight, behavior: 'smooth' });
+    expect(ref.current?.isSmartScrollActive()).toBe(true);
+  });
+
+  it('should scroll to the top when the method is called ', async () => {
+    const ref = createRef<MessageBoxHandle>();
+
+    render(
+      <MessageBox ref={ref} enableSmartScroll>
+        <div>Test message content</div>
+      </MessageBox>
+    );
+    const element = ref.current!;
+    const scrollSpy = jest.spyOn(element, 'scrollTo');
+
+    act(() => {
+      ref.current?.scrollToTop();
+    });
+
+    expect(scrollSpy).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
+    expect(ref.current?.isSmartScrollActive()).toBe(false);
+  });
+
+  it('should pause automatic scrolling when user scrolls up ', async () => {
+    const ref = createRef<MessageBoxHandle>();
+
+    render(
+      <MessageBox ref={ref} enableSmartScroll>
+        <div>Test message content</div>
+      </MessageBox>
+    );
+
+    expect(ref.current?.isSmartScrollActive()).toBe(true);
+
+    const element = ref.current!;
+
+    // Manually set scrollHeight and clientHeight for calculations
+    Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
+    Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
+
+    // Simulate scroll up by changing scrollTop
+    element.scrollTop = 200;
+    const scrollEvent = new Event('scroll', { bubbles: true });
+
+    act(() => {
+      element.dispatchEvent(scrollEvent);
+    });
+
+    expect(ref.current?.isSmartScrollActive()).toBe(false);
+  });
+
+  it('should resume automatic scrolling when user scrolls down to the bottom using scroll event', async () => {
+    jest.useFakeTimers();
+
+    const ref = createRef<MessageBoxHandle>();
+    render(
+      <MessageBox ref={ref} enableSmartScroll>
+        <div>Test message content</div>
+      </MessageBox>
+    );
+
+    const element = ref.current!;
+    // Manually set scrollHeight and clientHeight for calculations
+    Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
+    Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
+
+    // Simulate scroll up by changing scrollTop
+    element.scrollTop = 100;
+    const scrollEvent = new Event('scroll', { bubbles: true });
+
+    act(() => {
+      element.dispatchEvent(scrollEvent);
+    });
+
+    expect(ref.current?.isSmartScrollActive()).toBe(false);
+
+    act(() => {
+      // Simulate scroll down by changing scrollTop
+      element.scrollTop = 650; // scrollHeight - scrollTop - clientHeight - DELTA_DOWN (50) = 0
+      const scrollEvent = new Event('scroll', { bubbles: true });
+      element.dispatchEvent(scrollEvent);
+      jest.advanceTimersByTime(250);
+    });
+
+    expect(ref.current?.isSmartScrollActive()).toBe(true);
+    jest.useRealTimers();
+  });
+
+  it('should resume automatic scrolling when scrollToBottom method is used', async () => {
+    const ref = createRef<MessageBoxHandle>();
+    render(
+      <MessageBox ref={ref} enableSmartScroll>
+        <div>Test message content</div>
+      </MessageBox>
+    );
+
+    const element = ref.current!;
+    // Manually set scrollHeight and clientHeight for calculations
+    Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
+    Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
+
+    // Simulate scroll up by changing scrollTop
+    element.scrollTop = 100;
+    const scrollEvent = new Event('scroll', { bubbles: true });
+
+    act(() => {
+      element.dispatchEvent(scrollEvent);
+    });
+
+    expect(ref.current?.isSmartScrollActive()).toBe(false);
+
+    act(() => {
+      ref.current?.scrollToBottom({ resumeSmartScroll: true, behavior: 'auto' }); // resumes auto scroll and scrolls to bottom.
+    });
+
+    expect(ref.current?.isSmartScrollActive()).toBe(true);
+  });
+
+  it('should resume automatic scrolling when mouse wheel event is used', async () => {
+    const ref = createRef<MessageBoxHandle>();
+    render(
+      <MessageBox ref={ref} enableSmartScroll>
+        <div>Test message content</div>
+      </MessageBox>
+    );
+
+    const element = ref.current!;
+    // Manually set scrollHeight and clientHeight for calculations
+    Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
+    Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
+    Object.defineProperty(element, 'scrollTop', { configurable: true, value: 350 });
+
+    const scrollEvent = new Event('scroll', { bubbles: true });
+
+    act(() => {
+      element.dispatchEvent(scrollEvent);
+    });
+
+    expect(ref.current?.isSmartScrollActive()).toBe(false);
+
+    // Simulate mouse wheel event
+    act(() => {
+      Object.defineProperty(element, 'scrollTop', { configurable: true, value: 650 });
+      fireEvent.wheel(element, { deltaY: 10 });
+    });
+
+    expect(ref.current?.isSmartScrollActive()).toBe(true);
+  });
+
+  it('should resume automatic scrolling when user swipes up in touch screen', async () => {
+    const ref = createRef<MessageBoxHandle>();
+    render(
+      <MessageBox ref={ref} enableSmartScroll>
+        <div>Test message content</div>
+      </MessageBox>
+    );
+
+    const element = ref.current!;
+    // Manually set scrollHeight and clientHeight for calculations
+    Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
+    Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
+    Object.defineProperty(element, 'scrollTop', { configurable: true, value: 350 });
+
+    const scrollEvent = new Event('scroll', { bubbles: true });
+
+    act(() => {
+      element.dispatchEvent(scrollEvent);
+    });
+
+    expect(ref.current?.isSmartScrollActive()).toBe(false);
+
+    // Simulate touch event - swipe up
+    act(() => {
+      Object.defineProperty(element, 'scrollTop', { configurable: true, value: 650 });
+      fireEvent.touchStart(element, { touches: [{ clientY: 700 }] });
+      fireEvent.touchMove(element, { touches: [{ clientY: 700 }] });
+      fireEvent.touchMove(element, { touches: [{ clientY: 600 }] });
+      fireEvent.touchEnd(element);
+    });
+
+    expect(ref.current?.isSmartScrollActive()).toBe(true);
+  });
 });
diff --git a/packages/module/src/MessageBox/MessageBox.tsx b/packages/module/src/MessageBox/MessageBox.tsx
index 7ed34c25c..2a07eb893 100644
--- a/packages/module/src/MessageBox/MessageBox.tsx
+++ b/packages/module/src/MessageBox/MessageBox.tsx
@@ -1,9 +1,22 @@
 // ============================================================================
 // Chatbot Main - Messages
 // ============================================================================
-import type { HTMLProps, Ref, FunctionComponent } from 'react';
 
-import { useState, useRef, useCallback, useEffect, forwardRef } from 'react';
+import {
+  HTMLProps,
+  useState,
+  useRef,
+  useCallback,
+  useEffect,
+  forwardRef,
+  ForwardedRef,
+  useImperativeHandle,
+  ReactNode,
+  TouchEventHandler,
+  TouchEvent,
+  WheelEvent,
+  WheelEventHandler
+} from 'react';
 import JumpButton from './JumpButton';
 
 export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
@@ -12,10 +25,10 @@ export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
   /** Custom aria-label for scrollable portion of message box */
   ariaLabel?: string;
   /** Content to be displayed in the message box */
-  children: React.ReactNode;
+  children: ReactNode;
   /** Custom classname for the MessageBox component */
   className?: string;
-  /** Ref applied to message box */
+  /** @deprecated innerRef has been deprecated. Use ref instead. Ref applied to message box  */
   innerRef?: React.Ref<HTMLDivElement>;
   /** Modifier that controls how content in MessageBox is positioned within the container */
   position?: 'top' | 'bottom';
@@ -23,68 +36,192 @@ export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
   onScrollToTopClick?: () => void;
   /** Click handler for additional logic for when scroll to bottom jump button is clicked */
   onScrollToBottomClick?: () => void;
+  /** Flag to enable automatic scrolling when new messages are added */
+  enableSmartScroll?: boolean;
 }
 
-const MessageBoxBase: FunctionComponent<MessageBoxProps> = ({
-  announcement,
-  ariaLabel = 'Scrollable message log',
-  children,
-  innerRef,
-  className,
-  position = 'top',
-  onScrollToTopClick,
-  onScrollToBottomClick,
-  ...props
-}: MessageBoxProps) => {
-  const [atTop, setAtTop] = useState(false);
-  const [atBottom, setAtBottom] = useState(true);
-  const [isOverflowing, setIsOverflowing] = useState(false);
-  const defaultRef = useRef<HTMLDivElement>(null);
-  let messageBoxRef;
-  if (innerRef) {
-    messageBoxRef = innerRef;
-  } else {
-    messageBoxRef = defaultRef;
-  }
+export interface MessageBoxHandle extends HTMLDivElement {
+  /** Scrolls to the top of the message box */
+  scrollToTop: (options?: ScrollOptions) => void;
+  /** Scrolls to the bottom of the message box */
+  scrollToBottom: (options?: { resumeSmartScroll?: boolean } & ScrollOptions) => void;
+  /** Returns whether the smart scroll feature is currently active */
+  isSmartScrollActive: () => boolean;
+}
+
+export const MessageBox = forwardRef(
+  (
+    {
+      announcement,
+      ariaLabel = 'Scrollable message log',
+      children,
+      className,
+      position = 'top',
+      onScrollToTopClick,
+      onScrollToBottomClick,
+      enableSmartScroll = false,
+      ...props
+    }: MessageBoxProps,
+    ref: ForwardedRef<MessageBoxHandle | null>
+  ) => {
+    const [atTop, setAtTop] = useState(false);
+    const [atBottom, setAtBottom] = useState(true);
+    const [isOverflowing, setIsOverflowing] = useState(false);
+    const [autoScroll, setAutoScroll] = useState(true);
+    const lastScrollTop = useRef(0);
+    const animationFrame = useRef<any>(null);
+    const debounceTimeout = useRef<NodeJS.Timeout | null>(null);
+    const pauseAutoScrollRef = useRef(false);
+    const messageBoxRef = useRef<HTMLDivElement>(null);
+    const scrollQueued = useRef(false);
+    const resetUserScrollIntentTimeout = useRef<NodeJS.Timeout>();
+
+    // Configure handlers
+    const handleScroll = useCallback(() => {
+      const element = messageBoxRef.current;
+      if (!element) {
+        return;
+      }
 
-  // Configure handlers
-  const handleScroll = useCallback(() => {
-    const element = messageBoxRef.current;
-    if (element) {
       const { scrollTop, scrollHeight, clientHeight } = element;
-      setAtTop(scrollTop === 0);
-      setAtBottom(Math.round(scrollTop) + Math.round(clientHeight) >= Math.round(scrollHeight) - 1); // rounding means it could be within a pixel of the bottom
-    }
-  }, [messageBoxRef]);
-
-  const checkOverflow = useCallback(() => {
-    const element = messageBoxRef.current;
-    if (element) {
-      const { scrollHeight, clientHeight } = element;
-      setIsOverflowing(scrollHeight >= clientHeight);
-    }
-  }, [messageBoxRef]);
-
-  const scrollToTop = useCallback(() => {
-    const element = messageBoxRef.current;
-    if (element) {
-      element.scrollTo({ top: 0, behavior: 'smooth' });
-    }
-    onScrollToTopClick && onScrollToTopClick();
-  }, [messageBoxRef]);
-
-  const scrollToBottom = useCallback(() => {
-    const element = messageBoxRef.current;
-    if (element) {
-      element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
-    }
-    onScrollToBottomClick && onScrollToBottomClick();
-  }, [messageBoxRef]);
-
-  // Detect scroll position
-  useEffect(() => {
-    const element = messageBoxRef.current;
-    if (element) {
+
+      const roundedScrollTop = Math.round(scrollTop);
+      const roundedClientHeight = Math.round(clientHeight);
+      const roundedScrollHeight = Math.round(scrollHeight);
+
+      const distanceFromBottom = roundedScrollHeight - roundedScrollTop - roundedClientHeight;
+      const isScrollingDown = roundedScrollTop > lastScrollTop.current;
+
+      const DELTA_UP = 10;
+      const DELTA_DOWN = 50;
+      const DEBOUNCE_DELAY = 200;
+
+      const delta = isScrollingDown ? DELTA_DOWN : DELTA_UP;
+      const isAtBottom = distanceFromBottom <= delta;
+
+      setAtTop(roundedScrollTop === 0);
+      setAtBottom(roundedScrollTop + roundedClientHeight >= roundedScrollHeight - 1); // rounding means it could be within a pixel of the bottom
+
+      if (!enableSmartScroll || scrollQueued.current) {
+        return;
+      }
+
+      if (roundedScrollTop === 0) {
+        pauseAutoScrollRef.current = false;
+      }
+
+      if (debounceTimeout.current) {
+        clearTimeout(debounceTimeout.current);
+      }
+
+      if (!isAtBottom && !pauseAutoScrollRef.current) {
+        setAutoScroll(false);
+      }
+
+      // User is near bottom and scrolling down - debounce re-enabling auto-scroll
+      if (isAtBottom && isScrollingDown && !pauseAutoScrollRef.current) {
+        debounceTimeout.current = setTimeout(() => {
+          setAutoScroll(true);
+        }, DEBOUNCE_DELAY);
+      }
+
+      lastScrollTop.current = roundedScrollTop;
+    }, [messageBoxRef]);
+
+    const checkOverflow = useCallback(() => {
+      const element = messageBoxRef.current;
+      if (element) {
+        const { scrollHeight, clientHeight } = element;
+        setIsOverflowing(scrollHeight >= clientHeight);
+      }
+    }, [messageBoxRef]);
+
+    const resumeAutoScroll = useCallback(() => {
+      if (!enableSmartScroll) {
+        return;
+      }
+      pauseAutoScrollRef.current = false;
+      setAutoScroll(true);
+    }, [enableSmartScroll]);
+
+    const pauseAutoScroll = useCallback(() => {
+      if (!enableSmartScroll) {
+        return;
+      }
+      pauseAutoScrollRef.current = true;
+      setAutoScroll(false);
+    }, [enableSmartScroll]);
+    /**
+     * Scrolls to the top of the message box.
+     *
+     */
+    const scrollToTop = useCallback(
+      (options?: ScrollOptions) => {
+        const { behavior = 'smooth' } = options || {};
+
+        const element = messageBoxRef.current;
+
+        if (!element || scrollQueued.current) {
+          return;
+        }
+
+        scrollQueued.current = true;
+        pauseAutoScroll();
+
+        if (animationFrame.current) {
+          cancelAnimationFrame(animationFrame.current);
+          animationFrame.current = null;
+        }
+
+        animationFrame.current = requestAnimationFrame(() => {
+          element.scrollTo({ top: 0, behavior });
+          scrollQueued.current = false;
+        });
+        onScrollToTopClick && onScrollToTopClick();
+      },
+      [messageBoxRef]
+    );
+
+    /**
+     * Scrolls to the bottom of the message box.
+     *
+     * @param options.resumeSmartScroll - If true, resumes smart scroll behavior;
+     *                                    if false or omitted, scrolls without resuming auto-scroll.
+     * @param options.scrollOptions - Additional scroll options. behavior can be 'smooth' or 'auto'.
+     */
+    const scrollToBottom = useCallback(
+      (options?: { resumeSmartScroll?: boolean } & ScrollOptions) => {
+        const { behavior = 'smooth', resumeSmartScroll = false } = options || {};
+        resumeSmartScroll && resumeAutoScroll();
+
+        const element = messageBoxRef.current;
+        if (!element || pauseAutoScrollRef.current || scrollQueued.current) {
+          return;
+        }
+
+        scrollQueued.current = true;
+
+        if (animationFrame.current) {
+          cancelAnimationFrame(animationFrame.current);
+        }
+
+        animationFrame.current = requestAnimationFrame(() => {
+          element.scrollTo({ top: element.scrollHeight, behavior });
+          resumeAutoScroll();
+          scrollQueued.current = false;
+        });
+        onScrollToBottomClick && onScrollToBottomClick();
+      },
+      [messageBoxRef, enableSmartScroll]
+    );
+
+    // Detect scroll position
+    useEffect(() => {
+      const element = messageBoxRef.current;
+      if (!element) {
+        return;
+      }
+
       // Listen for scroll events
       element.addEventListener('scroll', handleScroll);
 
@@ -95,32 +232,102 @@ const MessageBoxBase: FunctionComponent<MessageBoxProps> = ({
       return () => {
         element.removeEventListener('scroll', handleScroll);
       };
-    }
-  }, [checkOverflow, handleScroll, messageBoxRef]);
-
-  return (
-    <>
-      <JumpButton position="top" isHidden={isOverflowing && atTop} onClick={scrollToTop} />
-      <div
-        role="region"
-        tabIndex={0}
-        aria-label={ariaLabel}
-        className={`pf-chatbot__messagebox ${position === 'bottom' && 'pf-chatbot__messagebox--bottom'} ${className ?? ''}`}
-        ref={innerRef ?? messageBoxRef}
-        {...props}
-      >
-        {children}
-        <div className="pf-chatbot__messagebox-announcement" aria-live="polite">
-          {announcement}
+    }, [checkOverflow, handleScroll, messageBoxRef]);
+
+    useImperativeHandle(ref, (): MessageBoxHandle => {
+      const node = messageBoxRef.current! as MessageBoxHandle;
+
+      // Attach custom methods to the element
+      node.scrollToTop = scrollToTop;
+      node.scrollToBottom = scrollToBottom;
+      node.isSmartScrollActive = () => enableSmartScroll && autoScroll;
+
+      return node;
+    });
+
+    let lastTouchY: number | null = null;
+
+    const onTouchEnd: TouchEventHandler<HTMLDivElement> = (event: TouchEvent<HTMLDivElement>) => {
+      lastTouchY = null;
+      props.onTouchEnd && props.onTouchEnd(event);
+    };
+
+    const handleUserScroll = (isScrollingDown: boolean) => {
+      const container = messageBoxRef.current;
+      if (!enableSmartScroll || !container) {
+        return;
+      }
+
+      if (!isScrollingDown) {
+        pauseAutoScrollRef.current = true;
+        clearTimeout(resetUserScrollIntentTimeout.current);
+        return;
+      }
+
+      const { scrollTop, scrollHeight, clientHeight } = container;
+      const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
+
+      if (distanceFromBottom < 100) {
+        pauseAutoScrollRef.current = false;
+        setAutoScroll(true);
+      }
+    };
+
+    const onWheel = (event: WheelEvent<HTMLDivElement>) => {
+      const isScrollingDown = event.deltaY > 0;
+      handleUserScroll(isScrollingDown);
+      props.onWheel && props.onWheel(event);
+    };
+
+    const onTouchMove = (event: TouchEvent<HTMLDivElement>) => {
+      const currentTouchY = event.touches[0].clientY;
+      let isScrollingDown = false;
+
+      if (lastTouchY !== null) {
+        isScrollingDown = currentTouchY < lastTouchY;
+      }
+
+      lastTouchY = currentTouchY;
+
+      handleUserScroll(isScrollingDown);
+      props.onTouchMove && props.onTouchMove(event);
+    };
+
+    const smartScrollHandlers: {
+      onWheel: WheelEventHandler<HTMLDivElement>;
+      onTouchMove: TouchEventHandler<HTMLDivElement>;
+      onTouchEnd: TouchEventHandler<HTMLDivElement>;
+    } = {
+      onWheel,
+      onTouchMove,
+      onTouchEnd
+    };
+
+    return (
+      <>
+        <JumpButton position="top" isHidden={isOverflowing && atTop} onClick={scrollToTop} />
+        <div
+          role="region"
+          tabIndex={0}
+          aria-label={ariaLabel}
+          className={`pf-chatbot__messagebox ${position === 'bottom' && 'pf-chatbot__messagebox--bottom'} ${className ?? ''}`}
+          ref={messageBoxRef}
+          {...props}
+          {...(enableSmartScroll ? { ...smartScrollHandlers } : {})}
+        >
+          {children}
+          <div className="pf-chatbot__messagebox-announcement" aria-live="polite">
+            {announcement}
+          </div>
         </div>
-      </div>
-      <JumpButton position="bottom" isHidden={isOverflowing && atBottom} onClick={scrollToBottom} />
-    </>
-  );
-};
-
-export const MessageBox = forwardRef((props: MessageBoxProps, ref: Ref<any>) => (
-  <MessageBoxBase innerRef={ref} {...props} />
-));
+        <JumpButton
+          position="bottom"
+          isHidden={isOverflowing && atBottom}
+          onClick={() => scrollToBottom({ resumeSmartScroll: true })}
+        />
+      </>
+    );
+  }
+);
 
 export default MessageBox;


Reviews:
user: login: edonehoo
body: A few small content suggestions for the docs, but looks pretty good!
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T14:46:40Z
user: login: rebeccaalpert
body: Overall this looks good to me. Thank you for the contribution. 
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-14T15:13:25Z
user: login: karthikjeeyar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T16:16:43Z
user: login: karthikjeeyar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T16:17:21Z
user: login: karthikjeeyar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T16:17:54Z
user: login: karthikjeeyar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T16:19:10Z
user: login: rebeccaalpert
body: Thank you for making those changes! I think other folks will be really excited to have this.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T15:37:56Z
user: login: edonehoo
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T16:12:34Z
user: login: thatblindgeye
body: I think this looks good. One question/concern I guess is whether we would want to include some documentation about allowing users to override the auto scroll? This may depend how it actually behaves as new messages are coming in, but I'm imagining if I'm at the bottom of the message window reading a message that entirely fits in the message area, but then a new, *long* message comes in, I may not want it to autoscroll just because I'm at the bottom.

Can be seen in the new demo; if I'm at the bottom and I type something, the bot message starts coming in and autoscrolls for me, but I would prefer to remain where I was in the scroll container so I can start reading the message from the beginning without having to manually scroll back to the start of it (could be more difficult/annoying for users interacting with anything other than a mouse as well).

cc @edonehoo @rebeccaalpert wdyt?
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T17:41:57Z
user: login: thatblindgeye
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-19T18:00:06Z

Review comments:
diff_hunk: @@ -62,6 +64,42 @@ It is also important to announce when new content appears onscreen for accessibi
 
 ```
 
+### Message auto-scrolling
+
+This demo displays automatic scrolling to the bottom of the MessageBox:
path: packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md
user: login: edonehoo
body: ```suggestion
This demo shows auto-scrolling functionality, which automatically scrolls to the bottom of the active chat.
```
created_at: 2025-05-14T14:40:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +64,42 @@ It is also important to announce when new content appears onscreen for accessibi
 
 ```
 
+### Message auto-scrolling
+
+This demo displays automatic scrolling to the bottom of the MessageBox:
+
+1. [`<MessageBox>`](/patternfly-ai/chatbot/ui#message-box) component provides an optional smart scroll behavior via the `enableSmartScroll` prop.
path: packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md
user: login: edonehoo
body: ```suggestion

To enable auto-scroll behavior pass the `enableSmartScroll` prop to the [`<MessageBox>`](/patternfly-ai/chatbot/ui#message-box) component.
```
created_at: 2025-05-14T14:42:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +64,42 @@ It is also important to announce when new content appears onscreen for accessibi
 
 ```
 
+### Message auto-scrolling
+
+This demo displays automatic scrolling to the bottom of the MessageBox:
+
+1. [`<MessageBox>`](/patternfly-ai/chatbot/ui#message-box) component provides an optional smart scroll behavior via the `enableSmartScroll` prop.
+
+#### `enableSmartScroll` (optional)
path: packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md
user: login: edonehoo
body: ```suggestion
```
created_at: 2025-05-14T14:43:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +64,42 @@ It is also important to announce when new content appears onscreen for accessibi
 
 ```
 
+### Message auto-scrolling
+
+This demo displays automatic scrolling to the bottom of the MessageBox:
+
+1. [`<MessageBox>`](/patternfly-ai/chatbot/ui#message-box) component provides an optional smart scroll behavior via the `enableSmartScroll` prop.
+
+#### `enableSmartScroll` (optional)
+
+When enabled:
+
+- Scroll position is automatically managed based on user interaction.
+- Scrolling is _not_ forced to the bottom on new messages unless explicitly triggered via the `scrollToBottom()` method.
path: packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md
user: login: edonehoo
body: ```suggestion
- Scrolling is _not_ forced to the bottom when new messages arrive, unless explicitly triggered via the `scrollToBottom()` method.
```
created_at: 2025-05-14T14:43:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +64,42 @@ It is also important to announce when new content appears onscreen for accessibi
 
 ```
 
+### Message auto-scrolling
+
+This demo displays automatic scrolling to the bottom of the MessageBox:
+
+1. [`<MessageBox>`](/patternfly-ai/chatbot/ui#message-box) component provides an optional smart scroll behavior via the `enableSmartScroll` prop.
+
+#### `enableSmartScroll` (optional)
+
+When enabled:
+
+- Scroll position is automatically managed based on user interaction.
+- Scrolling is _not_ forced to the bottom on new messages unless explicitly triggered via the `scrollToBottom()` method.
+- If the user scrolls up or interacts with UI controls like "Back to top" or "Back to bottom", the component pauses auto-scroll to respect user intent.
+- Auto-scroll resumes only when the user scrolls back down manually or programmatically via the `scrollToBottom({resumeSmartScroll: true})` method.
+
+#### Imperative Methods via `ref`
+
+When using `ref`, the component exposes the following methods:
+
+- `scrollToBottom()` – Scrolls to the bottom of the message container.
+- `scrollToTop()` – Scrolls to the top of the message container.
+- `isSmartScrollActive()` – Returns `true` if smart auto-scroll is currently active.
+- Native `HTMLDivElement` methods like `scrollTo()` are also available.
+
+This demo includes broader ChatBot features, including:
+
+1. A [`<ChatbotToggle>`](/patternfly-ai/chatbot/ui#toggle) that controls the [`<Chatbot>`](/patternfly-ai/chatbot/ui#container) container.
+2. A `<ChatbotContent>` and [`<MessageBox>`](/patternfly-ai/chatbot/ui#content-and-message-box) with:
+   - A `<ChatbotWelcomePrompt>`
+   - An initial user message and initial bot message
+3. A [`<ChatbotFooter>`](/patternfly-ai/chatbot/ui#footer) with a [`<ChatbotFootnote>`](/patternfly-ai/chatbot/ui#footnote-with-popover) and a `<MessageBar>`
path: packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md
user: login: edonehoo
body: ```suggestion
#### Imperative methods via `ref`

When using `ref`, the `<MessageBox>` component exposes the following methods:

- `scrollToBottom()`: Scrolls to the bottom of the message container.
- `scrollToTop()`: Scrolls to the top of the message container.
- `isSmartScrollActive()`: Returns `true` if smart auto-scroll is currently active.
- Native `HTMLDivElement` methods like `scrollTo()`.

This demo includes broader ChatBot features, including:

1. A [`<ChatbotToggle>`](/patternfly-ai/chatbot/ui#toggle) that controls the [`<Chatbot>`](/patternfly-ai/chatbot/ui#container) container.
2. A `<ChatbotContent>` and [`<MessageBox>`](/patternfly-ai/chatbot/ui#content-and-message-box) with:
   - A `<ChatbotWelcomePrompt>`
   - An initial user message and initial bot message
3. A [`<ChatbotFooter>`](/patternfly-ai/chatbot/ui#footer) with a [`<ChatbotFootnote>`](/patternfly-ai/chatbot/ui#footnote-with-popover) and a `<MessageBar>`
```
created_at: 2025-05-14T14:46:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,550 @@
+import React from 'react';
+
+import { Bullseye, Brand, DropdownList, DropdownItem, DropdownGroup, SkipToContent } from '@patternfly/react-core';
+
+import ChatbotToggle from '@patternfly/chatbot/dist/dynamic/ChatbotToggle';
+import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
+import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent';
+import ChatbotWelcomePrompt from '@patternfly/chatbot/dist/dynamic/ChatbotWelcomePrompt';
+import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter';
+import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar';
+import MessageBox, { MessageBoxHandle } from '@patternfly/chatbot/dist/dynamic/MessageBox';
+import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message';
+import ChatbotConversationHistoryNav, {
+  Conversation
+} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
+import ChatbotHeader, {
+  ChatbotHeaderMenu,
+  ChatbotHeaderMain,
+  ChatbotHeaderTitle,
+  ChatbotHeaderActions,
+  ChatbotHeaderSelectorDropdown,
+  ChatbotHeaderOptionsDropdown
+} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader';
+
+import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
+import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon';
+import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon';
+
+import PFHorizontalLogoColor from '../UI/PF-HorizontalLogo-Color.svg';
+import PFHorizontalLogoReverse from '../UI/PF-HorizontalLogo-Reverse.svg';
+import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg';
+import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg';
+import userAvatar from '../Messages/user_avatar.svg';
+import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
+import { getTrackingProviders } from '@patternfly/chatbot/dist/dynamic/tracking';
path: packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotScrolling.tsx
user: login: rebeccaalpert
body: I don't think we need any of the tracking stuff here - I would love if we could drop it from this demo.
created_at: 2025-05-14T14:51:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,550 @@
+import React from 'react';
+
+import { Bullseye, Brand, DropdownList, DropdownItem, DropdownGroup, SkipToContent } from '@patternfly/react-core';
+
+import ChatbotToggle from '@patternfly/chatbot/dist/dynamic/ChatbotToggle';
+import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
+import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent';
+import ChatbotWelcomePrompt from '@patternfly/chatbot/dist/dynamic/ChatbotWelcomePrompt';
+import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter';
+import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar';
+import MessageBox, { MessageBoxHandle } from '@patternfly/chatbot/dist/dynamic/MessageBox';
+import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message';
+import ChatbotConversationHistoryNav, {
+  Conversation
+} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
+import ChatbotHeader, {
+  ChatbotHeaderMenu,
+  ChatbotHeaderMain,
+  ChatbotHeaderTitle,
+  ChatbotHeaderActions,
+  ChatbotHeaderSelectorDropdown,
+  ChatbotHeaderOptionsDropdown
+} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader';
+
+import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
+import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon';
+import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon';
+
+import PFHorizontalLogoColor from '../UI/PF-HorizontalLogo-Color.svg';
+import PFHorizontalLogoReverse from '../UI/PF-HorizontalLogo-Reverse.svg';
+import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg';
+import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg';
+import userAvatar from '../Messages/user_avatar.svg';
+import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
+import { getTrackingProviders } from '@patternfly/chatbot/dist/dynamic/tracking';
+import { InitProps } from '@patternfly/chatbot/dist/dynamic/tracking';
+import '@patternfly/react-core/dist/styles/base.css';
+import '@patternfly/chatbot/dist/css/main.css';
+
+const footnoteProps = {
+  label: 'ChatBot uses AI. Check for mistakes.',
+  popover: {
+    title: 'Verify information',
+    description: `While ChatBot strives for accuracy, AI is experimental and can make mistakes. We cannot guarantee that all information provided by ChatBot is up to date or without error. You should always verify responses using reliable sources, especially for crucial information and decision making.`,
+    bannerImage: {
+      src: 'https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif',
+      alt: 'Example image for footnote popover'
+    },
+    cta: {
+      label: 'Dismiss',
+      onClick: () => {
+        alert('Do something!');
+      }
+    },
+    link: {
+      label: 'View AI policy',
+      url: 'https://www.redhat.com/'
+    }
+  }
+};
+
+const markdown = `A paragraph with *emphasis* and **strong importance**.
+
+> A block quote with ~strikethrough~ and a URL: https://reactjs.org.
+
+Here is an inline code - \`() => void\`
+
+Here is some YAML code:
+
+~~~yaml
+apiVersion: helm.openshift.io/v1beta1/
+kind: HelmChartRepository
+metadata:
+  name: azure-sample-repo0oooo00ooo
+spec:
+  connectionConfig:
+  url: https://raw.githubusercontent.com/Azure-Samples/helm-charts/master/docs
+~~~
+
+Here is some JavaScript code:
+
+~~~js
+import React from 'react';
+
+const MessageLoading = () => (
+  <div className="pf-chatbot__message-loading">
+    <span className="pf-chatbot__message-loading-dots">
+      <span className="pf-v6-screen-reader">Loading message</span>
+    </span>
+  </div>
+);
+
+export default MessageLoading;
+
+~~~
+`;
+
+// It's important to set a date and timestamp prop since the Message components re-render.
+// The timestamps re-render with them.
+const date = new Date();
+
+const initProps: InitProps = {
+  verbose: false,
+  segmentKey: 'TODO-key', // TODO add your key here
+  posthogKey: 'TODO-key',
+  umamiKey: 'TODO-key',
+  umamiHostUrl: 'http://localhost:3000', // TODO where is your Umami installation?
+  console: true,
+  something: 'test'
+};
+
+const tracking = getTrackingProviders(initProps);
+tracking.identify('user-123', { superUser: true }); // TODO get real user id + properties
+tracking.trackPageView(window.location.href);
+
+const actionEventName = 'MessageAction';
+const initialMessages: MessageProps[] = [
+  {
+    id: '1',
+    role: 'user',
+    content: 'Hello, can you give me an example of what you can do?',
+    name: 'User',
+    avatar: userAvatar,
+    timestamp: date.toLocaleString(),
+    avatarProps: { isBordered: true }
+  },
+  {
+    id: '2',
+    role: 'bot',
+    content: markdown,
+    name: 'Bot',
+    avatar: patternflyAvatar,
+    timestamp: date.toLocaleString(),
+    actions: {
+      positive: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Good response' }) },
+      negative: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Bad response' }) },
+      copy: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Copy' }) },
+      share: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Share' }) },
+      listen: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Listen' }) }
+    }
+  }
+];
+
+const welcomePrompts = [
+  {
+    title: 'Set up account',
+    message: 'Choose the necessary settings and preferences for your account.'
+  },
+  {
+    title: 'Troubleshoot issue',
+    message: 'Find documentation and instructions to resolve your issue.'
+  }
+];
+
+const initialConversations = {
+  Today: [{ id: '1', text: 'Hello, can you give me an example of what you can do?' }],
+  'This month': [
+    {
+      id: '2',
+      text: 'Enterprise Linux installation and setup'
+    },
+    { id: '3', text: 'Troubleshoot system crash' }
+  ],
+  March: [
+    { id: '4', text: 'Ansible security and updates' },
+    { id: '5', text: 'Red Hat certification' },
+    { id: '6', text: 'Lightspeed user documentation' }
+  ],
+  February: [
+    { id: '7', text: 'Crashing pod assistance' },
+    { id: '8', text: 'OpenShift AI pipelines' },
+    { id: '9', text: 'Updating subscription plan' },
+    { id: '10', text: 'Red Hat licensing options' }
+  ],
+  January: [
+    { id: '11', text: 'RHEL system performance' },
+    { id: '12', text: 'Manage user accounts' }
+  ]
+};
+
+const actionEvent2 = 'ActionEvent2';
+export const ChatbotScrollingDemo: React.FunctionComponent = () => {
+  const [chatbotVisible, setChatbotVisible] = React.useState<boolean>(true);
+  const [displayMode, setDisplayMode] = React.useState<ChatbotDisplayMode>(ChatbotDisplayMode.default);
+  const [messages, setMessages] = React.useState<MessageProps[]>(initialMessages);
+  const [selectedModel, setSelectedModel] = React.useState('Granite 7B');
+  const [isSendButtonDisabled, setIsSendButtonDisabled] = React.useState(false);
+  const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);
+  const [conversations, setConversations] = React.useState<Conversation[] | { [key: string]: Conversation[] }>(
+    initialConversations
+  );
+  const [announcement, setAnnouncement] = React.useState<string>();
+  const toggleRef = React.useRef<HTMLButtonElement>(null);
+  const chatbotRef = React.useRef<HTMLDivElement>(null);
+  const historyRef = React.useRef<HTMLButtonElement>(null);
+  const messageBoxRef = React.useRef<MessageBoxHandle>(null); // ref to MessageBox will have isSmartScrollActive, scrollToTop and scrollToBottom
+  const scrollQueued = React.useRef(false);
+
+  // Auto-scrolls to the latest message
+  React.useLayoutEffect(() => {
+    if (!messageBoxRef.current?.isSmartScrollActive() || scrollQueued.current) {
+      return undefined;
+    }
+
+    let rafId = 0;
+    // don't scroll the first load - in this demo, we know we start with two messages
path: packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotScrolling.tsx
user: login: rebeccaalpert
body: Even though we have this comment here, it appears that it does scroll on the first load. I don't think we want this behavior. The actual scroll works great otherwise.
created_at: 2025-05-14T14:52:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,550 @@
+import React from 'react';
+
+import { Bullseye, Brand, DropdownList, DropdownItem, DropdownGroup, SkipToContent } from '@patternfly/react-core';
+
+import ChatbotToggle from '@patternfly/chatbot/dist/dynamic/ChatbotToggle';
+import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
+import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent';
+import ChatbotWelcomePrompt from '@patternfly/chatbot/dist/dynamic/ChatbotWelcomePrompt';
+import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter';
+import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar';
+import MessageBox, { MessageBoxHandle } from '@patternfly/chatbot/dist/dynamic/MessageBox';
+import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message';
+import ChatbotConversationHistoryNav, {
+  Conversation
+} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
+import ChatbotHeader, {
+  ChatbotHeaderMenu,
+  ChatbotHeaderMain,
+  ChatbotHeaderTitle,
+  ChatbotHeaderActions,
+  ChatbotHeaderSelectorDropdown,
+  ChatbotHeaderOptionsDropdown
+} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader';
+
+import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
+import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon';
+import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon';
+
+import PFHorizontalLogoColor from '../UI/PF-HorizontalLogo-Color.svg';
+import PFHorizontalLogoReverse from '../UI/PF-HorizontalLogo-Reverse.svg';
+import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg';
+import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg';
+import userAvatar from '../Messages/user_avatar.svg';
+import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
+import { getTrackingProviders } from '@patternfly/chatbot/dist/dynamic/tracking';
+import { InitProps } from '@patternfly/chatbot/dist/dynamic/tracking';
+import '@patternfly/react-core/dist/styles/base.css';
+import '@patternfly/chatbot/dist/css/main.css';
+
+const footnoteProps = {
+  label: 'ChatBot uses AI. Check for mistakes.',
+  popover: {
+    title: 'Verify information',
+    description: `While ChatBot strives for accuracy, AI is experimental and can make mistakes. We cannot guarantee that all information provided by ChatBot is up to date or without error. You should always verify responses using reliable sources, especially for crucial information and decision making.`,
+    bannerImage: {
+      src: 'https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif',
+      alt: 'Example image for footnote popover'
+    },
+    cta: {
+      label: 'Dismiss',
+      onClick: () => {
+        alert('Do something!');
+      }
+    },
+    link: {
+      label: 'View AI policy',
+      url: 'https://www.redhat.com/'
+    }
+  }
+};
+
+const markdown = `A paragraph with *emphasis* and **strong importance**.
+
+> A block quote with ~strikethrough~ and a URL: https://reactjs.org.
+
+Here is an inline code - \`() => void\`
+
+Here is some YAML code:
+
+~~~yaml
+apiVersion: helm.openshift.io/v1beta1/
+kind: HelmChartRepository
+metadata:
+  name: azure-sample-repo0oooo00ooo
+spec:
+  connectionConfig:
+  url: https://raw.githubusercontent.com/Azure-Samples/helm-charts/master/docs
+~~~
+
+Here is some JavaScript code:
+
+~~~js
+import React from 'react';
+
+const MessageLoading = () => (
+  <div className="pf-chatbot__message-loading">
+    <span className="pf-chatbot__message-loading-dots">
+      <span className="pf-v6-screen-reader">Loading message</span>
+    </span>
+  </div>
+);
+
+export default MessageLoading;
+
+~~~
+`;
+
+// It's important to set a date and timestamp prop since the Message components re-render.
+// The timestamps re-render with them.
+const date = new Date();
+
+const initProps: InitProps = {
+  verbose: false,
+  segmentKey: 'TODO-key', // TODO add your key here
+  posthogKey: 'TODO-key',
+  umamiKey: 'TODO-key',
+  umamiHostUrl: 'http://localhost:3000', // TODO where is your Umami installation?
+  console: true,
+  something: 'test'
+};
+
+const tracking = getTrackingProviders(initProps);
+tracking.identify('user-123', { superUser: true }); // TODO get real user id + properties
+tracking.trackPageView(window.location.href);
+
+const actionEventName = 'MessageAction';
+const initialMessages: MessageProps[] = [
+  {
+    id: '1',
+    role: 'user',
+    content: 'Hello, can you give me an example of what you can do?',
+    name: 'User',
+    avatar: userAvatar,
+    timestamp: date.toLocaleString(),
+    avatarProps: { isBordered: true }
+  },
+  {
+    id: '2',
+    role: 'bot',
+    content: markdown,
+    name: 'Bot',
+    avatar: patternflyAvatar,
+    timestamp: date.toLocaleString(),
+    actions: {
+      positive: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Good response' }) },
+      negative: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Bad response' }) },
+      copy: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Copy' }) },
+      share: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Share' }) },
+      listen: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Listen' }) }
+    }
+  }
+];
+
+const welcomePrompts = [
+  {
+    title: 'Set up account',
+    message: 'Choose the necessary settings and preferences for your account.'
+  },
+  {
+    title: 'Troubleshoot issue',
+    message: 'Find documentation and instructions to resolve your issue.'
+  }
+];
+
+const initialConversations = {
+  Today: [{ id: '1', text: 'Hello, can you give me an example of what you can do?' }],
+  'This month': [
+    {
+      id: '2',
+      text: 'Enterprise Linux installation and setup'
+    },
+    { id: '3', text: 'Troubleshoot system crash' }
+  ],
+  March: [
+    { id: '4', text: 'Ansible security and updates' },
+    { id: '5', text: 'Red Hat certification' },
+    { id: '6', text: 'Lightspeed user documentation' }
+  ],
+  February: [
+    { id: '7', text: 'Crashing pod assistance' },
+    { id: '8', text: 'OpenShift AI pipelines' },
+    { id: '9', text: 'Updating subscription plan' },
+    { id: '10', text: 'Red Hat licensing options' }
+  ],
+  January: [
+    { id: '11', text: 'RHEL system performance' },
+    { id: '12', text: 'Manage user accounts' }
+  ]
+};
+
+const actionEvent2 = 'ActionEvent2';
+export const ChatbotScrollingDemo: React.FunctionComponent = () => {
+  const [chatbotVisible, setChatbotVisible] = React.useState<boolean>(true);
+  const [displayMode, setDisplayMode] = React.useState<ChatbotDisplayMode>(ChatbotDisplayMode.default);
+  const [messages, setMessages] = React.useState<MessageProps[]>(initialMessages);
+  const [selectedModel, setSelectedModel] = React.useState('Granite 7B');
+  const [isSendButtonDisabled, setIsSendButtonDisabled] = React.useState(false);
+  const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);
+  const [conversations, setConversations] = React.useState<Conversation[] | { [key: string]: Conversation[] }>(
+    initialConversations
+  );
+  const [announcement, setAnnouncement] = React.useState<string>();
+  const toggleRef = React.useRef<HTMLButtonElement>(null);
+  const chatbotRef = React.useRef<HTMLDivElement>(null);
+  const historyRef = React.useRef<HTMLButtonElement>(null);
+  const messageBoxRef = React.useRef<MessageBoxHandle>(null); // ref to MessageBox will have isSmartScrollActive, scrollToTop and scrollToBottom
+  const scrollQueued = React.useRef(false);
+
+  // Auto-scrolls to the latest message
+  React.useLayoutEffect(() => {
+    if (!messageBoxRef.current?.isSmartScrollActive() || scrollQueued.current) {
+      return undefined;
+    }
+
+    let rafId = 0;
+    // don't scroll the first load - in this demo, we know we start with two messages
+    scrollQueued.current = true;
+
+    rafId = requestAnimationFrame(() => {
+      messageBoxRef.current?.scrollToBottom();
+      scrollQueued.current = false;
+    });
+
+    return () => {
+      cancelAnimationFrame(rafId);
+      scrollQueued.current = false;
+    };
+  }, [messages, messageBoxRef]);
+
+  const onSelectModel = (
+    _event: React.MouseEvent<Element, MouseEvent> | undefined,
+    value: string | number | undefined
+  ) => {
+    setSelectedModel(value as string);
+    tracking.trackSingleItem('ModelSelected', { model: value });
+  };
+
+  const onSelectDisplayMode = (
+    _event: React.MouseEvent<Element, MouseEvent> | undefined,
+    value: string | number | undefined
+  ) => {
+    setDisplayMode(value as ChatbotDisplayMode);
+  };
+
+  // you will likely want to come up with your own unique id function; this is for demo purposes only
+  const generateId = () => {
+    const id = Date.now() + Math.random();
+    return id.toString();
+  };
+
+  const handleSend = (message) => {
+    setIsSendButtonDisabled(true);
+    tracking.trackSingleItem('UserInputReceived', { text: message });
+    const newMessages: MessageProps[] = [];
+    // We can't use structuredClone since messages contains functions, but we can't mutate
+    // items that are going into state or the UI won't update correctly
+    messages.forEach((message) => newMessages.push(message));
+    // It's important to set a timestamp prop since the Message components re-render.
+    // The timestamps re-render with them.
+    const date = new Date();
+    newMessages.push({
+      id: generateId(),
+      role: 'user',
+      content: message,
+      name: 'User',
+      avatar: userAvatar,
+      timestamp: date.toLocaleString(),
+      avatarProps: { isBordered: true }
+    });
+    newMessages.push({
+      id: generateId(),
+      role: 'bot',
+      content: 'API response goes here',
+      name: 'Bot',
+      isLoading: true,
+      avatar: patternflyAvatar,
+      timestamp: date.toLocaleString()
+    });
+    setMessages(newMessages);
+    messageBoxRef.current?.scrollToBottom();
+    // make announcement to assistive devices that new messages have been added
+    setAnnouncement(`Message from User: ${message}. Message from Bot is loading.`);
+
+    // this is for demo purposes only; in a real situation, there would be an API response we would wait for
+    setTimeout(() => {
+      const loadedMessages: MessageProps[] = [];
+      // We can't use structuredClone since messages contains functions, but we can't mutate
+      // items that are going into state or the UI won't update correctly
+      newMessages.forEach((message) => loadedMessages.push(message));
+      const messageIndex = loadedMessages.length - 1;
+      loadedMessages.pop();
+      loadedMessages.push({
+        id: generateId(),
+        role: 'bot',
+        content: 'API response goes here.',
+        name: 'Bot',
+        isLoading: false,
+        avatar: patternflyAvatar,
+        timestamp: date.toLocaleString(),
+        actions: {
+          positive: { onClick: () => tracking.trackSingleItem(actionEvent2, { response: 'Good response' }) },
+          negative: { onClick: () => tracking.trackSingleItem(actionEvent2, { response: 'Bad response' }) },
+          copy: { onClick: () => tracking.trackSingleItem(actionEvent2, { response: 'Copy' }) },
+          share: { onClick: () => tracking.trackSingleItem(actionEvent2, { response: 'Share' }) },
+          listen: { onClick: () => tracking.trackSingleItem(actionEvent2, { response: 'Listen' }) }
+        }
+      });
+
+      const longMessage =
+        `It includes a long message. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
+          magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+          consequat. Duis aute irure dolor in reprehenderit in voluptatevelit esse cillum dolore eu fugiat nulla
+          pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
+          est laborum.`.repeat(2);
+
+      const contentChunks = longMessage.split(' ');
+      let chunkIndex = 0;
+
+      setMessages(loadedMessages);
+
+      let accumulatedContent = '';
+      let lastUpdateTime = 0;
+      const interval = setInterval(() => {
+        if (chunkIndex < contentChunks.length) {
+          accumulatedContent += ` ${contentChunks[chunkIndex]}`;
+          chunkIndex++;
+
+          // Only update every 100 ms or every 10 chunks
+          const now = Date.now();
+          const shouldUpdate = now - lastUpdateTime > 100 || chunkIndex % 10 === 0;
+
+          if (shouldUpdate) {
+            const updatedMessages = loadedMessages.map((msg, i) => {
+              if (i === messageIndex) {
+                return {
+                  ...msg,
+                  content: msg.content + accumulatedContent
+                };
+              }
+              return msg;
+            });
+
+            setMessages(updatedMessages);
+            lastUpdateTime = now;
+          }
+        } else {
+          setIsSendButtonDisabled(false);
+          clearInterval(interval);
+        }
+      }, 50);
+
+      // Scrolls to the bottom of the message box and resumes auto-scrolling
+      messageBoxRef.current?.scrollToBottom({ resumeSmartScroll: true });
+      // make announcement to assistive devices that new message has loaded
+      setAnnouncement(`Message from Bot: API response goes here`);
+      tracking.trackSingleItem('BotResponded', { undefined });
+    }, 100);
+  };
+
+  const findMatchingItems = (targetValue: string) => {
+    let filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => {
+      const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase()));
+      if (filteredItems.length > 0) {
+        acc[key] = filteredItems;
+      }
+      return acc;
+    }, {});
+
+    // append message if no items are found
+    if (Object.keys(filteredConversations).length === 0) {
+      filteredConversations = [{ id: '13', noIcon: true, text: 'No results found' }];
+    }
+    return filteredConversations;
+  };
+
+  const horizontalLogo = (
+    <Bullseye>
+      <Brand className="show-light" src={PFHorizontalLogoColor} alt="PatternFly" />
+      <Brand className="show-dark" src={PFHorizontalLogoReverse} alt="PatternFly" />
+    </Bullseye>
+  );
+
+  const iconLogo = (
+    <>
+      <Brand className="show-light" src={PFIconLogoColor} alt="PatternFly" />
+      <Brand className="show-dark" src={PFIconLogoReverse} alt="PatternFly" />
+    </>
+  );
+
+  const handleSkipToContent = (e) => {
+    e.preventDefault();
+    /* eslint-disable indent */
+    switch (displayMode) {
+      case ChatbotDisplayMode.default:
+        if (!chatbotVisible && toggleRef.current) {
+          toggleRef.current.focus();
+        }
+        if (chatbotVisible && chatbotRef.current) {
+          chatbotRef.current.focus();
+        }
+        break;
+
+      case ChatbotDisplayMode.docked:
+        if (chatbotRef.current) {
+          chatbotRef.current.focus();
+        }
+        break;
+      default:
+        if (historyRef.current) {
+          historyRef.current.focus();
+        }
+        break;
+    }
+    /* eslint-enable indent */
+  };
+
+  return (
+    <>
+      <SkipToContent onClick={handleSkipToContent} href="#">
+        Skip to chatbot
+      </SkipToContent>
+      <ChatbotToggle
+        tooltipLabel="Chatbot"
+        isChatbotVisible={chatbotVisible}
+        onToggleChatbot={function () {
+          setChatbotVisible(!chatbotVisible);
+          tracking.trackSingleItem('Chatbot Visible', { isVisible: !chatbotVisible }); // TODO correct?
+        }}
+        id="chatbot-toggle"
+        ref={toggleRef}
+      />
+      <Chatbot isVisible={chatbotVisible} displayMode={displayMode} ref={chatbotRef}>
+        <ChatbotConversationHistoryNav
+          displayMode={displayMode}
+          onDrawerToggle={() => {
+            setIsDrawerOpen(!isDrawerOpen);
+            setConversations(initialConversations);
+          }}
+          isDrawerOpen={isDrawerOpen}
+          setIsDrawerOpen={setIsDrawerOpen}
+          activeItemId="1"
+          // eslint-disable-next-line no-console
+          onSelectActiveItem={(e, selectedItem) => console.log(`Selected history item with id ${selectedItem}`)}
+          conversations={conversations}
+          onNewChat={() => {
+            setIsDrawerOpen(!isDrawerOpen);
+            setMessages([]);
+            setConversations(initialConversations);
+          }}
+          handleTextInputChange={(value: string) => {
+            if (value === '') {
+              setConversations(initialConversations);
+            }
+            // this is where you would perform search on the items in the drawer
+            // and update the state
+            const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value);
+            setConversations(newConversations);
+          }}
+          drawerContent={
+            <>
+              <ChatbotHeader>
+                <ChatbotHeaderMain>
+                  <ChatbotHeaderMenu
+                    ref={historyRef}
+                    aria-expanded={isDrawerOpen}
+                    onMenuToggle={() => setIsDrawerOpen(!isDrawerOpen)}
+                  />
+                  <ChatbotHeaderTitle
+                    displayMode={displayMode}
+                    showOnFullScreen={horizontalLogo}
+                    showOnDefault={iconLogo}
+                  ></ChatbotHeaderTitle>
+                </ChatbotHeaderMain>
+                <ChatbotHeaderActions>
+                  <ChatbotHeaderSelectorDropdown value={selectedModel} onSelect={onSelectModel}>
+                    <DropdownList>
+                      <DropdownItem value="Granite 7B" key="granite">
+                        Granite 7B
+                      </DropdownItem>
+                      <DropdownItem value="Llama 3.0" key="llama">
+                        Llama 3.0
+                      </DropdownItem>
+                      <DropdownItem value="Mistral 3B" key="mistral">
+                        Mistral 3B
+                      </DropdownItem>
+                    </DropdownList>
+                  </ChatbotHeaderSelectorDropdown>
+                  <ChatbotHeaderOptionsDropdown onSelect={onSelectDisplayMode}>
+                    <DropdownGroup label="Display mode">
+                      <DropdownList>
+                        <DropdownItem
+                          value={ChatbotDisplayMode.default}
+                          key="switchDisplayOverlay"
+                          icon={<OutlinedWindowRestoreIcon aria-hidden />}
+                          isSelected={displayMode === ChatbotDisplayMode.default}
+                        >
+                          <span>Overlay</span>
+                        </DropdownItem>
+                        <DropdownItem
+                          value={ChatbotDisplayMode.docked}
+                          key="switchDisplayDock"
+                          icon={<OpenDrawerRightIcon aria-hidden />}
+                          isSelected={displayMode === ChatbotDisplayMode.docked}
+                        >
+                          <span>Dock to window</span>
+                        </DropdownItem>
+                        <DropdownItem
+                          value={ChatbotDisplayMode.fullscreen}
+                          key="switchDisplayFullscreen"
+                          icon={<ExpandIcon aria-hidden />}
+                          isSelected={displayMode === ChatbotDisplayMode.fullscreen}
+                        >
+                          <span>Fullscreen</span>
+                        </DropdownItem>
+                      </DropdownList>
+                    </DropdownGroup>
+                  </ChatbotHeaderOptionsDropdown>
+                </ChatbotHeaderActions>
+              </ChatbotHeader>
+              <ChatbotContent>
+                {/* Update the announcement prop on MessageBox whenever a new message is sent
+                 so that users of assistive devices receive sufficient context  */}
+                <MessageBox announcement={announcement} enableSmartScroll ref={messageBoxRef}>
+                  <ChatbotWelcomePrompt
+                    title="Hi, ChatBot User!"
+                    description="How can I help you today?"
+                    prompts={welcomePrompts}
+                  />
+                  {/* This code block enables scrolling to the top of the last message.
path: packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotScrolling.tsx
user: login: rebeccaalpert
body: We can definitely pop this comment out. :) 
created_at: 2025-05-14T14:53:49Z
author_association: MEMBER
diff_hunk: @@ -12,79 +25,201 @@ export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
   /** Custom aria-label for scrollable portion of message box */
   ariaLabel?: string;
   /** Content to be displayed in the message box */
-  children: React.ReactNode;
+  children: ReactNode;
   /** Custom classname for the MessageBox component */
   className?: string;
-  /** Ref applied to message box */
-  innerRef?: React.Ref<HTMLDivElement>;
path: packages/module/src/MessageBox/MessageBox.tsx
user: login: rebeccaalpert
body: I think we can't drop this prop, but we can stop using it for anything and stick ```/** @deprecated ``` in front of the comment.
created_at: 2025-05-14T14:56:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,550 @@
+import React from 'react';
+
+import { Bullseye, Brand, DropdownList, DropdownItem, DropdownGroup, SkipToContent } from '@patternfly/react-core';
+
+import ChatbotToggle from '@patternfly/chatbot/dist/dynamic/ChatbotToggle';
+import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
+import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent';
+import ChatbotWelcomePrompt from '@patternfly/chatbot/dist/dynamic/ChatbotWelcomePrompt';
+import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter';
+import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar';
+import MessageBox, { MessageBoxHandle } from '@patternfly/chatbot/dist/dynamic/MessageBox';
+import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message';
+import ChatbotConversationHistoryNav, {
+  Conversation
+} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
+import ChatbotHeader, {
+  ChatbotHeaderMenu,
+  ChatbotHeaderMain,
+  ChatbotHeaderTitle,
+  ChatbotHeaderActions,
+  ChatbotHeaderSelectorDropdown,
+  ChatbotHeaderOptionsDropdown
+} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader';
+
+import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
+import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon';
+import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon';
+
+import PFHorizontalLogoColor from '../UI/PF-HorizontalLogo-Color.svg';
+import PFHorizontalLogoReverse from '../UI/PF-HorizontalLogo-Reverse.svg';
+import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg';
+import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg';
+import userAvatar from '../Messages/user_avatar.svg';
+import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
+import { getTrackingProviders } from '@patternfly/chatbot/dist/dynamic/tracking';
path: packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotScrolling.tsx
user: login: karthikjeeyar
body: Removed tracking stuff.
created_at: 2025-05-14T16:16:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,79 +25,201 @@ export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
   /** Custom aria-label for scrollable portion of message box */
   ariaLabel?: string;
   /** Content to be displayed in the message box */
-  children: React.ReactNode;
+  children: ReactNode;
   /** Custom classname for the MessageBox component */
   className?: string;
-  /** Ref applied to message box */
-  innerRef?: React.Ref<HTMLDivElement>;
path: packages/module/src/MessageBox/MessageBox.tsx
user: login: karthikjeeyar
body: Done.
created_at: 2025-05-14T16:17:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +64,42 @@ It is also important to announce when new content appears onscreen for accessibi
 
 ```
 
+### Message auto-scrolling
+
+This demo displays automatic scrolling to the bottom of the MessageBox:
+
+1. [`<MessageBox>`](/patternfly-ai/chatbot/ui#message-box) component provides an optional smart scroll behavior via the `enableSmartScroll` prop.
+
+#### `enableSmartScroll` (optional)
+
+When enabled:
+
+- Scroll position is automatically managed based on user interaction.
+- Scrolling is _not_ forced to the bottom on new messages unless explicitly triggered via the `scrollToBottom()` method.
+- If the user scrolls up or interacts with UI controls like "Back to top" or "Back to bottom", the component pauses auto-scroll to respect user intent.
+- Auto-scroll resumes only when the user scrolls back down manually or programmatically via the `scrollToBottom({resumeSmartScroll: true})` method.
+
+#### Imperative Methods via `ref`
+
+When using `ref`, the component exposes the following methods:
+
+- `scrollToBottom()` – Scrolls to the bottom of the message container.
+- `scrollToTop()` – Scrolls to the top of the message container.
+- `isSmartScrollActive()` – Returns `true` if smart auto-scroll is currently active.
+- Native `HTMLDivElement` methods like `scrollTo()` are also available.
+
+This demo includes broader ChatBot features, including:
+
+1. A [`<ChatbotToggle>`](/patternfly-ai/chatbot/ui#toggle) that controls the [`<Chatbot>`](/patternfly-ai/chatbot/ui#container) container.
+2. A `<ChatbotContent>` and [`<MessageBox>`](/patternfly-ai/chatbot/ui#content-and-message-box) with:
+   - A `<ChatbotWelcomePrompt>`
+   - An initial user message and initial bot message
+3. A [`<ChatbotFooter>`](/patternfly-ai/chatbot/ui#footer) with a [`<ChatbotFootnote>`](/patternfly-ai/chatbot/ui#footnote-with-popover) and a `<MessageBar>`
path: packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md
user: login: karthikjeeyar
body: I have updated the documentation.
created_at: 2025-05-14T16:17:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,550 @@
+import React from 'react';
+
+import { Bullseye, Brand, DropdownList, DropdownItem, DropdownGroup, SkipToContent } from '@patternfly/react-core';
+
+import ChatbotToggle from '@patternfly/chatbot/dist/dynamic/ChatbotToggle';
+import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
+import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent';
+import ChatbotWelcomePrompt from '@patternfly/chatbot/dist/dynamic/ChatbotWelcomePrompt';
+import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter';
+import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar';
+import MessageBox, { MessageBoxHandle } from '@patternfly/chatbot/dist/dynamic/MessageBox';
+import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message';
+import ChatbotConversationHistoryNav, {
+  Conversation
+} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
+import ChatbotHeader, {
+  ChatbotHeaderMenu,
+  ChatbotHeaderMain,
+  ChatbotHeaderTitle,
+  ChatbotHeaderActions,
+  ChatbotHeaderSelectorDropdown,
+  ChatbotHeaderOptionsDropdown
+} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader';
+
+import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
+import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon';
+import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon';
+
+import PFHorizontalLogoColor from '../UI/PF-HorizontalLogo-Color.svg';
+import PFHorizontalLogoReverse from '../UI/PF-HorizontalLogo-Reverse.svg';
+import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg';
+import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg';
+import userAvatar from '../Messages/user_avatar.svg';
+import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
+import { getTrackingProviders } from '@patternfly/chatbot/dist/dynamic/tracking';
+import { InitProps } from '@patternfly/chatbot/dist/dynamic/tracking';
+import '@patternfly/react-core/dist/styles/base.css';
+import '@patternfly/chatbot/dist/css/main.css';
+
+const footnoteProps = {
+  label: 'ChatBot uses AI. Check for mistakes.',
+  popover: {
+    title: 'Verify information',
+    description: `While ChatBot strives for accuracy, AI is experimental and can make mistakes. We cannot guarantee that all information provided by ChatBot is up to date or without error. You should always verify responses using reliable sources, especially for crucial information and decision making.`,
+    bannerImage: {
+      src: 'https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif',
+      alt: 'Example image for footnote popover'
+    },
+    cta: {
+      label: 'Dismiss',
+      onClick: () => {
+        alert('Do something!');
+      }
+    },
+    link: {
+      label: 'View AI policy',
+      url: 'https://www.redhat.com/'
+    }
+  }
+};
+
+const markdown = `A paragraph with *emphasis* and **strong importance**.
+
+> A block quote with ~strikethrough~ and a URL: https://reactjs.org.
+
+Here is an inline code - \`() => void\`
+
+Here is some YAML code:
+
+~~~yaml
+apiVersion: helm.openshift.io/v1beta1/
+kind: HelmChartRepository
+metadata:
+  name: azure-sample-repo0oooo00ooo
+spec:
+  connectionConfig:
+  url: https://raw.githubusercontent.com/Azure-Samples/helm-charts/master/docs
+~~~
+
+Here is some JavaScript code:
+
+~~~js
+import React from 'react';
+
+const MessageLoading = () => (
+  <div className="pf-chatbot__message-loading">
+    <span className="pf-chatbot__message-loading-dots">
+      <span className="pf-v6-screen-reader">Loading message</span>
+    </span>
+  </div>
+);
+
+export default MessageLoading;
+
+~~~
+`;
+
+// It's important to set a date and timestamp prop since the Message components re-render.
+// The timestamps re-render with them.
+const date = new Date();
+
+const initProps: InitProps = {
+  verbose: false,
+  segmentKey: 'TODO-key', // TODO add your key here
+  posthogKey: 'TODO-key',
+  umamiKey: 'TODO-key',
+  umamiHostUrl: 'http://localhost:3000', // TODO where is your Umami installation?
+  console: true,
+  something: 'test'
+};
+
+const tracking = getTrackingProviders(initProps);
+tracking.identify('user-123', { superUser: true }); // TODO get real user id + properties
+tracking.trackPageView(window.location.href);
+
+const actionEventName = 'MessageAction';
+const initialMessages: MessageProps[] = [
+  {
+    id: '1',
+    role: 'user',
+    content: 'Hello, can you give me an example of what you can do?',
+    name: 'User',
+    avatar: userAvatar,
+    timestamp: date.toLocaleString(),
+    avatarProps: { isBordered: true }
+  },
+  {
+    id: '2',
+    role: 'bot',
+    content: markdown,
+    name: 'Bot',
+    avatar: patternflyAvatar,
+    timestamp: date.toLocaleString(),
+    actions: {
+      positive: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Good response' }) },
+      negative: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Bad response' }) },
+      copy: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Copy' }) },
+      share: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Share' }) },
+      listen: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Listen' }) }
+    }
+  }
+];
+
+const welcomePrompts = [
+  {
+    title: 'Set up account',
+    message: 'Choose the necessary settings and preferences for your account.'
+  },
+  {
+    title: 'Troubleshoot issue',
+    message: 'Find documentation and instructions to resolve your issue.'
+  }
+];
+
+const initialConversations = {
+  Today: [{ id: '1', text: 'Hello, can you give me an example of what you can do?' }],
+  'This month': [
+    {
+      id: '2',
+      text: 'Enterprise Linux installation and setup'
+    },
+    { id: '3', text: 'Troubleshoot system crash' }
+  ],
+  March: [
+    { id: '4', text: 'Ansible security and updates' },
+    { id: '5', text: 'Red Hat certification' },
+    { id: '6', text: 'Lightspeed user documentation' }
+  ],
+  February: [
+    { id: '7', text: 'Crashing pod assistance' },
+    { id: '8', text: 'OpenShift AI pipelines' },
+    { id: '9', text: 'Updating subscription plan' },
+    { id: '10', text: 'Red Hat licensing options' }
+  ],
+  January: [
+    { id: '11', text: 'RHEL system performance' },
+    { id: '12', text: 'Manage user accounts' }
+  ]
+};
+
+const actionEvent2 = 'ActionEvent2';
+export const ChatbotScrollingDemo: React.FunctionComponent = () => {
+  const [chatbotVisible, setChatbotVisible] = React.useState<boolean>(true);
+  const [displayMode, setDisplayMode] = React.useState<ChatbotDisplayMode>(ChatbotDisplayMode.default);
+  const [messages, setMessages] = React.useState<MessageProps[]>(initialMessages);
+  const [selectedModel, setSelectedModel] = React.useState('Granite 7B');
+  const [isSendButtonDisabled, setIsSendButtonDisabled] = React.useState(false);
+  const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);
+  const [conversations, setConversations] = React.useState<Conversation[] | { [key: string]: Conversation[] }>(
+    initialConversations
+  );
+  const [announcement, setAnnouncement] = React.useState<string>();
+  const toggleRef = React.useRef<HTMLButtonElement>(null);
+  const chatbotRef = React.useRef<HTMLDivElement>(null);
+  const historyRef = React.useRef<HTMLButtonElement>(null);
+  const messageBoxRef = React.useRef<MessageBoxHandle>(null); // ref to MessageBox will have isSmartScrollActive, scrollToTop and scrollToBottom
+  const scrollQueued = React.useRef(false);
+
+  // Auto-scrolls to the latest message
+  React.useLayoutEffect(() => {
+    if (!messageBoxRef.current?.isSmartScrollActive() || scrollQueued.current) {
+      return undefined;
+    }
+
+    let rafId = 0;
+    // don't scroll the first load - in this demo, we know we start with two messages
path: packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotScrolling.tsx
user: login: karthikjeeyar
body: Added a condition to not scroll on the first load.
created_at: 2025-05-14T16:19:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: patternfly-build
created_at: 2025-05-08T18:55:41Z
body: Preview: https://chatbot-pr-chatbot-550.surge.sh

A11y report: https://chatbot-pr-chatbot-550-a11y.surge.sh


author_association: NONE
user: login: karthikjeeyar
created_at: 2025-05-16T16:17:52Z
body: Thanks for pointing this out. you're right that when a long message comes in and the user is already at the bottom, the current auto-scroll behavior might jump past the beginning of that message, especially for users on touch devices.

I followed the common behaviour found in the interfaces like ChatGPT, where auto-scroll continues as long as the user is at the bottom when a new message starts. But I can see how that might not be ideal in cases like you mentioned above.

To handle this more gracefully, consumers can choose to use `messages.length` instead of the full `messages` array in their dependency list. This ensures that scrolling happens only when a new message is added, and it scrolls to the start of the latest message, letting users read from the top and scroll down naturally. Once they reach the bottom again, auto-scroll behavior resumes.

```
React.useEffect(() => {
  ref.current?.scrollToBottom();
  // Scrolls to the latest message just once.
}, [messages.length])
```


https://github.com/user-attachments/assets/2efb6bf0-91d0-46ef-845e-2a6706ba8272


Please let me know if this works or if there any other suggestions to handle this. BTW, I have added a usage comment on this [commit](https://github.com/patternfly/chatbot/pull/550/commits/297df6fcf36a5d2dcf20ccd398b56d979b1af625)


author_association: CONTRIBUTOR
user: login: rebeccaalpert
created_at: 2025-05-19T17:38:57Z
body: I think this is fine for what's worth!
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-05-19T19:22:24Z
body: :tada: This PR is included in version 6.3.0-prerelease.16 :tada:

The release is available on:
- [GitHub release](https://github.com/patternfly/chatbot/releases/tag/prerelease-v6.3.0-prerelease.16)
- [npm package (@prerelease dist-tag)](https://www.npmjs.com/package/@patternfly/chatbot/v/6.3.0-prerelease.16)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE