Component 42 of 60
Scroll Area
Adds styled scrolling to overflowing content.
Starting the preview. GPUI compiles to WebAssembly, so the first load takes a few seconds. This browser has neither WebGPU nor WebGL 2, so the live preview cannot run. The code on this page still works. The preview did not start.
fn scroll_area_preview(&self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
ScrollArea::new("tags")
.aria_label("Tags")
.w(px(240.))
.h(px(240.))
.children((1..=50).map(|i| {
div()
.px(px(12.))
.py(px(8.))
.child(format!("Version 1.0.{i}"))
}))
}
Installation
After the one-time setup, run the command from your app root. The CLI
writes the modules and keeps src/ui/mod.rs in step.
gpuicn add scroll-area
If you prefer to copy the files by hand, declare them yourself:
// src/ui/mod.rs
pub mod theme;
pub mod scroll_area;
// src/main.rs
mod ui;
Usage
Put this in a view module and return example() from its render method.
use crate::ui::scroll_area::*;
use gpui_kit::{IntoElement, ParentElement, Styled};
fn example() -> impl IntoElement {
ScrollArea::new("tags")
.aria_label("Tags")
.h(gpui_kit::px(200.))
.children((0..50).map(|i| gpui_kit::div().child(format!("Tag {i}"))))
}
API reference
pub fn scroll_area_scrollbar_for<H: ScrollbarHandle + Clone>(
id: impl Into<ElementId>,
target: &H,
cx: &App,
) -> Scrollbar
Overlay on a relative container holding a list or scroll viewport.
pub fn new(id: impl Into<ElementId>) -> Self
Creates a `ScrollArea` with a stable caller-owned ID.
pub fn aria_label(mut self, value: impl Into<SharedString>) -> Self
Sets the accessible name of the control.
pub fn track_scroll(mut self, handle: &ScrollHandle) -> Self
Shares the scroll position with the caller, for example to scroll a focused row into view. Without it the area keeps one per ID.
pub fn axis(mut self, axis: Axis) -> Self
Selects horizontal or vertical layout and keyboard navigation.
Installed source, 166 lines
//! Native scrolling with Nova-styled GPUI Kit scrollbars.
use super::theme::UiTheme;
use gpui_kit::base::{Scrollbar, ScrollbarHandle, ScrollbarMode};
use gpui_kit::{
AnyElement, App, Axis, ElementId, InteractiveElement as _, IntoElement, ParentElement,
RenderOnce, Role, ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled, Window,
div, prelude::FluentBuilder as _, px,
};
/// Overlay on a relative container holding a list or scroll viewport.
pub fn scroll_area_scrollbar_for<H: ScrollbarHandle + Clone>(
id: impl Into<ElementId>,
target: &H,
cx: &App,
) -> Scrollbar {
let t = UiTheme::read(cx);
Scrollbar::vertical(target)
.id(id)
.mode(ScrollbarMode::Scrolling)
.styles(|styles| {
styles
// No rail: the thumb floats over content in every state.
.track(|style| style.width(t.space(2.5)).bg(gpui_kit::transparent_black()))
.track_hover(|style| style.bg(gpui_kit::transparent_black()))
.track_active(|style| {
style
.bg(gpui_kit::transparent_black())
.border_color(gpui_kit::transparent_black())
})
.thumb(|style| {
style
.bg(t.colors.border)
.width(t.space(2.))
.inset(px(1.))
.radius(t.space(1.))
})
.thumb_hover(|style| style.bg(t.colors.muted_foreground.opacity(0.72)))
})
}
#[derive(IntoElement)]
/// A native scroll viewport with a Nova-styled Kit scrollbar.
pub struct ScrollArea {
id: ElementId,
style: gpui_kit::StyleRefinement,
label: Option<SharedString>,
axis: Axis,
scroll: Option<ScrollHandle>,
children: Vec<AnyElement>,
}
impl ScrollArea {
/// Creates a `ScrollArea` with a stable caller-owned ID.
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
style: Default::default(),
label: None,
axis: Axis::Vertical,
scroll: None,
children: vec![],
}
}
/// Sets the accessible name of the control.
pub fn aria_label(mut self, value: impl Into<SharedString>) -> Self {
self.label = Some(value.into());
self
}
/// Shares the scroll position with the caller, for example to scroll a
/// focused row into view. Without it the area keeps one per ID.
pub fn track_scroll(mut self, handle: &ScrollHandle) -> Self {
self.scroll = Some(handle.clone());
self
}
/// Selects horizontal or vertical layout and keyboard navigation.
pub fn axis(mut self, axis: Axis) -> Self {
self.axis = axis;
self
}
}
impl ParentElement for ScrollArea {
fn extend(&mut self, children: impl IntoIterator<Item = AnyElement>) {
self.children.extend(children);
}
}
impl Styled for ScrollArea {
fn style(&mut self) -> &mut gpui_kit::StyleRefinement {
&mut self.style
}
}
impl RenderOnce for ScrollArea {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let t = UiTheme::read(cx).clone();
let handle = match self.scroll.clone() {
Some(handle) => handle,
None => window
.use_keyed_state((self.id.clone(), "scroll"), cx, |_, _| ScrollHandle::new())
.read(cx)
.clone(),
};
let keyboard = handle.clone();
let axis = self.axis;
let viewport = div()
.id("viewport")
.role(Role::ScrollView)
.focusable()
.size_full()
.track_scroll(&handle)
.when(axis == Axis::Vertical, |viewport| {
viewport.overflow_y_scroll()
})
.when(axis == Axis::Horizontal, |viewport| {
viewport.overflow_x_scroll()
})
.when_some(self.label, |viewport, label| viewport.aria_label(label))
.on_key_down(move |event, window, cx| {
if event.keystroke.modifiers.modified() {
return;
}
let mut offset = keyboard.offset();
let max = if axis == Axis::Vertical {
keyboard.max_offset().y
} else {
keyboard.max_offset().x
};
let page = if axis == Axis::Vertical {
keyboard.bounds().size.height
} else {
keyboard.bounds().size.width
};
let value = if axis == Axis::Vertical {
&mut offset.y
} else {
&mut offset.x
};
*value = match event.keystroke.key.as_str() {
"up" if axis == Axis::Vertical => *value + px(32.),
"down" if axis == Axis::Vertical => *value - px(32.),
"left" if axis == Axis::Horizontal => *value + px(32.),
"right" if axis == Axis::Horizontal => *value - px(32.),
"pageup" => *value + page,
"pagedown" => *value - page,
"home" => px(0.),
"end" => -max,
_ => return,
};
*value = (*value).clamp(-max.max(px(0.)), px(0.));
keyboard.set_offset(offset);
window.refresh();
cx.stop_propagation();
})
.child(div().flex().flex_col().min_w_full().children(self.children));
let scrollbar = scroll_area_scrollbar_for("scrollbar", &handle, cx).axis(axis);
let root = div()
.id(self.id)
.relative()
.min_h_0()
.min_w_0()
.font_family(t.fonts.body)
.text_color(t.colors.foreground)
.child(viewport)
.child(scrollbar);
super::theme::apply_style(root, &self.style)
}
}
Platform notes
Visual reference: shadcn/ui 4.19.0, Neutral Nova. Implementation:
crates/ui/registry/scroll_area/scroll_area.rs, using GPUI Kit 0.6.4.
ScrollArea uses a native GPUI scroll viewport and a Kit scrollbar. It supports horizontal or vertical scrolling, wheel input, scrollbar dragging and keyboard arrows, Page Up/Down, Home and End. Content layout and viewport dimensions belong to the caller.
The Rust source and Usage example define the supported API. Browser DOM/CSS behavior, exact exit animations and cross-platform screen-reader parity are not implied by the native component. See migration qualification for the checks completed on this revision.