SwCarousel

SwCarousel is an auto-rotating carousel built on transform: translateX, for banners / hero images. It clones the first and last slides for a seamless loop (when reaching a clone frame it jumps back to the real frame without animation — no visible flicker), with built-in autoplay, touch threshold paging, and dots / thumbnail indicators.

items-based: pass :items data + a #default="{ item, index }" scoped slot to render each slide (the component clones based on items; index is the real logical index).

The #default slot is required: the component doesn't know how to render your item. Passing :items without a #default slot renders blank frames (dev console warns).

Use SwRail for free horizontal scrolling: SwCarousel is "one screen at a time + autoplay"; for "a row of N items that scrolls" use SwRail.

Basic usage

Auto-rotating banner

加载中...
<SwCarousel :items="banners" aria-label="Homepage banner">
  <template #default="{ item }">
    <SwLink :to="item.link"><SwImage :src="item.img" ratio="16/9" /></SwLink>
  </template>
</SwCarousel>

SwCarousel provides no link / text / button of its own — each banner's content (image / title / button / target URL) comes entirely from item data, rendered via the #default="{ item, index }" scoped slot. For clickable banners, pick one of the two shapes below:

  • Image-only banner (whole slide clickable): wrap the whole slide in SwLink (see Basic usage above).
  • Banner with text + button: put the SwLink / SwButton CTA inside the slide, and don't wrap the whole thing in <a> (nesting interactive elements inside a link is invalid HTML).

Image + title + CTA link (all driven by item)

加载中...
<!-- Real banner: image + title + CTA button, link from item.link -->
<SwCarousel :items="banners" aria-label="Homepage banner">
  <template #default="{ item }">
    <div class="banner">
      <SwImage :src="item.img" ratio="12/5" />
      <h3 class="title">{{ item.title }}</h3>
      <SwLink :to="item.link" class="cta">{{ item.cta }}</SwLink>
    </div>
  </template>
</SwCarousel>

Autoplay & pausing

Autoplay is on by default (autoplayDelay controls dwell time per slide in ms, default 4000 — enough time to read banners with text; pass 3000 for image-only). It auto-pauses on desktop hover, keyboard focus, pointer drag, and when the page goes to the background (visibilitychange), resuming on leave / return; it does not autoplay when the system "reduce motion" preference is active. A single slide or :autoplay="false" disables it. Manual navigation (indicators / thumbnails / keyboard / ref methods) resets the autoplay timer — the slide you just picked gets the full autoplayDelay, instead of being swapped away by an imminent tick.

<SwCarousel :items="banners" :autoplay-delay="5000" :pause-on-hover="true" />
<SwCarousel :items="banners" :autoplay="false" />  <!-- autoplay off -->

Loop & single-slide degradation

  • loop (default true): seamless loop. With :loop="false" it stops at the first / last slide without wrapping.
  • Single-slide auto-degradation: when items.length <= 1, no clones, no autoplay, no indicator.

Touch paging

On mobile, a finger swipe beyond dragThreshold (fraction of container width, default 0.15) pages; otherwise it springs back.

Behavior knobs: no loop / initial-index / speed / drag-threshold

加载中...

Indicators (dots / thumbnails / custom)

indicator is 'dots' (default) / 'thumbnails' / 'none'; position is controlled by the unified indicatorPlacement (shared by dots and thumbnails):

placementEffect
bottom-center (default)Bottom center, horizontal
bottom-left / bottom-rightBottom left / right aligned
left / rightLeft / right side, vertical column
outsideBelow the banner in normal flow (not overlaid)

Dot placement: right / bottom-right / outside

加载中...

Dot styling

