Carousel is a horizontally paged strip that snaps to the nearest page on release — the standard mobile primitive behind shop bundle strips, onboarding pagers and reward rows. Children are pages; drag or flick moves between them, release settles centered on one page with a configurable duration and easing.

It is deliberately not built on ScrollView: the component owns the whole gesture (pointer capture, velocity sampling, settle tween) on a plain container, so Unity's ScrollView elastic-offset clamp bug — the one that makes long touch lists jump near the tail — structurally cannot reach it, and the ends use a controlled rubber-band resistance instead.

Setup

Children of the element are the pages. page-width-percent below 100 leaves the neighbouring pages peeking at the edges:

xml
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:luna="CupkekGames.Luna"> <luna:Carousel name="Offers" page-width-percent="72" settle-duration="0.28" start-index="1"> <ui:VisualElement class="offer-card">...</ui:VisualElement> <ui:VisualElement class="offer-card">...</ui:VisualElement> <ui:VisualElement class="offer-card">...</ui:VisualElement> </luna:Carousel> <luna:CarouselDots carousel-name="Offers" /> </ui:UXML>

The carousel needs a height (USS or inline) — pages stretch to fill it. Page widths are computed by the component in pixels from the carousel's own width, so percent-width circularity never enters the layout. Give the page's content a margin if neighbouring cards should read as separate cards while peeking.

Attributes

AttributeDefaultMeaning
page-width-percent100Page slot width as a percent of the carousel width. Below 100, neighbours peek at the edges.
settle-duration0.25Seconds for the release/SnapTo settle tween. 0 lands instantly.
settle-easingQuadOutEasingType applied to the settle tween.
flick-threshold600Release speed in px/s above which a short drag still advances one page.
start-index0Page centered on first layout.

API

csharp
Carousel carousel = root.Q<Carousel>("Offers"); carousel.CurrentIndex; // settled page index carousel.PageCount; // live child count carousel.SnapTo(2); // animated snap carousel.SnapTo(0, animate: false); carousel.IndexChanged += i => RefreshBuyButton(i); // fires when a settle lands on a new page carousel.PagesChanged += RebuildBadges; // page count or size changed

A press only becomes a drag after 8px of travel, so buttons inside pages (the buy button on an IAP card) receive plain clicks; once the drag threshold is crossed, the carousel captures the pointer and the child press cancels — the standard hand-off.

Indicator dots

CarouselDots renders one dot per page, highlights the current one, and snaps on dot click. Bind by element name in UXML (carousel-name, resolved when the dots attach to the panel) or from code with dots.Bind(carousel). Place it outside the carousel element — inside it, dot presses would be interpreted as page drags.

Retheme via variables (explicit half-size radius, USS has no pill clamp):

css
.my-shop .luna-carousel-dot { --luna-carousel-dot: rgba(255, 255, 255, 0.3); } .my-shop .luna-carousel-dot--active { --luna-carousel-dot-active: rgb(52, 211, 153); }

Classes

ClassElement
.luna-carouselThe root (clips overflow).
.luna-carousel__trackThe moving strip (translate transform, component-driven).
.luna-carousel__pageApplied to every child; carries the load-bearing flex-shrink: 0.
.luna-carousel-dots / .luna-carousel-dot / .luna-carousel-dot--activeThe dots element and its children.

Showcase

The Components showcase ships CarouselShowcase.unity — an IAP-style offer strip with peeked neighbours, dots, and Prev/Next buttons wired to SnapTo. Everything visual there is stock Carousel/CarouselDots.

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