Skip to content

Carousel: scroll-snap rebuild, opt-in autoplay, play/pause, and UI redesign#42484

Open
mdo wants to merge 23 commits into
v6-devfrom
mdo/bootstrap-issue-32649
Open

Carousel: scroll-snap rebuild, opt-in autoplay, play/pause, and UI redesign#42484
mdo wants to merge 23 commits into
v6-devfrom
mdo/bootstrap-issue-32649

Conversation

@mdo

@mdo mdo commented Jun 8, 2026

Copy link
Copy Markdown
Member

Description

Rebuilds the carousel on a native CSS scroll-snap container—JavaScript only handles autoplay, the controls/indicators, and active-slide syncing—unlocking multiple slides per view, peek, gaps, center mode, and variable-width slides via CSS custom properties. Autoplay is now strictly opt-in through a boolean autoplay option, stops on any user interaction, and ships a discoverable .carousel-control-play-pause button (with an indicator progress fill) to satisfy WCAG 2.2.2. Adds an ends option (stop/wrap/loop) and refreshes the layout: controls and indicators stack in the flow by default, move on top of the slides with .carousel-overlay, and use .carousel-icon-* glyphs that inherit currentcolor (so they work inside .btn-*). Removes the touch option (now native to the scroll container) and .carousel-caption.

Motivation & Context

Resolves #32649: the previous carousel autoplayed in confusing, opt-out ways and offered no accessible pause/stop mechanism. This makes autoplay intentional and accessible, and modernizes the component on browser-native scroll-snap with a refreshed UI, in line with the v6 direction.

Type of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Refactoring (non-breaking change)
  • Breaking change (fix or feature that would change existing functionality)

Checklist

  • I have read the contributing guidelines
  • My code follows the code style of the project (using npm run lint)
  • My change introduces changes to the documentation
  • I have updated the documentation accordingly
  • I have added tests to cover my changes
  • All new and existing tests passed

Live previews

Related issues

Closes #32649

mdo added 6 commits June 5, 2026 20:14
Replace the `ride` option with a boolean `autoplay` option (default
`false`), so carousels are static by default and only autoplay when
explicitly opted in with `data-bs-autoplay="true"`.

This removes the confusing `ride="true"` behavior that started
autoplaying only after the first user interaction, and the cryptic
`ride="carousel"` string value. Addresses the first two points of #32649.
Address the WCAG 2.2.2 (Pause, Stop, Hide) concern in #32649.

Behavior: once a user takes control of an autoplaying carousel — clicking
a control or indicator, using the keyboard, or swiping — autoplay stops
for good instead of resuming, respecting their intent. A new runtime
`_playing` flag tracks autoplay intent and gates `_maybeEnableCycle()`.

Control: add a `.carousel-control-play-pause` button as the discoverable
pause/stop mechanism WCAG actually requires (a hover-only pause does not
qualify). It toggles autoplay, reflects state by swapping its icon (via
pause-fill/play-fill CSS mask icons) and `aria-label`, and can also start
autoplay on an otherwise static carousel.

Includes unit tests, SCSS tokens/styles with dark-mode support, docs, and
a migration note.
Replace the float/translateX engine and custom swipe handler with a native
horizontal scroll-snap container. Sliding, touch dragging, momentum, and
keyboard scrolling now come from the browser; JavaScript only layers on
autoplay, the prev/next/indicator controls, and active-slide syncing via an
IntersectionObserver.

This unlocks features the old engine couldn't do, all via CSS custom
properties on `.carousel`:

- Multiple slides per view (`--carousel-items`) with responsive
  `.carousel-items-*` utilities
- Peek of adjacent slides (`--carousel-peek`) and gaps (`--carousel-gap`)
- Variable-width slides (`.carousel-auto`) and center mode (`.carousel-center`)

`.carousel-fade` becomes a stacked-opacity mode that upgrades to a View
Transition where supported. `touch: false` now applies `touch-action: pan-y`.
The markup, public JS API, and slide/slid events are preserved; the
transitional `.carousel-item-{start,end,next,prev}` classes and the
`.carousel.pointer-event` helper are removed.

Includes a full unit-test rewrite, SCSS engine, docs with multi-item/peek/
variable-width examples, and a migration entry.
Rename the per-slide layout tokens for clarity and add new layout options:

- `--carousel-peek` -> `--carousel-items-peek`, `--carousel-gap` ->
  `--carousel-items-gap` (both now carry a length unit so they're safe inside
  the flex-basis `calc()`)
- New `--carousel-gap` for spacing the stacked layout's rows
- New `.carousel-stacked` layout (controls outside the inner viewport, above or
  below) and `.carousel-indicators-dots` round indicator style
- Fix the `.carousel-item` flex-basis so the peek isn't subtracted twice (the
  `padding-inline` already insets the content box)
Replace the `wrap` boolean with an `ends` option taking `stop`, `wrap`
(default), or `loop`:

- `stop` disables the prev/next controls at each end (moving focus to the
  opposite control first, so focus is never lost)
- `wrap` jumps from the last slide back to the first (and vice versa)
- `loop` continues seamlessly into a transient clone of the destination slide,
  then teleports to the real one with no visible backward jump (single-slide
  scroll layouts only; otherwise falls back to `wrap`)

Rework programmatic navigation to scroll the viewport directly with `scrollBy`
instead of `scrollIntoView`, suspending scroll-snap for the duration and
restoring it once the scroll settles. This fixes multi-slide jumps (indicator
clicks, reaching the last slide) that `scroll-snap-stop: always` previously
clamped to one slide, avoids yanking the page to an off-screen carousel, and
works in RTL. Navigation steps are now measured from the live scroll position
so a stale active index can't make a control silently no-op.

Includes unit tests covering the ends modes, the loop transition, end-control
disabling, and the scroll-settle behavior.
Add docs and examples for the new stacked carousels (top, bottom, and with
dot indicators), the `ends` option (wrap/stop/loop) under a new "End behavior"
section, and refresh the options table and intro for the scroll-snap model.
@mdo mdo requested review from a team as code owners June 8, 2026 05:02
@coliff

coliff commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Nice! I'm liking the design updates and new options.

Feedback / Issues I've spotted.

  • Captions - carousel-caption are all overlapping (displaying on all slides).
  • Dark carousel doesn't have dark controls or caption text.
  • I think the Loop carousel should be the default (not wrap). Wrap seems less smooth and I think Loop just makes more sense and what a user would expect (the Bootstrap 5 way).

mdo added 4 commits June 8, 2026 22:18
- `.carousel` is now a flex column by default (controls/indicators sit in the
  flow above or below the slides); overlaying them on top of the slides moves
  to a new `.carousel-overlay` modifier.
- Rename the control-icon classes to `.carousel-icon-{prev,next,pause,play}`,
  painted with `currentcolor` so they inherit the surrounding text/button color
  and work inside `.btn-*`. `.carousel-control-play-pause` is now just a
  behavior hook that shows the matching glyph via `.paused`.
- Redesign indicators as pills: the active one widens, and while autoplaying it
  fills like a progress bar over the slide's interval
  (`carousel-indicator-progress` keyframes driven by `--carousel-interval`).
- Remove `.carousel-caption` and its `--carousel-caption-*` tokens.
- While cycling, toggle a `.carousel-playing` class and expose the wait as
  `--carousel-interval` so CSS can animate the active indicator's progress
  fill. Schedule each tick from the slide being navigated *to*, so per-item
  `data-bs-interval`s don't lag a slide behind.
- Remove the `touch` option: horizontal dragging is part of the native
  scroll-snap container, so there's nothing left for JavaScript to toggle.
- Fade is now a plain CSS opacity crossfade; drop the View Transition path,
  which double-animated against the CSS transition and visibly stuttered.
