Skip to content

feat: add floating scroll-to-bottom button#701

Open
hankkyy wants to merge 1 commit into
fathah:mainfrom
hankkyy:feat/scroll-to-bottom
Open

feat: add floating scroll-to-bottom button#701
hankkyy wants to merge 1 commit into
fathah:mainfrom
hankkyy:feat/scroll-to-bottom

Conversation

@hankkyy

@hankkyy hankkyy commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Feature

Show a floating scroll-to-bottom button (↓) when the user scrolls up away from the latest messages. Clicking it smooth-scrolls back to the bottom and re-engages auto-scroll for new messages.

Changes

useChatScroll.ts

  • Added isScrolledUp: boolean to the return type — updated via scroll event listener
  • Added scrollToBottomNow() imperative action — resets scrolled-up state and scrolls to bottom
  • Uses useState alongside the existing ref so React knows when to show/hide the button

Chat.tsx

  • Destructures isScrolledUp and scrollToBottomNow from useChatScroll
  • Renders a .chat-scroll-bottom button (with ArrowDown icon) when isScrolledUp is true

main.css

  • .chat-messages now has position: relative so the sticky button anchors correctly
  • .chat-scroll-bottom — 36x36px circle, sticky at bottom, with box-shadow, hover lift effect

i18n

  • Added scrollToBottom key to all 10 supported locales

Show a floating ↓ button when the user scrolls up away from the latest
messages. Clicking it smooth-scrolls to the bottom and re-engages
auto-scroll.

Changes:
- useChatScroll: expose isScrolledUp state and scrollToBottomNow()
  imperative action alongside existing containerRef/bottomRef
- Chat: render .chat-scroll-bottom button (sticky, centered) when
  isScrolledUp, using ArrowDown icon from lucide-react
- main.css: .chat-messages gets position:relative, .chat-scroll-bottom
  is a 36px circle with shadow and hover lift
- i18n: add scrollToBottom string to all 10 locales
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a floating "scroll to bottom" button that appears when the user scrolls away from the latest messages, smooth-scrolling back and re-engaging auto-scroll on click. The implementation covers the hook, UI, CSS, and i18n for all 10 locales.

  • useChatScroll.ts: adds isScrolledUp React state (mirroring the existing userScrolledUpRef) and a new scrollToBottomNow action; both are returned to the caller.
  • Chat.tsx: conditionally renders a sticky ArrowDown button inside .chat-messages when isScrolledUp is true, with proper aria-label and title attributes.
  • CSS / i18n: .chat-messages gains position: relative; the new .chat-scroll-bottom rule uses position: sticky anchored to the flex column; all locale files receive the scrollToBottom key.

Confidence Score: 3/5

The feature works end-to-end for instant-stop scrolls, but clicking the button while the chat is mid-animation leaves auto-scroll re-paused and causes the button to flicker back into view.

The core race condition in scrollToBottomNow means that clicking the button in the normal case (smooth scrolling from a position well above the bottom) re-sets userScrolledUpRef.current back to true via intermediate scroll events before the animation completes. The user clicked go to bottom and stay there, but auto-scroll ends up paused again — the exact opposite of what was intended. This affects the primary interaction the feature was built around.

src/renderer/src/screens/Chat/hooks/useChatScroll.ts — specifically the scrollToBottomNow callback and its interaction with the handleScroll listener.

Important Files Changed

