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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.