Scroll Area

Rust/UI component that provides custom scrolling functionality with cross-browser styling.

utils

Tags

v1.2.0-beta.50
v1.2.0-beta.49
v1.2.0-beta.48
v1.2.0-beta.47
v1.2.0-beta.46
v1.2.0-beta.45
v1.2.0-beta.44
v1.2.0-beta.43
v1.2.0-beta.42
v1.2.0-beta.41
v1.2.0-beta.40
v1.2.0-beta.39
v1.2.0-beta.38
v1.2.0-beta.37
v1.2.0-beta.36
v1.2.0-beta.35
v1.2.0-beta.34
v1.2.0-beta.33
v1.2.0-beta.32
v1.2.0-beta.31
v1.2.0-beta.30
v1.2.0-beta.29
v1.2.0-beta.28
v1.2.0-beta.27
v1.2.0-beta.26
v1.2.0-beta.25
v1.2.0-beta.24
v1.2.0-beta.23
v1.2.0-beta.22
v1.2.0-beta.21
v1.2.0-beta.20
v1.2.0-beta.19
v1.2.0-beta.18
v1.2.0-beta.17
v1.2.0-beta.16
v1.2.0-beta.15
v1.2.0-beta.14
v1.2.0-beta.13
v1.2.0-beta.12
v1.2.0-beta.11
v1.2.0-beta.10
v1.2.0-beta.9
v1.2.0-beta.8
v1.2.0-beta.7
v1.2.0-beta.6
v1.2.0-beta.5
v1.2.0-beta.4
v1.2.0-beta.3
v1.2.0-beta.2
v1.2.0-beta.1
v1.2.0-beta.0
use leptos::prelude::*;

use crate::components::ui::scroll_area::ScrollArea;
use crate::components::ui::separator::Separator;

#[component]
pub fn DemoScrollArea() -> impl IntoView {
    let tags = (0..=50).rev().map(|i| format!("v1.2.0-beta.{}", i)).collect::<Vec<_>>();

    view! {
        <ScrollArea class="w-48 h-72 rounded-md border">
            <div class="p-4">
                <h4 class="mb-4 text-sm font-medium leading-none">Tags</h4>
                {tags
                    .into_iter()
                    .map(|tag| {
                        view! {
                            <>
                                <div class="text-sm">{tag}</div>
                                <Separator class="my-2" />
                            </>
                        }
                    })
                    .collect_view()}
            </div>
        </ScrollArea>
    }
}

Installation

You can run either of the following commands:

# cargo install ui-cli --force
ui add demo_scroll_area
ui add scroll_area

Update the imports to match your project setup.

Copy and paste the following code into your project:

components/ui/scroll_area.rs

use leptos::prelude::*;
use leptos_ui::void;
use tw_merge::*;

mod components {
    use super::*;
    void! {ScrollAreaThumb, div, "bg-border relative flex-1 rounded-full"}
    void! {ScrollAreaCorner, div, "bg-border"}
}

pub use components::*;

/* ========================================================== */
/*                     ✨ COMPONENTS ✨                       */
/* ========================================================== */

#[component]
pub fn ScrollArea(children: Children, #[prop(into, optional)] class: String) -> impl IntoView {
    let merged_class = tw_merge!("relative overflow-hidden", class);

    view! {
        <div data-name="ScrollArea" class=merged_class>
            <ScrollAreaViewport>{children()}</ScrollAreaViewport>
            <ScrollBar />
            <ScrollAreaCorner />
        </div>
    }
}

#[component]
pub fn ScrollAreaViewport(children: Children, #[prop(into, optional)] class: String) -> impl IntoView {
    let merged_class = tw_merge!(
        "focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 overflow-auto",
        class
    );

    view! {
        <div data-name="ScrollAreaViewport" class=merged_class>
            {children()}
        </div>
    }
}

/* ========================================================== */
/*                       🧬 ENUMS 🧬                          */
/* ========================================================== */

#[derive(Clone, Copy, Default)]
pub enum ScrollBarOrientation {
    #[default]
    Vertical,
    Horizontal,
}

#[component]
pub fn ScrollBar(
    #[prop(default = ScrollBarOrientation::default())] orientation: ScrollBarOrientation,
    #[prop(into, optional)] class: String,
) -> impl IntoView {
    let orientation_class = match orientation {
        ScrollBarOrientation::Vertical => "h-full w-2.5 border-l border-l-transparent",
        ScrollBarOrientation::Horizontal => "h-2.5 flex-col border-t border-t-transparent",
    };

    let merged_class = tw_merge!("flex touch-none p-px transition-colors select-none", orientation_class, class);

    view! {
        <div data-name="ScrollBar" class=merged_class>
            <ScrollAreaThumb />
        </div>
    }
}

/* ========================================================== */
/*                       🧬 STRUCT 🧬                         */
/* ========================================================== */

