Carousel: scroll-snap rebuild, opt-in autoplay, play/pause, and UI redesign#42484
Open
mdo wants to merge 23 commits into
Open
Carousel: scroll-snap rebuild, opt-in autoplay, play/pause, and UI redesign#42484mdo wants to merge 23 commits into
mdo wants to merge 23 commits into
Conversation
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.
Contributor
|
Nice! I'm liking the design updates and new options. Feedback / Issues I've spotted.
|
- `.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.
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.
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.
- 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
autoplayoption, stops on any user interaction, and ships a discoverable.carousel-control-play-pausebutton (with an indicator progress fill) to satisfy WCAG 2.2.2. Adds anendsoption (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 inheritcurrentcolor(so they work inside.btn-*). Removes thetouchoption (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
Checklist
npm run lint)Live previews
Related issues
Closes #32649