Carousel — <mv-carousel>

Carousel built on native scroll snap: arrows, dots synced via IntersectionObserver, keyboard, looping, autoplay with a progress dot (pauses on hover, focus and reduced motion), multiple slides per view, vertical mode and mouse drag.

CategoryLayout
TypeWeb Component (<mv-carousel>)
Statusstable
Keywordscarousel, slider, scroll-snap, autoplay, gallery, swipe, drag

When to use

Avoid when

Install

node scripts/add.mjs carousel --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["carousel"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/carousel/carousel.js, components/carousel/carousel.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div style="display:grid;gap:2rem;width:100%;max-width:44rem;margin-inline:auto">
  <mv-carousel id="carousel-demo-hero" label="Featured destinations" loop autoplay="5000">
    <article style="position:relative;display:grid;align-content:end;gap:.25rem;aspect-ratio:16/8;padding:1.5rem;border-radius:var(--mv-radius-xl);color:#fff;background:radial-gradient(120% 90% at 85% 10%,#ffd7a8 0,transparent 55%),linear-gradient(135deg,#f0845c,#b14a6d 60%,#5b2a6e)">
      <span style="font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;opacity:.85">Portugal · 4 nights</span>
      <h3 style="margin:0;font-size:1.75rem;letter-spacing:-.02em">Lisbon</h3>
      <p style="margin:0;max-width:26rem;font-size:.875rem;opacity:.9">Yellow trams, miradouros and pastéis de nata at sunset.</p>
    </article>
    <article style="position:relative;display:grid;align-content:end;gap:.25rem;aspect-ratio:16/8;padding:1.5rem;border-radius:var(--mv-radius-xl);color:#fff;background:radial-gradient(110% 80% at 20% 0%,#ffc4d6 0,transparent 55%),linear-gradient(160deg,#e45f7f,#7d3c98 55%,#2c2a6b)">
      <span style="font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;opacity:.85">Japan · 9 nights</span>
      <h3 style="margin:0;font-size:1.75rem;letter-spacing:-.02em">Kyoto</h3>
      <p style="margin:0;max-width:26rem;font-size:.875rem;opacity:.9">Wooden temples, the lanes of Gion and cherry blossoms in spring.</p>
    </article>
    <article style="position:relative;display:grid;align-content:end;gap:.25rem;aspect-ratio:16/8;padding:1.5rem;border-radius:var(--mv-radius-xl);color:#fff;background:radial-gradient(120% 90% at 80% 0%,#9ff3e1 0,transparent 50%),linear-gradient(150deg,#1f9c9a,#1d5f8a 55%,#16294f)">
      <span style="font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;opacity:.85">Iceland · 6 nights</span>
      <h3 style="margin:0;font-size:1.75rem;letter-spacing:-.02em">Reykjavik</h3>
      <p style="margin:0;max-width:26rem;font-size:.875rem;opacity:.9">Northern lights, steaming lagoons and roads at the edge of the world.</p>
    </article>
    <article style="position:relative;display:grid;align-content:end;gap:.25rem;aspect-ratio:16/8;padding:1.5rem;border-radius:var(--mv-radius-xl);color:#fff;background:radial-gradient(120% 90% at 15% 10%,#ffe29a 0,transparent 55%),linear-gradient(140deg,#e89b3c,#c2553a 55%,#6b2b2b)">
      <span style="font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;opacity:.85">Morocco · 5 nights</span>
      <h3 style="margin:0;font-size:1.75rem;letter-spacing:-.02em">Marrakech</h3>
      <p style="margin:0;max-width:26rem;font-size:.875rem;opacity:.9">Fragrant souks, hidden riads and mint tea on the rooftops.</p>
    </article>
  </mv-carousel>

  <mv-carousel id="carousel-demo-cards" label="Recent articles" controls="footer" style="--mv-carousel-per-view:3;--mv-carousel-gap:.75rem">
    <a href="#" style="display:grid;gap:.35rem;padding:1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface);color:inherit;text-decoration:none"><span style="font-size:.75rem;color:var(--mv-accent-fg);font-weight:600">Design</span><strong style="font-size:.9rem;line-height:1.3">Designing a token system that lasts</strong><span style="font-size:.75rem;color:var(--mv-fg-muted)">6 min read</span></a>
    <a href="#" style="display:grid;gap:.35rem;padding:1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface);color:inherit;text-decoration:none"><span style="font-size:.75rem;color:var(--mv-accent-fg);font-weight:600">Accessibility</span><strong style="font-size:.9rem;line-height:1.3">Visible focus, no compromises</strong><span style="font-size:.75rem;color:var(--mv-fg-muted)">4 min read</span></a>
    <a href="#" style="display:grid;gap:.35rem;padding:1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface);color:inherit;text-decoration:none"><span style="font-size:.75rem;color:var(--mv-accent-fg);font-weight:600">CSS</span><strong style="font-size:.9rem;line-height:1.3">Scroll snap: the carousel without JavaScript</strong><span style="font-size:.75rem;color:var(--mv-fg-muted)">8 min read</span></a>
    <a href="#" style="display:grid;gap:.35rem;padding:1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface);color:inherit;text-decoration:none"><span style="font-size:.75rem;color:var(--mv-accent-fg);font-weight:600">Performance</span><strong style="font-size:.9rem;line-height:1.3">Jank-free animation with transform</strong><span style="font-size:.75rem;color:var(--mv-fg-muted)">5 min read</span></a>
    <a href="#" style="display:grid;gap:.35rem;padding:1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface);color:inherit;text-decoration:none"><span style="font-size:.75rem;color:var(--mv-accent-fg);font-weight:600">Web Components</span><strong style="font-size:.9rem;line-height:1.3">Native forms with ElementInternals</strong><span style="font-size:.75rem;color:var(--mv-fg-muted)">7 min read</span></a>
    <a href="#" style="display:grid;gap:.35rem;padding:1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface);color:inherit;text-decoration:none"><span style="font-size:.75rem;color:var(--mv-accent-fg);font-weight:600">Motion</span><strong style="font-size:.9rem;line-height:1.3">Respecting prefers-reduced-motion</strong><span style="font-size:.75rem;color:var(--mv-fg-muted)">3 min read</span></a>
  </mv-carousel>
</div>

API

Attributes

NameTypeDefaultDescription
labelstringAccessible name of the region (aria-label).
loopbooleanWraps back to the start after the last slide (and vice versa).
autoplaynumber (ms) | booleanAutomatic scrolling (5000 ms if empty), with a play/pause button; disabled with reduced motion.
orientationhorizontal | verticalhorizontalScroll direction (vertical: height via --mv-carousel-height).
controlsoverlay | footeroverlayArrows overlaid on the edges or grouped in the footer with the dots.
no-controlsbooleanHides the arrows.
no-dotsbooleanHides the dots.
dragtrue | falsetrueMouse drag (touch uses native scrolling).

Properties

NameTypeDescription
indexnumberActive page (setting it = goTo).
lengthnumberNumber of slides.
pagesnumberNumber of positions (slides − per view + 1).
playingbooleanAutoplay active (read-only).

Methods

NameDescription
next() / prev()Next / previous slide.
goTo(index, { smooth })Goes to a position.
play() / pause()Controls autoplay.

Events

NameDescription
mv-changedetail: { index, slide } when the active position changes.

Content structure

NameDescription
childrenEach child becomes a slide (role=group, “2 of 5”).

CSS classes

NameDescription
mv-carousel-track / -slide / -arrow / -dots / -dot / -playGenerated parts; data-active on the current slide.

CSS variables

NameDefaultDescription
--mv-carousel-per-view1Slides visible at once (integer; can vary per media query).
--mv-carousel-gapvar(--mv-space-4)Space between slides.
--mv-carousel-height20remHeight in vertical orientation.
--mv-carousel-radius0Track radius (clips slides while scrolling).

Accessibility

APG Carousel pattern: labeled region with aria-roledescription=“carousel”, slides are role=group with aria-roledescription=“slide” (“2 of 5”). Arrows and dots are named buttons (aria-current on the active dot). The track is focusable: ←/→ (↑/↓ when vertical), Home/End. Autoplay has a pause button and stops on hover and focus, off screen, in a hidden tab and with reduced motion; the track is aria-live=polite only when it is not auto-scrolling.