Switch the docs examples, homepage/cheatsheet examples, and test fixtures to
`.carousel-overlay` + `.carousel-icon-*` and drop `.carousel-caption`. Document
the stacked-by-default layout, the `.carousel-overlay` modifier, the renamed
control-icon classes, the autoplay progress indicator, and the removed `touch`
option and captions in the migration guide.
@mdo mdo changed the title Carousel: opt-in autoplay, play/pause control, and scroll-snap rebuild Carousel: scroll-snap rebuild, opt-in autoplay, play/pause, and UI redesign Jun 9, 2026
mdo added 4 commits June 9, 2026 08:47
Change the default end behavior from `wrap` to `loop`, so carousels scroll
seamlessly past the ends by default (falling back to `wrap` where seamless
looping doesn't apply). Tests that exercise the wrap jump now request it
explicitly.
- Remove the legacy `--carousel-control-*` tokens and the entire
  `carousel-dark-*` token map / `.carousel-dark` block and dark color-mode
  rule; dark styling now rides on `light-dark()` in `.carousel-overlay`.
- Flip RTL prev/next icons with `transform: scaleX(-1)` instead of swapping
  mask images, and drop the dead commented-out overlay rules.
- Drop the now-unused `colors` and `color-mode` Sass imports.
- Docs: lead the end-behavior section with `loop` (the new default), rework
  the dark example onto `.carousel-overlay-controls` + `.btn-*` controls, and
  round the slide corners.
Its last consumer was the carousel's dark color-mode block, now removed in
favor of `light-dark()`, so the flag is no longer referenced anywhere.
@mdo mdo added this to v6.0.0 Jun 9, 2026
@github-project-automation github-project-automation Bot moved this to Inbox in v6.0.0 Jun 9, 2026
mdo added 7 commits June 9, 2026 15:20
Clear the pending autoplay timer in `dispose()` so a queued tick can't fire
after teardown and throw on the now-null `_element`, and remove the
`pointerdown` listener bound to the viewport (`.carousel-inner`)—which
`super.dispose()` doesn't cover, since it only drops listeners on `_element`.
Adds tests for both.
Drop the `--carousel-indicator-active-width` and `--carousel-indicator-active-bg`
tokens, which only carried `null` fallbacks, and inline their values directly on
the active indicator.
Replace the removed `.carousel-control-prev`/`.carousel-control-next` classes
with `.btn-icon btn-sm` buttons across the docs examples, homepage and cheatsheet
examples, and JS test fixtures. Also drop the leftover `.carousel-indicators-dots`
class and the obsolete "Custom transition" docs section, and correct the
peek/gap token names (`--carousel-items-peek`/`-gap`) in the migration guide.
Follow-up to removing the now-unused flag: drop its row from the global options
table and the sentence about disabling dark mode via Sass.
Audit fixes:
- Measure the "current" slide in `to()` from the live scroll position
  (`_navIndex()`) rather than the async `_activeIndex`, so an indicator/control
  used mid-scroll compares against where the viewport actually rests.
- Validate the `ends` option in `_configAfterMerge`, falling back to the
  default (`loop`) for unknown values so navigation and end-control logic agree.
- Strip ids from the entire cloned subtree during a loop transition, not just
  the clone root, to avoid duplicate ids while the clone is on screen.
- Sync the active slide and fire `slid` after a programmatic scroll settles
  when no IntersectionObserver is available.
- Stop autoplay at the last slide under `ends: 'stop'` instead of re-arming a
  timer that can never advance.

Adds tests for each, plus hover-pause and dispose-mid-loop coverage.
The redesigned pill indicators no longer fade via opacity, so the token has no
remaining consumers.
mdo added 2 commits June 9, 2026 20:55
- Document the `ends` option, the removed `.carousel-control-*` / `.carousel-dark`
  classes and v5 tokens, and the removed `$enable-dark-mode` flag in the
  migration guide.
- Clarify the `.active` starting-slide and indicator notes, theme the overlay
  example controls (`.btn-subtle .theme-secondary`), rework the per-item
  interval example with a play/pause control, and sharpen the `slid` event
  description.
- Add a play/pause control to the integration fixture, and fix a corrupted
  custom-property reference in the homepage carousel example CSS.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Inbox

Development

Successfully merging this pull request may close these issues.

2 participants