MessageScroller
The transcript: follows new output, never moves you while you read back.
import { MessageScroller } from "@flinbu/seed";A conversation with Pluto
Send a message: your turn scrolls near the top and the reply grows below it.
import { Button, InputGroup, Marker, Message, MessageScroller, Trace } from "@flinbu/seed";
import { ArrowUp } from "lucide-react";
const [turns, setTurns] = React.useState<{ id: number; from: "user" | "assistant"; text: string }[]>([
{ id: 1, from: "user", text: "What changed in v2.4?" },
{ id: 2, from: "assistant", text: "Eight tokens moved: the accent ramp is one step lighter in Dark, and two radius tokens were renamed. Nothing was removed." },
]);
const [draft, setDraft] = React.useState("");
const send = () => {
const t = draft.trim(); if (!t) return;
const id = turns.length + 1;
setTurns((p) => [...p, { id, from: "user", text: t }]);
setDraft("");
setTimeout(() => setTurns((p) => [...p, { id: id + 1, from: "assistant", text: "I'll check that and propose a change for you to approve." }]), 600);
};
<div className="flex h-[min(32rem,80dvh)] w-full max-w-xl flex-col overflow-hidden rounded-petal rounded-tl-petal-tip border border-secondary bg-primary">
<MessageScroller className="flex-1">
<Marker variant="separator">Today</Marker>
{turns.map((t) => <Message key={t.id} from={t.from} actions={t.from === "assistant" ? actions : undefined}>{t.text}</Message>)}
<Trace summary="Pluto checked your Dark theme · 6s" steps={STEPS} />
<Marker status="running">Checking 435 tokens</Marker>
</MessageScroller>
<div className="border-t border-secondary p-sm">
<InputGroup multiline rows={2} aria-label="Message Pluto" placeholder="Ask Pluto…" value={draft} onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }}
bottom={<Button size="sm" icon={<ArrowUp />} aria-label="Send" disabled={!draft.trim()} onClick={send} />} />
</div>
</div>Use it for
- Any chat with streamed answers.
Use something else for
- Putting the composer inside it: keep it below.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
autoScroll | boolean | true | Follow output at the bottom. |
peek | number | 48 | Previous turn left visible (px). |
defaultScrollPosition | 'end''start' | 'end' | Where it starts. |
height | numberstring | – | Or give the parent a height. |
jumpLabel | string | 'Jump to latest' | Button text. |
Accessibility
- A focusable log announced politely.
- Scrolling up by wheel, touch or keyboard stops following; 'Jump to latest' brings you back.