#[component]
pub fn SnapScrollArea(
    #[prop(into, default = SnapAreaVariant::default())] variant: SnapAreaVariant,
    #[prop(into, optional)] class: String,
    children: Children,
) -> impl IntoView {
    let snap_item = SnapAreaClass { variant };
    let merged_class = snap_item.with_class(class);

    view! {
        <div data-name="SnapScrollArea" class=merged_class>
            {children()}
        </div>
    }
}

#[derive(TwClass, Default)]
#[tw(class = "")]
pub struct SnapAreaClass {
    variant: SnapAreaVariant,
}

#[derive(TwVariant)]
pub enum SnapAreaVariant {
    // * snap-x by default
    #[tw(default, class = "overflow-x-auto snap-x")]
    Center,
}

/* ========================================================== */
/*                       🧬 STRUCT 🧬                         */
/* ========================================================== */

#[component]
pub fn SnapItem(
    #[prop(into, default = SnapVariant::default())] variant: SnapVariant,
    #[prop(into, optional)] class: String,
    children: Children,
) -> impl IntoView {
    let snap_item = SnapItemClass { variant };
    let merged_class = snap_item.with_class(class);

    view! {
        <div data-name="SnapItem" class=merged_class>
            {children()}
        </div>
    }
}

#[derive(TwClass, Default)]
#[tw(class = "shrink-0")]
pub struct SnapItemClass {
    variant: SnapVariant,
}

#[derive(TwVariant)]
pub enum SnapVariant {
    // * snap-center by default
    #[tw(default, class = "snap-center")]
    Center,
}

Update the imports to match your project setup.

Usage

You can use the ScrollArea component in combination with the Separator component.

use crate::components::ui::scroll_area::ScrollArea;
<ScrollArea class="h-72 w-48 rounded-md border">
    <div class="p-4">
        // Your scrollable content here
    </div>
</ScrollArea>

Examples

Horizontal Scrolling

Horizontal scroll area for displaying wide content with custom scrollbar styling. This example demonstrates how to create horizontal scrolling containers in Leptos with cross-browser consistent scrollbars and smooth scroll behavior in Rust applications.

Image 1
Image 2
Image 3
Image 4
Image 5
use leptos::prelude::*;

use crate::components::ui::scroll_area::ScrollArea;

#[component]
pub fn DemoScrollAreaHorizontal() -> impl IntoView {
    let images = (1..=5).map(|i| format!("Image {}", i)).collect::<Vec<_>>();

    view! {
        <ScrollArea class="w-96 whitespace-nowrap rounded-md border">
            <div class="flex gap-4 p-4 w-max">
                {images
                    .into_iter()
                    .map(|label| {
                        view! {
                            <div class="shrink-0">
                                <div class="overflow-hidden rounded-md">
                                    <div class="flex justify-center items-center text-sm h-[200px] w-[150px] bg-muted text-muted-foreground">
                                        {label}
                                    </div>
                                </div>
                            </div>
                        }
                    })
                    .collect_view()}
            </div>
        </ScrollArea>
    }
}

Scroll Snap

Scroll area with CSS scroll snap for precise item-to-item scrolling. This example shows how to implement snap scrolling in Leptos for creating Card carousel-like experiences and gallery views with smooth snap points in Rust applications.

snap point
Image 1
Image 2
Image 3
Image 4
Image 5
Image 6
use leptos::prelude::*;

use crate::components::ui::scroll_area::{SnapItem, SnapScrollArea};

#[component]
pub fn DemoScrollAreaSnap() -> impl IntoView {
    let images = (1..=6).map(|i| format!("Image {}", i)).collect::<Vec<_>>();

    view! {
        <div class="overflow-hidden relative">
            <div class="flex justify-start items-end pt-10 mb-6 ml-[50%]">
                <div class="px-1.5 ml-2 font-mono leading-6 text-indigo-600 bg-indigo-50 rounded ring-1 ring-inset ring-indigo-600 dark:text-white dark:bg-indigo-500 dark:ring-0 text-[0.625rem] dark:highlight-white/10">
                    snap point
                </div>
                <div class="absolute top-0 bottom-0 left-1/2 border-l border-indigo-500"></div>
            </div>

            <SnapScrollArea class="flex relative gap-6 pb-14 w-full px-[calc(50%-10rem)]">
                {images
                    .into_iter()
                    .map(|label| {
                        view! {
                            <SnapItem>
                                <div class="flex justify-center items-center w-80 h-40 text-sm rounded-md bg-muted text-muted-foreground">
                                    {label}
                                </div>
                            </SnapItem>
                        }
                    })
                    .collect::<Vec<_>>()}
            </SnapScrollArea>
        </div>
    }
}

Get notified when new stuff drops.

Rust/UI Icons - Send