Carousel

Free

Auto-playing image carousel with dots, lines, numbers or no indicator, prev/next arrows and a slide counter.

</>sv-carousel carouselslidermedia

Complexity tier: Interactive The primitive bar, plus declared async states, a keyboard model, focus restoration and undo for destructive actions.

import { SvCarouselComponent } from '@styloviz/display';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Looping with number indicator

example.htmlAngular
<sv-carousel
  [slides]="slides"
  [autoPlay]="true"
  [interval]="5000"
  [loop]="true"
  indicator="numbers"
  aspectRatio="16/9"
  (slideChange)="onSlide($event)"
  (pausedChange)="onPause($event)" />

Calls to action

example.htmlAngular
<!-- slides = [
  { id: 'sale', title: 'Summer sale', image: '/img/sale.webp', cta: 'Shop the sale', ctaLink: '/sale' },
  { id: 'news', title: 'Join the newsletter', cta: 'Subscribe' },
] — a CTA with ctaLink is a link; one without is a button you handle. -->
<sv-carousel [slides]="slides" (ctaClick)="onCta($event)" />

When to use

  • Cycling through a small set of images or promotional slides.
  • You want autoplay with arrows and a choice of indicator styles.
  • For browsing many images on demand, use the Image Gallery.

Async states

  • loading
  • empty
  • error
  • retry

Inputs

NameTypeDefaultDescription
loadingbooleanfalseTrue while the slides are being fetched.
errorSvDataError | string | nullnullLoad failure. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`.
emptyMessagestring'No slides to show'Copy for the no-data state.
filteredbooleanfalseTrue when the emptiness is caused by a filter rather than an empty set.
slidesreadonly CarouselSlide[][]Slides to display (image, caption, optional link).
variantCarouselVariant'default'Visual variant. `default` slides full-bleed; `fade` cross-fades in place; `card` insets each slide as a rounded, shadowed card; `coverflow` shows the neighbouring slides scaled down either side of the active one. Under `prefers-reduced-motion` every variant keeps its layout but drops the transition.
indicatorCarouselIndicator'dots'Indicator style: dots, lines, numbers or none.
autoPlaybooleanfalseAuto-advance slides.
intervalnumber4000Auto-advance interval in ms.
loopbooleantrueWrap from last slide back to first.
arrowsbooleantrueShow prev/next arrows.
aspectRatiostring'16/9'CSS aspect-ratio for the viewport.
pauseOnHoverbooleantruePause auto-advance while the carousel is hovered or focused.
ariaLabelstring'Carousel'Accessible name for the carousel region.
prioritybooleanfalseLoad the first slide's image eagerly at high fetch priority. Set it when the carousel is above the fold — a hero image is usually the page's Largest Contentful Paint, and a lazy one is fetched only after layout. Later slides stay lazy either way.

Outputs

EventPayload typeDescription
retryvoidThe host should re-run the failed load.
slideChangenumberEmitted with the active slide index when it changes.
pausedChangebooleanEmitted when the user toggles auto-play; payload is the paused state.
ctaClickCarouselSlideEmitted with the slide whose call-to-action was activated (click, Enter or Space). A slide with `ctaLink` still navigates; this fires first, so it suits analytics. A slide without one has no other effect — handle it here.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.

Template

Bound to the names this component actually exposes.

usage.htmlAngular
<sv-carousel
  [slides]="data.items()"
  [loading]="data.loading()"
  [error]="data.error()"
  [filtered]="data.filtered()"
  (retry)="data.reload()"
/>

Host class

Drive the three async inputs from your own fetch.

host.component.tsTypeScript
// This component reports its state but does not emit a query, so drive
// it from your own fetch and bind the three async inputs.
readonly items = signal<Row[]>([]);
readonly loading = signal(false);
readonly error = signal<SvDataError | null>(null);

async load(): Promise<void> {
  this.loading.set(true);
  this.error.set(null);
  try {
    this.items.set(await this.api.fetchRows());
  } catch (e) {
    this.error.set(toSvDataError(e));
  } finally {
    this.loading.set(false);
  }
}
  • This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `slides`, `loading` and `error`.
  • Set `filtered` when a search or filter is active. It is the difference between "you have no orders" and "no orders match this filter" — telling a customer with 4,000 orders the first one reads as a bug.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • Indicator styles: dots, lines, numbers or none.
  • Autoplay with configurable interval, `pauseOnHover`, and a built-in pause/play button.
  • Custom slide templates.

Best practices

  • Do: Leave `pauseOnHover` on (default) so users can read each slide; a pause/play control also appears during autoplay.
  • Don't: Don't hide important content or CTAs solely inside auto-advancing slides.
  • Tip: Keep slide counts low (3–6); carousels have low engagement past the first slide.

Accessibility

  • Exposed as a `region` with `aria-roledescription="carousel"`; slides are labelled groups and a live region announces changes (silenced while auto-rotating).
  • Auto-play pauses on hover and keyboard focus and can be stopped with the pause button (WCAG 2.2.2); ←/→ navigate slides.
  • Prev/next, indicators and pause are labelled buttons; the active indicator exposes `aria-current`.

Feedback on Carousel

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.