By default dots are white with a shadow (visible on any background, not tied to the brand color); the active dot becomes an elongated bar with animation (longer when horizontal / taller when vertical). Dots also have an enlarged click hit-area (transparent, doesn't change the visuals) for easier tapping. All styling is customizable via CSS variables:

Customize dots via CSS variables (active bar / brand color / gap)

加载中...
<SwCarousel
  :items="banners"
  style="--sw-carousel-dot-active-color: var(--primary); --sw-carousel-dot-size: 0.625rem"
>
  <template #default="{ item }"><SwImage :src="item.img" ratio="16/9" /></template>
</SwCarousel>

Thumbnail navigation

indicator="thumbnails" + :thumbnails (an image array, length must match items) renders a thumbnail strip. The active thumbnail uses a white box-shadow outline (not a hard border, reflow-safe); inactive ones are dimmed via brightness and brighten on hover. With many thumbnails the strip becomes a horizontal-scroll filmstrip that auto-scrolls the active one into view on change (the mouse wheel also scrolls it).

thumbnailTrigger controls how thumbnails switch slides: 'click' (default, click to switch) / 'hover' (hover switches immediately).

thumbnailLabels supplies each outer thumbnail button with its complete accessible name, for example :thumbnail-labels="movies.map(m => m.title)". The full label remains available when visual titles are truncated or #thumbnail renders custom content. Leading/trailing whitespace is trimmed; omitted, blank or missing entries retain localized ordinal names. Keep labels and thumbnails equal in length and order. A length mismatch warns in development: supplied labels remain usable and missing entries fall back per index. aria-current, keyboard and touch behavior remain unchanged.

Thumbnail navigation (bottom center, click to switch)

加载中...

Thumbnail navigation (hover to switch)

加载中...

Thumbnails render as bare <img> by default (no SwImage, no decryption). To customize how each thumbnail renders — most typically for .bnc encrypted images (broken with a bare img) — use the #thumbnail scoped slot ({ thumb, index, active }) with SwImage; without the slot the behavior is exactly as before:

#thumbnail slot: thumbnails via SwImage (same recipe for .bnc encrypted images)

加载中...
<SwCarousel :items="movies" indicator="thumbnails" :thumbnails="movies.map((m) => m.cover)">
  <template #default="{ item }"><SwImage :src="item.cover" alt="" fill /></template>
  <!-- the bare-img fallback cannot decrypt; SwImage fill fills the thumb button and decrypts .bnc -->
  <template #thumbnail="{ thumb }"><SwImage :src="thumb" alt="" fill /></template>
</SwCarousel>
<!-- Thumbnail nav (vertical, right, hover to switch) -->
<SwCarousel
  :items="list"
  indicator="thumbnails"
  :thumbnails="list.map(x => x.thumb)"
  indicator-placement="right"
  thumbnail-trigger="hover"
>
  <template #default="{ item }"><SwImage :src="item.img" ratio="16/9" /></template>
</SwCarousel>

Custom indicator

For a fully custom indicator (progress bar / counter / titled thumbnails) use the #indicator slot:

#indicator slot: n / total counter

加载中...
<SwCarousel :items="list">
  <template #default="{ item }"><img :src="item.img"></template>
  <template #indicator="{ current, count, goTo }">
    <div class="my-pager">{{ current + 1 }} / {{ count }}</div>
  </template>
</SwCarousel>

CSS variables

Indicator (dots / thumbnails) styling is fully customizable via CSS variables — no need to override internal selectors:

VariableDefaultDescription
--sw-carousel-indicator-z2Indicator z-index (navigation chrome; sibling overlays at 1 won't cover the dots)
--sw-carousel-indicator-offset0.75remDistance from edge (shared by both axes)
--sw-carousel-indicator-inline-offsetfalls back to --sw-carousel-indicator-offsetInline-axis (left / right) distance from edge; use it to align the indicator with a max-width content container instead of the viewport when the carousel is full-bleed. Not applicable to bottom-center (inline-axis centered). The thumbnail strip's max-width derives from the same fallback chain (100% − offset×2)
--sw-carousel-dot-size0.5remDot size
--sw-carousel-dot-active-size1.25remActive dot "bar" size (longer when horizontal / taller when vertical)
--sw-carousel-dot-gap0.5remGap between dots
--sw-carousel-dot-colorwhite rgb(255 255 255)Dot color
--sw-carousel-dot-active-colorwhiteActive dot color
--sw-carousel-dot-shadow0 0 0.125rem rgb(0 0 0 / 35%)Dot shadow (visible on white too)
--sw-carousel-thumb-width3.5remThumbnail width
--sw-carousel-thumb-height2.25remThumbnail height
--sw-carousel-thumb-gap0.5remGap between thumbnails
--sw-carousel-thumb-radius0.5remThumbnail corner radius
--sw-carousel-thumb-dim0.5Inactive thumbnail dim brightness (brightness)
--sw-carousel-thumb-hover0.8Thumbnail hover brightness (brightness)
--sw-carousel-thumb-active-shadow0 0 0 2px rgb(255 255 255 / 90%)Active thumbnail outline (box-shadow, reflow-safe)

Imperative control

<script setup>
const carouselRef = ref()
</script>

<template>
  <SwCarousel ref="carouselRef" :items="list"><template #default="{ item }">…</template></SwCarousel>
  <button @click="carouselRef.next()">Next</button>
</template>

Multiple slides per view (slidesPerView)

The classic "N cards per row, auto-rotating" recommendation-row shape — slidesPerView sets how many slides fit per view (responsive via semantic breakpoints), gap sets spacing; stepping is always one slide, and loop / autoplay / dragging / dots all keep working. Complements SwRail (free scrolling, no loop/autoplay): pick this component for auto-rotation, SwRail for free scrolling.

Multi per view: 3 desktop / 2 tablet / 1 mobile + gap + autoplay

加载中...

Props

NameTypeRequiredDefaultDescription
itemsT[]✅—Carousel data source
loopbooleantrueSeamless loop
autoplaybooleantrueAuto-rotation
autoplayDelaynumber4000Dwell time per slide (ms) — enough to read text banners; pass 3000 for image-only
initialIndexnumber0Initial logical index
speednumber300Transition duration (ms)
pauseOnHoverbooleantruePause while hovered or keyboard-focused (same switch, same semantics)
dragThresholdnumber0.15Swipe paging threshold (fraction of container width)
indicator'dots' | 'thumbnails' | 'none''dots'Indicator type
indicatorPlacement'bottom-center' | 'bottom-left' | 'bottom-right' | 'left' | 'right' | 'outside''bottom-center'Indicator placement (shared by dots / thumbnails; left / right = vertical)
thumbnailsstring[]undefinedThumbnail strip images when indicator='thumbnails' (length must match items)
thumbnailLabelsstring[]undefinedFull button names aligned with thumbnails; trimmed; omitted, blank or missing entries fall back to localized ordinals
thumbnailTrigger'click' | 'hover''click'How thumbnails switch slides ('hover' = switch on hover)
ariaLabelstringundefinedOnly when set, outputs role="region" + aria-label
trackCarouselTrackPropundefinedBuilt-in per-slide exposure tracking (opt-in; no prop, no behavior) — see "Per-slide exposure tracking" below
slidesPerViewnumber | { mobile?, tablet?, desktop? }1Slides per view (#220): a number applies to all breakpoints; an object uses semantic breakpoints (mobile <768 / tablet 768–1023 / desktop ≥1024, missing tiers fall back to the nearest smaller one). With multiple slides the carousel still steps one slide at a time, loop clones max(spv) slides at each end, dots count = items count (each dot = leftmost slide), and change carries the leftmost logical index. Not reactive after initialization (changing this prop after mount has no effect; breakpoint-tier switching is reactive)
gapstring'0px'Gap between slides (CSS length like '1rem'), for use with slidesPerView. Not reactive after initialization (same as slidesPerView)

Per-slide exposure tracking (:track)

A carousel's exposure semantics differ from SwLink's "entered viewport once": under loop / autoplay, non-active slides are translated out of the visible area, so IntersectionObserver on a slot-level SwLink is unreliable — only the carousel engine knows "which slide is showing", hence the component-level :track:

<SwCarousel :items="banners" :track="{
  exposure: (item, index) => ({
    event: 'appPageitemExposure',
    payload: {
      item_id: (item as Banner).id, item_type: 'banner', item_name: (item as Banner).title,
      parent_item_id: '', position: index + 1, page_key: 'home', page_name: 'Home',
    },
  }),
}">
  <template #default="{ item }">
    <SwLink :to="item.link"><SwImage :src="item.img" ratio="16/9" /></SwLink>
  </template>
</SwCarousel>
  • Timing: each slide reports when it becomes current and the container has entered the viewport (one-shot IO ≥50%); each slide reports at most once per component instance (autoplay loops don't re-report); replacing items (new content) resets the dedupe set.
  • exposure(item, index) maps a carousel item to a tracking spec; returning undefined skips that slide.
  • Click tracking is not built in — slide content is a business slot; click semantics belong to the SwLink :track.click on the content (see example above).
  • Reporting goes through useTracker (silent no-op when the tracker gate is off).

Events

NamePayloadDescription
changeindex: numberCurrent slide changed (real logical index)

Slots

NameScopeDescription
default{ item, index, active }Slide content (index is the real logical index). Required — missing it renders blank frames and warns in dev
thumbnail{ thumb, index, active }Custom content for each thumbnail (e.g. SwImage for .bnc encrypted images); falls back to a bare <img> when omitted. Slot content must bring its own sizing (SwImage fill and bare img both fill the thumb button)
indicator{ current, count, goTo }Custom indicator (for non-standard cases: progress bar / counter / titled thumbnails); replacing the whole strip loses the built-in wheel scrolling / scroll-into-view chrome — use #thumbnail if you only need custom thumbnail rendering

Exposed methods (defineExpose)

MemberSignatureDescription
next / prev() => voidNext / previous slide (seamless under loop)
goTo(index: number, opts?: { immediate?: boolean }) => voidJump to slide N (immediate = no animation)
play / pause() => voidResume / pause autoplay
currentReadonly<Ref<number>>Current real logical index

Notes

  • items-based + required #default: render each slide via the #default="{ item, index }" scoped slot; index is the real logical index (clone frames also map back to the real index). Passing :items without a #default slot renders blank frames and warns in the dev console.
  • Banner links come from item data: the component has no built-in link; wrap the whole slide in SwLink for image-only banners, or put the CTA (SwLink / SwButton) inside the slide for banners with buttons — don't wrap the whole thing in <a>.
  • Muted media for autoplay: if a slide contains a video, autoplay scenarios follow browser policy (muted).
  • Use SwRail for horizontal scrolling: SwCarousel is a one-screen-at-a-time carousel, not a free rail.

active is true only for the current real slide, never for loop clones. autoplay is reactive: disable it while video prepares/plays, then enable it after poster fallback to restart the full image delay. See SwVideoBackground.