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.
Children of the element are the pages. page-width-percent below 100 leaves the neighbouring pages peeking at the edges:
<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.
| Attribute | Default | Meaning |
|---|---|---|
page-width-percent | 100 | Page slot width as a percent of the carousel width. Below 100, neighbours peek at the edges. |
settle-duration | 0.25 | Seconds for the release/SnapTo settle tween. 0 lands instantly. |
settle-easing | QuadOut | EasingType applied to the settle tween. |
flick-threshold | 600 | Release speed in px/s above which a short drag still advances one page. |
start-index | 0 | Page centered on first layout. |
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 changedA 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.
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):
.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); }| Class | Element |
|---|---|
.luna-carousel | The root (clips overflow). |
.luna-carousel__track | The moving strip (translate transform, component-driven). |
.luna-carousel__page | Applied to every child; carries the load-bearing flex-shrink: 0. |
.luna-carousel-dots / .luna-carousel-dot / .luna-carousel-dot--active | The dots element and its children. |
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)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction