Swiper
npx @vyui/cli add swiperOverview
VySwiper is the styled carousel built on the @vyui/core SwiperRoot and SwiperItem primitives. Slides follow the finger natively, snap to the nearest page on release, and a quick flick advances even before the halfway point. It supports controlled index state, data-driven or manual slides, measured full-width pages, seamless looping, autoplay, axis locking, and opt-in indicator dots.
@vyui/core Swiper primitives, whose gesture is a shared main-thread drag controller. Reach for SwiperRoot directly when you need spacing, alignment, RTL, or custom thresholds.Usage
Bind the active index with v-model, pass an items array, and render each slide through the item slot.
<script setup lang="ts">
import { ref } from 'vue'
import { VySwiper } from '@vyui/kit/swiper'
const current = ref(0)
const slides = [
{ title: 'Explore', description: 'Find something new.' },
{ title: 'Save', description: 'Keep favorites close.' },
{ title: 'Share', description: 'Send them to friends.' },
]
</script>
<template>
<VySwiper v-model="current" :items="slides" show-indicators>
<template #item="{ item, index }">
<view class="h-48 p-6 bg-neutral-100">
<text class="text-xl font-semibold">{{ item.title }}</text>
<text class="mt-2 text-neutral-600">{{ item.description }}</text>
<text class="mt-4 text-sm text-neutral-500">Slide {{ index + 1 }}</text>
</view>
</template>
</VySwiper>
</template>
When itemWidth is omitted, the wrapper measures its container and makes each slide fill the available width (using 300px until the first layout measurement arrives). Set itemWidth explicitly for fixed-width slides.
Loop and autoplay
Set loop to navigate seamlessly across both ends — the edge slides are cloned so motion continues over the seam instead of rewinding. Set autoplay to true for the default 3000ms interval, or pass an interval in milliseconds.
<script setup lang="ts">
import { ref } from 'vue'
import { VySwiper } from '@vyui/kit/swiper'
const current = ref(0)
const promotions = [
{ label: 'Free delivery', color: 'bg-primary-100' },
{ label: 'Member pricing', color: 'bg-success-100' },
{ label: 'New arrivals', color: 'bg-warning-100' },
]
</script>
<template>
<VySwiper
v-model="current"
:items="promotions"
:autoplay="5000"
loop
axis-lock
show-indicators
>
<template #item="{ item }">
<view :class="['h-40 items-center justify-center', item.color]">
<text class="text-xl font-semibold">{{ item.label }}</text>
</view>
</template>
</VySwiper>
</template>
Autoplay pauses while the user is dragging and resumes after the release settles. Without loop, autoplay stops advancing at the final slide.
Manual slides
Omit items and place core SwiperItem children in the default slot. The wrapper derives the item count from the number of top-level slot nodes, so keep the slot to direct SwiperItem children.
<script setup lang="ts">
import { ref } from 'vue'
import { SwiperItem } from '@vyui/core'
import { VySwiper } from '@vyui/kit/swiper'
const current = ref(0)
</script>
<template>
<VySwiper v-model="current" :item-width="320">
<SwiperItem>
<view class="h-40 p-5 bg-primary-100"><text>First slide</text></view>
</SwiperItem>
<SwiperItem>
<view class="h-40 p-5 bg-secondary-100"><text>Second slide</text></view>
</SwiperItem>
</VySwiper>
</template>
Indicators
Indicators are opt-in via showIndicators and appear when there is more than one slide. The active dot is derived from modelValue, so keep it bound. size scales the dots.
<VySwiper v-model="current" :items="slides" size="lg" show-indicators>
<template #item="{ item }">
<view class="h-48 p-6">
<text>{{ item.title }}</text>
</view>
</template>
</VySwiper>
Custom styles
Use class for the root or ui for individual theme slots.
<VySwiper
v-model="current"
:items="slides"
show-indicators
class="rounded-2xl"
:ui="{
item: 'rounded-2xl',
indicators: 'bottom-4',
indicator: 'bg-neutral-400',
indicatorActive: 'bg-error-500',
}"
>
<template #item="{ item }">
<view class="h-48 p-6 bg-neutral-100">
<text>{{ item.title }}</text>
</view>
</template>
</VySwiper>
Features and behavior
v-modelcontrols the zero-based active index; it updates after a completed swipe or autoplay step, and changing it programmatically animates the track to the new slide.- Release snaps to the nearest page: a drag past
0.3of the slide width advances, and a flick of at least300px/sadvances one slide from where the drag started, even on a short drag. - Non-looping swipers clamp at the first and last slide. Looping swipers clone the slide set on both sides of the track so seam crossings are continuous; in loop mode a programmatic index change takes the shortest path, including over the seam.
axisLockclassifies the gesture once it moves ~8px: within 45° of horizontal the swiper consumes it, otherwise the drag is released to the surrounding scroll surface. Use it whenever a swiper sits inside a vertical scroller.itemWidthsets the slide and snap width. When omitted, the measured wrapper width follows layout changes such as device rotation.showIndicatorsrenders the dot strip as a fixed overlay outside the moving track.
direction="vertical" only moves the indicator strip to a vertical layout. Slides, track movement, and gesture recognition remain horizontal.API
These tables are generated directly from the component source.
Props
| Prop | Default | Type |
|---|---|---|
autoplay | false | number | boolean | undefined`true` enables autoplay at a default interval; a number sets the interval in ms. Autoplay pauses while the user is dragging. |
axisLock | false | boolean | undefinedOnly consume predominantly-horizontal gestures, releasing vertical drags to the host scroll surface. Useful for carousels inside a vertical scroll. |
direction | "horizontal" | "vertical" | "horizontal" | undefined |
items | — | any[] | undefinedItems array. When set, the wrapper renders one `SwiperItem` per entry via the `item` slot. When omitted, the `default` slot is rendered as-is and is expected to contain manually-placed `<SwiperItem>` children. |
itemWidth | — | number | undefinedWidth of each item in px. When omitted, the swiper measures its container and uses the available width so slides follow viewport rotation. |
loop | false | boolean | undefinedNavigate circularly: dragging/autoplay past the last item wraps around. |
modelValue | — | number | undefinedControlled active index. Bind with `v-model`. |
showIndicators | false | boolean | undefinedShow the fixed dot indicator strip. Opt-in. |
size | — | "sm" | "md" | "lg" | undefined |
ui | — | Partial<Record<"root" | "item" | "indicator" | "indicators" | "indicatorActive", any>> | undefined |
Emits
| Event | Payload |
|---|---|
update:modelValue | [value: number] |
The kit wrapper does not forward the core swipeStart / swipeEnd events — use SwiperRoot directly when you need drag lifecycle callbacks.
Slots
| Slot | Bindings |
|---|---|
default | {} | undefined |
item | { item: any; index: number; } |
Built on @vyui/core
VySwiper composes the headless SwiperRoot and SwiperItem primitives. Use them directly for the options the kit wrapper does not expose: item spacing (spaceBetween), viewport alignment (align / containerWidth), RTL, offset clamping (offsetLimit), custom snap thresholds and duration, disabled, and the drag lifecycle emits. SwiperRoot also exposes a setIndex(index) method (via template ref) that wraps or clamps and animates to a slide.
<SwiperRoot v-model="current" :item-width="300" :item-count="3" :space-between="12">
<SwiperItem>...</SwiperItem>
<SwiperItem>...</SwiperItem>
<SwiperItem>...</SwiperItem>
<template #overlay>
<!-- fixed content that must not move with the track -->
</template>
</SwiperRoot>
SwiperRoot props
| Prop | Default | Type |
|---|---|---|
align | "start" | "start" | "center" | "end" | undefinedActive-item alignment within the viewport when it is wider than an item. `'start'` (default) | `'center'` | `'end'`. Requires `containerWidth`. Mirrors lynx-ui `modeConfig.align`. |
autoplay | false | boolean | undefinedAuto-advance through items on an interval. Pauses while dragging. |
axisLock | false | boolean | undefinedOnly consume gestures that are predominantly horizontal (±45°). A vertical drag is released to the host scroll surface. Mirrors lynx-ui's `experimentalHorizontalSwipeOnly`. |
circular | false | boolean | undefinedAlias for `loop` — matches lynx-ui's `circular` naming. `loop` wins. |
containerWidth | 0 | number | undefinedViewport width, px — needed for `align: center/end` and the end clamp. |
defaultValue | — | number | undefined |
disabled | false | boolean | undefined |
duration | 300 | number | undefinedSnap animation duration in ms. |
interval | 3000 | number | undefinedAutoplay step interval in ms (time an item is shown before advancing). |
itemCount* | — | number |
itemWidth* | — | number |
loop | false | boolean | undefinedNavigate circularly: dragging/autoplay past the last item wraps to the first (and vice-versa) SEAMLESSLY — edge slides are cloned so motion continues across the seam instead of snap-rewinding. When false, the track clamps at the ends. |
mode | "normal" | "normal" | "carousel" | undefinedLayout mode. `'normal'` (default) is the standard paged carousel honoring `align`. `'carousel'` is an alias kept for forward-compat; treated as normal. Mirrors lynx-ui `mode`. |
modelValue | — | number | undefined |
offsetLimit | — | [number, number] | undefinedExplicit `[startLimit, endLimit]` non-loop offset clamp (px past each edge the track may rest). Mirrors lynx-ui `offsetLimit`. |
rtl | false | boolean | undefinedRight-to-left layout. A forward swipe moves visually rightward. |
spaceBetween | 0 | number | undefinedGap between adjacent items, px. The snap unit becomes `itemWidth + spaceBetween`. Mirrors lynx-ui `modeConfig.spaceBetween`. |
threshold | 0.3 | number | undefinedFraction of the snap unit dragged past which the snap rounds up. |
velocityThreshold | 300 | number | undefinedpx/s flick above which a release advances by one item. |
SwiperRoot emits
| Event | Payload |
|---|---|
update:modelValue | [value: number] |
swipeStart | [] |
swipeEnd | [value: number] |
SwiperItem props
| Prop | Default | Type |
|---|---|---|
width | — | number | undefinedOverride the item width inherited from SwiperRoot. Rare — only set when an item should occupy a different track segment than the snap unit. |
Styling and theming
Override globally through appConfig.ui.swiper or locally with ui.
| UI slot | Purpose |
|---|---|
root | Relative, full-width clipping wrapper. |
item | Classes applied to each generated SwiperItem. |
indicators | Fixed indicator strip positioned over the viewport. |
indicator | Inactive indicator dot. |
indicatorActive | Additional class applied to the active dot. |
The direction variant positions the indicator strip horizontally at the bottom or vertically at the right. The size variant (sm / md / lg) controls dot diameter and spacing. Defaults are horizontal and md.
Accessibility
- The swiper does not add carousel, group, or live-region semantics, and the indicator dots are visual elements, not controls. There is no keyboard navigation — provide separate accessible controls when users must be able to move directly between slides, and avoid placing essential information only in an automatically advancing slide.
- Loop mode renders cloned copies of the slide slot that are not hidden from the accessibility tree. Test looped carousels with VoiceOver and TalkBack before using them for interactive content.
Platform notes
- Drag tracking, velocity sampling, snapping, looping, and autoplay all run in Lynx main-thread worklets, so slides track the finger without a background-thread round trip.
- Touch and mouse are both wired: on-device swipes use touch events, while desktop browsers (Lynx web) drive the same worklets from mouse events — the live preview above works with a mouse. There is no keyboard support.
- The wrapper listens for Lynx
layoutchangeto update full-width slides after its container resizes. - Avoid changing slide identity or count during a gesture, especially in loop mode where the slot is cloned.
Related components
SwipeActionfor revealing row actions with a horizontal gesture.Draggable— the headless pan-gesture primitive family behind vyui's drag surfaces.Tabsfor persistent, directly selectable peer views.