Components/Message Scroller

Message Scroller

The window a chat transcript scrolls inside: a 420 × 480 frame holding 585 of messages, and one 28px button that appears when you are no longer at the newest one.

MessageScroller / Button · Components frameLive in the browser · drawn to match the file
Direction
State
Down · Default28 × 28 in the file
Six variants, and one of them rotates

Direction is not two drawings. It is the same 16px chevron turned 180 degrees, which is why Up and Down cannot drift apart: change the glyph once and both follow. Three states across two directions is the whole component.

A border and a ring, together

The button always carries a 1px base.border, because it floats over a transcript and needs an edge against the messages behind it. Focus adds a 3px base.ring outside that border rather than replacing it - the only control in the kit that wears both at once.

Inert is a state you will not find here

The source says the button is inert until there is content in its direction. There is no Disabled variant, because a scroller with nothing below simply does not render one. The matrix draws what you can see, not what you hide.

The frame

A window onto a transcript that is taller than it

420 × 480, clipped, with 585 of content inside it. The whole component is that ratio: a viewport shorter than what it holds, and a button that appears when you are not at the live edge. Drag the switch to send it back to the newest message and watch the reason go away.

MessageScroller · Components frameLive in the browser · drawn to match the file
Position
OliviaDid the API keys come through on your side?
Just checked - all four are live.
Good. I'll wire the staging env tonight.Delivered
Thanks. Ping me if anything 401s.Delivered · 09:41
OliviaWill do. One more thing -
The rate limit is 60/min, not 600.
420 × 480, content 420 × 585offset -42
Why the offset is -42 and not a round number

The last message starts 522 into the content and the viewport is 480 tall, so parking the content at -42 puts that message exactly at the fold: one turn behind, the smallest gap that still earns the button. Any further and you are simply scrolled up.

The rows are real Messages

Six instances of the Message component, 32 apart, inside 20 of padding - avatars, headers and delivery footers included. The scroller adds nothing to a message; it only decides which ones you can see.

Five things it is for

A scroller earns its keep when the transcript moves

None of this is visible in a still frame, which is why the file can only draw the shell. The source lists ten behaviours; these are the five you can see happen. Every offset below is computed from the row heights, not placed by hand.

Scenario
OliviaCan you take the Tuesday review?
Yes - I'll prep the numbers Monday night.
OliviaSeparately, the staging box is down again.
Restarting it now, give it two minutes to come back.
OliviaDo we still need the Friday sync?
I'd drop it. Nothing has changed since Tuesday.
OliviaWho is covering support next week?
Marc until Wednesday, me after that.
OliviaAnd the contractor invoices?
Approved this morning, they go out with the batch.
OliviaThat is everything from me.
Good. Talk Monday.
The review · offset 0

A turn is a question and its answer, not a message. Stepping moves the viewport to the top of the next turn rather than a fixed number of pixels, so a two-line question and a one-line one land the same way.

The usage frame

The transcript, and the pair of controls beside it

620 by 822. One section is the scroller doing its job; the other lays the two directions side by side, because in a real transcript they are one control with two jobs rather than two buttons that happen to look alike.

Scroll controls

Start scrolls to the top of the transcript, End to the newest message. Each is inert when there is nothing to scroll to in that direction.

Note what the frame does not own. It has no header, no composer and no empty state, because a scroller is not a chat window - it is the piece in the middle that decides what a chat window shows. Put a Field under it and a header above it and you have the window.

In the file

Two components, six variants

Read straight out of the Penpot file. The axes below are the property names you will see in the variant dropdown.

MessageScroller / ButtonDirection × StateDown · Up × Default · Hover · Focus - 28 × 28, the Up chevron is the Down one turned 1806
MessageScrollernone420 × 480 clipped, over 420 × 585 of transcript1

Using the Message Scroller

Keeping a streaming transcript from yanking the page around. The source splits it into a headless provider that owns the scroll state, a viewport that takes the native scroll events, a content container, an item wrapper per row, and the button. The kit draws the two of those you can see: the frame and the button.

Because the ratio is the component. A viewport shorter than what it holds is the entire premise - if the transcript fit, there would be nothing to scroll and no button to draw. The content is parked at -42, which puts the newest message exactly at the fold: one turn behind, the smallest gap that still earns the control.

Only the rotation. Up is the Down chevron turned 180 degrees rather than a second glyph, so the two can never drift apart. Start scrolls to the top of the transcript, End to the newest message, and each is inert when there is nothing to reach in that direction.

Because inert here means absent. The source hides the button rather than greying it, so a scroller sitting at the live edge simply does not render one. Drawing a disabled state would be drawing something the component never shows.

Not at all. The transcript is six Message instances 32 apart inside 20 of padding - avatars, a header and a delivery footer included. The scroller adds nothing to a row; it only decides which rows you can see. It has no composer and no header either, so a real chat window is this with a Field under it.

The behaviours a still frame cannot. Anchored turns jump to the top of a turn rather than a fixed distance. Group chat anchors on markers - a day, someone joining - because with three people the turn stops being the useful unit. Context kept is the one the component exists for: messages arrive while you read back and the viewport does not move, the button takes a count instead. Opening a thread lands on the first unread rather than the top or the bottom. And loading earlier messages shifts the offset by exactly the height that went on top, so the row under your eye stays put.

Data display · 6 variants