gpuicn Components

Component 42 of 60

Scroll Area

Adds styled scrolling to overflowing content.

site/src/previews/scroll_area.rs

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.

Scroll Area, running in the browser. The second tab shows the preview's own source, which includes its layout and demo state. For the smallest working example, see Usage below. Download the whole showcase for its imports and helpers.

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.