Follow the thread's bottom, and offer a jump back to it

Sending a message while scrolled up left it out of sight: the thread only
ever anchored its bottom, so nothing brought the view down. The column now
follows new messages while it sits at the bottom, stays put while it's
scrolled up, and always brings a message the user just sent into view.
While the thread sits above its bottom, "Jump to latest" floats over it.

Whether the thread is at its bottom is read from where the content ends
rather than from `scrollTop`, whose sign a reversed column reports
differently across engines. Content that grows after it lands (an image,
a wider window) is followed too, so the newest message never slips under
the composer.
This commit is contained in:
2026-10-23 16:03:37 +01:00
parent d9aea8e9c8
commit 55fc5ee97b
5 changed files with 408 additions and 31 deletions
@@ -0,0 +1,28 @@
import Button from "@moduletrace/twui/layout/Button";
import { ArrowDown } from "lucide-react";
type Props = {
onClick: () => void;
};
/**
* "Jump to latest", floating over the bottom of the thread while it sits
* above it: a bot's reply that landed out of sight, or a long scroll back
* through the conversation.
*/
export default function ScrollToBottomButton({ onClick }: Props) {
return (
<Button
type="button"
color="secondary"
title="Jump to the latest message"
aria-label="Jump to the latest message"
variant="outlined"
onClick={onClick}
className="absolute bottom-3 left-1/2 z-20 shrink-0 -translate-x-1/2 gap-1.5! px-2.5! py-1.5! rounded-default-sm border border-solid border-hairline bg-surface shadow-popover text-xs font-bold text-ink hover:bg-sunken focus-visible:ring-2 focus-visible:ring-ink"
>
<ArrowDown size={13} aria-hidden />
Jump to latest
</Button>
);
}