Learn egui in Neovim — Ep 10: Scroll Area (Log Viewer with Auto-Scroll)
Video: Learn egui in Neovim — Ep 10: Scroll Area (Log Viewer with Auto-Scroll) by Taught by Celeste AI - AI Coding Coach
egui::ScrollArea::vertical().stick_to_bottom(true)— a scrollable list that follows new entries as they arrive.
Most useful UIs have a list that grows. Chat messages, log entries, search results, file listings. Past a few dozen items, the list outgrows the window and you need scrolling. egui's ScrollArea adds a viewport to any vertical or horizontal block of widgets — and stick_to_bottom(true) gives you the chat-style auto-scroll behaviour where the bottom is always visible.
What we are building
A log viewer. A button to add new entries. A scrolling list of log lines below it. Every time you click "Add Log Entry", a new line appears at the bottom and the viewport auto-scrolls to follow it.
The script
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Log Viewer");
ui.separator();
if ui.button("Add Log Entry").clicked() {
self.next_id += 1;
self.logs.push(format!("Event #{}", self.next_id));
}
ui.separator();
egui::ScrollArea::vertical()
.stick_to_bottom(true)
.show(ui, |ui| {
for (i, log) in self.logs.iter().enumerate() {
ui.label(format!("[{}] {}", i + 1, log));
}
});
});
A button, a separator, and a scroll area. The scroll area contains a for loop that emits one label per log entry.
ScrollArea::vertical
egui::ScrollArea::vertical()
.stick_to_bottom(true)
.show(ui, |ui| {
// widgets that may overflow
});
The closure receives a Ui reference; widgets added inside it are rendered into a viewport that scrolls vertically. If the content fits without scrolling, no scrollbar appears. If it overflows, a scrollbar appears on the right and the content can be scrolled.
vertical() is the most common variant; there is also horizontal() and both(). For two-way scrolling (a large image, a wide table), use both().
stick_to_bottom
.stick_to_bottom(true)
Default behaviour: the scroll position is preserved across frames. If the user scrolled up to read older logs, new logs appear off-screen and the viewport stays where the user left it.
stick_to_bottom(true): as long as the user has not manually scrolled, the viewport keeps the latest content visible. The moment new entries appear at the bottom, the viewport scrolls to follow. If the user does scroll up, the stickiness pauses — they see what they scrolled to, not the new content.
This is the chat-app behaviour. Slack, Discord, terminal logs all do this. It is the right default for an append-only list.
Iterating with index
for (i, log) in self.logs.iter().enumerate() {
ui.label(format!("[{}] {}", i + 1, log));
}
enumerate gives both the index and the value. We display [1] Event #1, [2] Event #2, ... — the line number alongside the message.
For very long lists (thousands of entries), this naive approach gets slow because every entry generates a String per frame. egui's ScrollArea::show_rows exists for that case — it lets you tell egui the row height and total row count, and it only renders the rows visible in the viewport. Virtualised scrolling. Not needed here, but useful at scale.
Other ScrollArea options
.max_height(px)/.max_width(px)— cap the viewport size. Useful for embedded scroll areas that should not grow beyond a fixed region..auto_shrink([true, true])— let the area shrink to its content if the content is small. Default is on; turn off if you want a fixed-size viewport..id_salt("foo")— give the scroll area a unique ID if you have multiple in the same panel (egui will warn at runtime if IDs collide).
Why this works in immediate mode
A scroll area in retained-mode UIs needs careful state: where is the viewport positioned, has it changed since last frame, what items are visible. In egui, the scroll position is internal state egui manages on your behalf, and the contents are simply the widgets you add inside .show(). If your data changes (more logs), the next frame's content has more labels; egui notices and updates the scrollbar accordingly.
The stick_to_bottom behaviour is a tiny piece of additional logic: was the viewport at the bottom last frame? If yes and the content grew, scroll to the new bottom.
Running it
cargo run. The window has 15 starter log entries. Click "Add Log Entry" — a new line appears at the bottom. The viewport sticks to the bottom, so the new line is always visible.
Scroll up manually. Click "Add Log Entry" several times. The new entries are added but the viewport stays where you left it. Scroll back down — the stickiness re-engages, and the next click takes you to the latest entry again.
When to scroll vs. paginate
ScrollArea is the right tool for sequential reading: chat messages, logs, comments. Pagination is the right tool for finding something in a large list: search results, product catalogs.
Rule of thumb: if users mostly read in order, scroll. If they mostly skim and click, paginate.
Common mistakes
Forgetting stick_to_bottom on log viewers. Without it, new logs are off-screen and users miss them.
ScrollArea inside ScrollArea inside ScrollArea. Nested scrolling is confusing. egui supports it, but every level of nesting makes the UI harder to use. One scroll area per panel is the rule.
Reading too many entries. A scroll area that contains 100,000 labels will eat CPU because egui still iterates them all to compute layout. Use show_rows for large lists.
Forgetting .id_salt when you have two scroll areas at the same level. egui logs a warning at runtime, and the two areas may share scroll state (they should not).
What's next
Next episode: top and bottom panels. Menu bars at the top, status bars at the bottom. Combined with SidePanel and CentralPanel, you get the four-panel-plus-content layout that almost every desktop app uses.
Recap
egui::ScrollArea::vertical().show(ui, |ui| ...) for any vertical list that may overflow. .stick_to_bottom(true) for log-style auto-following. Use .show_rows when the list is too long for naive iteration. Combine with SidePanel or CentralPanel for full-window scrolling.
Next episode: top and bottom panels. See you in the next one.