Scrolling

In LVGL scrolling works very intuitively: if a Widget is outside its parent content area (the size without padding), the parent becomes scrollable and scrollbar(s) will appear. That's it.

Edit on GitHub

Overview

In LVGL scrolling works very intuitively: if a Widget is outside its parent content area (the size without padding), the parent becomes scrollable and scrollbar(s) will appear. No scroll-specific API is required — content that overflows is enough.

Any Widget can be scrollable, including the base widget, lv_image, lv_button, etc. A Widget scrolls either horizontally or vertically in one stroke; diagonal scrolling is not possible.

Scrollbar

Mode

The scrollbar is shown according to the configured mode, set with lv_obj_set_scrollbar_mode(widget, LV_SCROLLBAR_MODE_...):

Styling

The scrollbar is a dedicated part of every scrollable Widget, LV_PART_SCROLLBAR. Add a style to that part to recolor or resize it:

 
static lv_style_t style_red;
lv_style_init(&style_red);
lv_style_set_bg_color(&style_red, lv_color_red());

lv_obj_add_style(widget, &style_red, LV_PART_SCROLLBAR);

A Widget enters the LV_STATE_SCROLLED state while it is being scrolled, so a style added to LV_PART_SCROLLBAR | LV_STATE_SCROLLED takes effect only during scrolling.

pad_left/right/top/bottom set the spacing around the scrollbar, width sets its thickness, and length sets its length. If length is 0 it is sized automatically from the content. The minimum length is fixed to 10; the maximum is the Widget's height or width depending on the scrollbar's orientation.

Right-to-left scrolling

When the base direction is LV_BASE_DIR_RTL, the vertical scrollbar is placed on the left side. base_dir is inherited, so it can be set on the LV_PART_SCROLLBAR part, on the Widget's LV_PART_MAIN, or on any parent.

Scrollable

A Widget can be made non-scrollable by clearing its LV_OBJ_FLAG_SCROLLABLE flag with

lv_obj_remove_flag(widget, LV_OBJ_FLAG_SCROLLABLE)

(XML: scrollable="false"). Overflowing content is then simply clipped. Non-scrollable Widgets can still propagate scrolling to their parents.

The scrollable direction is controlled by lv_obj_set_scroll_dir(widget, LV_DIR_...):

OR-ed values are possible, e.g. LV_DIR_TOP | LV_DIR_LEFT.

Scroll chaining

If a Widget can't be scrolled further, the additional scroll is propagated to its parent and continues up to the Screen. This is called scroll chaining and is controlled by the LV_OBJ_FLAG_SCROLL_CHAIN_HOR/VER flags (XML: scroll_chain). With chaining disabled, propagation stops at the Widget and the parent does not move.

Scroll momentum and elastic scroll

These behaviors only reveal themselves through interaction, so one example toggles them live on a list:

Snapping

Children can be snapped to a position when scrolling ends. Make a child snappable with the LV_OBJ_FLAG_SNAPPABLE flag (XML: snappable), and choose the alignment with

lv_obj_set_scroll_snap_x(widget, LV_SCROLL_SNAP_...)

or lv_obj_set_scroll_snap_y(widget, LV_SCROLL_SNAP_...):

On release, LVGL computes where momentum would end, finds the nearest snap point, and animates to it.

Scroll one

The LV_OBJ_FLAG_SCROLL_ONE flag (XML: scroll_one) limits each gesture to a single snappable child. It requires snappable children and a snap alignment other than LV_SCROLL_SNAP_NONE.

Scroll on focus

When the "scroll on focus" feature is enabled, LVGL automatically scrolls a focused child into view — recursively, so nested scrollable Widgets and even off-page tabview content are handled. This is driven by input-device focus (e.g. pressing "Tab" in a group) rather than by declarative setup.

Floating children

A child with the LV_OBJ_FLAG_FLOATING flag (XML: floating="true") is ignored by the layout and is not moved when its parent scrolls. It stays pinned in place — the typical pattern for a floating action button over a scrollable list.

Scrolling programmatically

Scroll position can be moved from code, with or without animation:

The current position is read with:

Scrolling events

The following events are emitted while scrolling:

Any scroll coordinate can be read from the callback (see Scrolling programmatically) to drive other UI.

Transforming children while scrolling

Because LV_EVENT_SCROLL fires on every position change, the handler can reposition or restyle children relative to the viewport — here each child is projected onto a circle and faded with distance, an effect the declarative style layer cannot express.

Scrolling patterns

Infinite scrolling

Rows are created on demand and far-off rows are deleted as the column is scrolled, so the element count stays bounded no matter how far the user goes.

Endless circular scrolling

When an edge is reached, the boundary child is moved to the opposite end and the scroll position is compensated, so a finite list feels infinite in both directions.

Self size

Self size establishes the size of a Widget's content. For example a table with 10 rows of 50 px has a "self height" of 500 px; if the user sets only 200 px of height, LVGL sees the larger self size and makes the table scrollable. So not only children but also a larger self size can make a Widget scrollable.

Normally this is only relevant when creating a custom widget. LVGL queries it through the LV_EVENT_GET_SELF_SIZE event:

 
if(event_code == LV_EVENT_GET_SELF_SIZE) {
    lv_point_t * p = lv_event_get_param(e);

    /* If x or y < 0 it doesn't need to be calculated now. */
    if(p->x >= 0) p->x = 200; /* self width  */
    if(p->y >= 0) p->y = 50;  /* self height */
}

Last updated on

On this page