Filename Overview
src/renderer/src/screens/Chat/hooks/useChatScroll.ts Adds isScrolledUp state and scrollToBottomNow action; scrollToBottomNow uses smooth scroll which races against the scroll event listener, causing the button to reappear and auto-scroll to break mid-animation.
src/renderer/src/screens/Chat/Chat.tsx Conditionally renders the scroll-to-bottom button using the new hook values; button placement, accessibility attributes, and i18n usage are correct.
src/renderer/src/assets/main.css Adds position: relative to .chat-messages and a new .chat-scroll-bottom style; sticky positioning works correctly because .chat-messages is already display: flex; flex-direction: column, so align-self: center is valid.
src/shared/i18n/locales/en/chat.ts Adds scrollToBottom key; also present in all 10 locale files.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant Chat.tsx
    participant useChatScroll
    participant DOM

    User->>DOM: scrolls up in .chat-messages
    DOM->>useChatScroll: scroll event fires
    useChatScroll->>useChatScroll: "atBottom=false → setIsScrolledUp(true)"
    useChatScroll-->>Chat.tsx: "isScrolledUp=true"
    Chat.tsx-->>User: renders scroll-to-bottom button

    User->>Chat.tsx: clicks button
    Chat.tsx->>useChatScroll: scrollToBottomNow()
    useChatScroll->>useChatScroll: "userScrolledUpRef=false, setIsScrolledUp(false)"
    useChatScroll->>DOM: "scrollIntoView({ behavior: smooth })"
    Note over DOM,useChatScroll: ⚠️ smooth animation fires intermediate scroll events
    DOM-->>useChatScroll: "scroll event (mid-animation, atBottom=false)"
    useChatScroll->>useChatScroll: setIsScrolledUp(true) ← re-enables button mid-scroll
    DOM-->>useChatScroll: "scroll event (reached bottom, atBottom=true)"
    useChatScroll->>useChatScroll: setIsScrolledUp(false)
    useChatScroll-->>Chat.tsx: "isScrolledUp=false"
    Chat.tsx-->>User: button hidden
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant Chat.tsx
    participant useChatScroll
    participant DOM

    User->>DOM: scrolls up in .chat-messages
    DOM->>useChatScroll: scroll event fires
    useChatScroll->>useChatScroll: "atBottom=false → setIsScrolledUp(true)"
    useChatScroll-->>Chat.tsx: "isScrolledUp=true"
    Chat.tsx-->>User: renders scroll-to-bottom button

    User->>Chat.tsx: clicks button
    Chat.tsx->>useChatScroll: scrollToBottomNow()
    useChatScroll->>useChatScroll: "userScrolledUpRef=false, setIsScrolledUp(false)"
    useChatScroll->>DOM: "scrollIntoView({ behavior: smooth })"
    Note over DOM,useChatScroll: ⚠️ smooth animation fires intermediate scroll events
    DOM-->>useChatScroll: "scroll event (mid-animation, atBottom=false)"
    useChatScroll->>useChatScroll: setIsScrolledUp(true) ← re-enables button mid-scroll
    DOM-->>useChatScroll: "scroll event (reached bottom, atBottom=true)"
    useChatScroll->>useChatScroll: setIsScrolledUp(false)
    useChatScroll-->>Chat.tsx: "isScrolledUp=false"
    Chat.tsx-->>User: button hidden
Loading

Reviews (1): Last reviewed commit: "feat: add floating scroll-to-bottom butt..." | Re-trigger Greptile

Comment on lines +28 to +32
const scrollToBottomNow = useCallback(() => {
userScrolledUpRef.current = false;
setIsScrolledUp(false);
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, []);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Smooth scroll fires intermediate events that re-engage scrolled-up state

scrollToBottomNow sets userScrolledUpRef.current = false and setIsScrolledUp(false), then immediately kicks off a behavior: "smooth" scroll. The smooth animation generates a continuous stream of scroll events as the container travels from its current position to the bottom. Each intermediate event calls handleScroll, which evaluates atBottom = scrollHeight - scrollTop - clientHeight < 60 — a check that returns false for most of the animation. This causes setIsScrolledUp(true) and userScrolledUpRef.current = true to fire repeatedly mid-animation: the button flashes back into view and auto-scroll is paused again the instant the user clicks it.

The simplest fix is to use instant scrolling in scrollToBottomNow so no intermediate events are generated. Direct assignment to scrollTop is cleaner than scrollIntoView here because it operates on the container you already have a ref to. Alternatively, keep scrollIntoView but guard handleScroll with an isProgrammaticScrollRef flag that is set before the call and cleared in a short setTimeout.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant