LunaScroller is a game-grade scroll container: drag past a threshold, flick into a momentum glide with tunable friction, rubber-banded ends with spring-back, mouse wheel, an optional styled scrollbar, and scroll-into-view on gamepad/keyboard focus change. It shares its gesture core with Carousel, so a horizontal offer strip composes inside a vertical scroller through axis-lock arbitration: whichever axis dominates at the drag threshold owns the gesture, and a lost gesture stays lost for that pointer.

Like the Carousel, it is deliberately not built on Unity's ScrollView — the component owns the whole gesture on a plain container, so the ScrollView elastic-offset clamp bug (long touch lists jumping near the tail) structurally cannot occur.

Setup

Children become the scrolled content. The scroller needs a size from USS or layout; content overflows it and is clipped:

xml
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:luna="CupkekGames.Luna"> <luna:LunaScroller name="Shop" style="flex-grow: 1;"> <ui:Label text="DAILY OFFERS" /> <luna:Carousel name="Offers" page-width-percent="64">...</luna:Carousel> <ui:VisualElement name="RewardList">...</ui:VisualElement> </luna:LunaScroller> </ui:UXML>

Attributes

AttributeDefaultMeaning
axisVerticalScroll direction. Horizontal lays content out in a row.
friction4Per-second exponential decay of the glide velocity. Higher stops sooner.
wheel-speed60Pixels scrolled per mouse-wheel delta unit.
show-scrollbartrueStyled thumb; auto-hidden when the content fits.
scroll-into-viewtrueScroll a newly focused descendant into view (deferred one frame so nav transitions land first).
spring-duration0.2Settle time for spring-back, ScrollTo, and track clicks.

API

csharp
LunaScroller scroller = root.Q<LunaScroller>("Shop"); scroller.ScrollOffset; // px scrolled, 0 at the start edge (set = instant) scroller.MaxScroll; // content size minus viewport scroller.ScrollTo(0f); // default motion: spring-duration + ease-out scroller.ScrollTo(0f, 0.45f, EasingType.QuadInOut); // explicit duration + easing; duration 0 = instant scroller.ScrollTo(element); // scroll-into-view; same overloads, no-op when visible scroller.ScrollChanged += px => UpdateParallax(px);

The scrollbar thumb is draggable and the track click-jumps with a settle. Presses inside the content only become drags after 8px of travel, so buttons keep their plain clicks — past the threshold the scroller captures the pointer and the child press cancels.

Theming

css
.my-shop .luna-scroller__thumb { --luna-scroller-thumb: rgba(255, 255, 255, 0.25); } .my-shop .luna-scroller__thumb:hover { --luna-scroller-thumb-hover: rgba(255, 255, 255, 0.5); }
ClassElement
.luna-scroller / .luna-scroller--horizontalRoot (clips overflow).
.luna-scroller__contentThe moving content (translate transform; carries the load-bearing flex-shrink: 0).
.luna-scroller__bar / .luna-scroller__thumbScrollbar track and thumb.

LunaListView

LunaListView is a fixed-height virtualized list built on the scroller: rows outside the viewport (plus a 2-row overscan band) are recycled through a pool, so a 10,000-entry inventory costs a screenful of elements. It inherits every scroller behavior above — glide, rubber-band, scrollbar, wheel, axis-lock — and adds the list contract:

csharp
LunaListView list = new LunaListView(); list.FixedItemHeight = 64; list.MakeItem = () => new RewardRow(); list.BindItem = (element, index) => ((RewardRow)element).Bind(_rewards[index]); list.UnbindItem = (element, index) => ((RewardRow)element).Clear(); // optional list.ItemsSource = _rewards; // any IList list.RefreshItems(); // re-bind visible rows after data mutates list.Rebuild(); // full reset — also drops the row pool (row shape may have changed)

Rows are absolute-positioned and carry .luna-listview__row.

Single selection ships with the Unity-compatible event signature, so handlers written for ListView.selectionChanged plug straight in:

csharp
list.SelectionType = SelectionType.Single; // default None; Multiple warns and falls back list.SelectionChanged += items => Show(items.FirstOrDefault()); list.SelectedIndex = 3; // -1 clears; out-of-range clears object current = list.SelectedItem; list.SetSelectionWithoutNotify(3); list.ScrollToIndex(40); // minimal scroll into view; no-op when visible list.ScrollToIndex(400, ScrollAlignment.Center, 0.42f); // "jump to my rank": centred with neighbours

ScrollAlignment is Auto (minimal, the default), Start, Center, or End — explicit alignments always scroll, so a "jump to me" button works even when the row is already on screen. Animated jumps longer than two viewports teleport most of the distance and ease only the tail: it still reads as a scroll, but the virtualizer binds ~a screen of rows instead of every row in between.

Clicking a row selects it; clicking the selected row notifies again (toggle-deselect patterns rely on that), while setting SelectedIndex to its current value stays silent. A drag never selects — past the 8px threshold the scroller owns the pointer. Assigning a new ItemsSource clears the selection silently. The selected row carries .luna-listview__row--selected, themed via --luna-listview-selected.

It is deliberately not Unity's editor ListView: no multi-select, reorder, tree view, or dynamic heights — those arrive demand-driven (drag-reorder is next in the queue). Vertical only. GridViewList renders through it.

Showcase

The Components showcase ships ScrollerShowcase.unity — a shop-style page with a horizontal Carousel offer strip inside the vertical scroller (the axis-lock composition), a reward track long enough to flick through, the offset readout, and a ScrollTo button.

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL