Rebuild ol-carousel on native scroll snapping - #13220
Merged
lokesh merged 9 commits intoAug 14, 2026
Merged
Conversation
Replaces the transform-based track and its hand-rolled gesture and spring physics with a native scroll container plus CSS scroll snapping. Swipe momentum, overscroll, axis locking, and trackpad and wheel scrolling now come from the platform instead of being approximated in JS. Snap points are declared on the items (`start` every Nth, `end` on the last), so the browser owns landing — including the ragged final page, which rests flush against the viewport edge. Removed: pointer/touch handlers, axis lock, velocity smoothing, spring animation, rubber-band, page offset table, nearest-page search, wheel paging, drag-click suppression, and index-based lazy loading. Off-page items are no longer inert; in a scroll container they are legitimately reachable by tab, screen reader, find-in-page and voice control. Still custom, per browser support: scrollend is Safari 26.2 only, so it is feature-detected with a debounced scroll fallback; scrollbar-width is paired with a ::-webkit-scrollbar fallback; item widths stay on ResizeObserver rather than container queries, which would raise the floor to Safari 16 and fail hard rather than degrade. Public API is unchanged. Adds 37 unit tests; jest needed a transform block since .babelrc does not apply to node_modules and lit ships ESM.
Trim the explanatory blocks down to one or two lines each. The rationale for choosing native scrolling belongs in the PR, not repeated inline.
50 tasks
…tor/native-scroll
…tor/native-scroll
Upstream moved the carousel design-docs page from design/carousel.html.jinja to design/components/carousel.html.jinja as part of the design-system docs restructure. Reapplied this branch's description rewrite (spring physics -> native scroll snapping) at the new path. Claude-Session: https://claude.ai/code/session_01VaHTJt7VK8TXfC219ibatv
cdrini
approved these changes
Aug 13, 2026
cdrini
left a comment
Collaborator
There was a problem hiding this comment.
Works like a charm! One ux note is that if i swipe very quickly, it moves past multiple pages. Note sure if we want to prevent that.
… into ol-carousel/refactor/native-scroll
The tablist and per-indicator aria-labels were hardcoded English. Add
label-pages and label-go-to-page attributes with English defaults,
following the same pattern as <ol-pagination>. The template holds
{page}/{total} placeholders so translations can reorder them.
Claude-Session: https://claude.ai/code/session_016RMfy95Q9ritR7KF727ouR
The IntersectionObserver re-implemented Slick's data-lazy swap so the component could render Slick-shaped cards. In a scroll container that is the browser's job: a real src plus loading="lazy" defers off-page covers on its own, because the viewport clips its overflow. Nothing renders data-lazy cards into <ol-carousel> today, and the card template gets rewritten when it does. Document the contract on the component and the design page instead of carrying the shim. Claude-Session: https://claude.ai/code/session_016RMfy95Q9ritR7KF727ouR
Collaborator
|
Now that this is merged, are we going to migrate the existing carousels to it? Is there an issue for that |
Collaborator
Author
|
@RayBB |
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.

Part of #13240 (Epic: UI Modernization — consistency, mobile, accessibility), under the Mobile pass workstream.
This is also a response to @cdrini's review on #13101 — "Is there a reason we're not using the new browser-native CSS scrolling/snapping features?" The answer turned out to be no, so here is the native version.
[refactor] Rebuilds
<ol-carousel>on a native scroll container with CSS scroll snapping, replacing the transform-based track and its hand-rolled gesture and spring physics.Low risk:
<ol-carousel>still has no production consumer — it appears only on/developers/design#carousel. Nothing user-facing changes. The public API (attributes,page/totalPages,next/prev/goToPage,ol-carousel-page-change) is untouched.How it feels different
Swiping is the operating system's, not ours. Momentum, acceleration, overscroll bounce and axis locking now come from the platform. They will match every other scrollable surface on the patron's device, on every device, and stay correct as the OS changes them.
Scrolling never fights the page. Vertical panning was already the browser's; now the horizontal axis is too, so there is no arbitration between them to get wrong.
A plain mouse wheel works. Previously only a two-finger trackpad swipe could move the rail — mouse users had to find the arrows.
Covers load when they are actually near. Lazy loading is driven by real intersection with the scrollport rather than page-index arithmetic.
Scrolling is smooth under load. Native scrolling runs on the compositor, so a rail decoding covers mid-swipe no longer competes with a
requestAnimationFrameanimation on the main thread.Off-page books are reachable. They are no longer
inert. Tabbing to one scrolls it into view, and screen readers, find-in-page and voice control can reach the whole rail.The one deliberate loss: arrow-button travel uses the browser's smooth-scroll easing instead of our spring curve, and mouse click-drag is gone — as it is on native scrollers generally.
Technical
The viewport becomes the scroll container. Snap points are declared on the items —
scroll-snap-align: starton every Nth,endon the last — so the browser owns landing, including the ragged final page.goToPage()callsscrollTo({left})with nobehavior, deferring to CSSscroll-behavior, which means oneprefers-reduced-motionmedia query switches the whole component to instant.Deleted: pointer/touch handlers, axis lock, velocity smoothing, spring animation, rubber-band, the page offset table, nearest-page search, wheel paging, drag-click suppression, index-based lazy loading. 349 JS lines, down from 425 on master (and from 591 on the #13101 branch).
Where browser support still needs custom code, checked against our browserslist:
scrollend— Safari 26.2 onlyscrollfallback carries every iOS 18.x patron. Both paths tested.scrollbar-width— Safari 18.2::-webkit-scrollbarfallback.ResizeObserver(Safari 13.1). Container queries would raise the floor to Safari 16 and fail hard — items lose their width — wherescroll-behavior(15.4) andoverscroll-behavior(16) merely degrade gracefully.::scroll-markerwould delete the whole tablist, but it is Chromium-only. Kept as-is.Jest needed a
transformblock inpackage.json:.babelrcis not applied tonode_modules, andlitships ESM, so no existing test could import a realLitElement.Testing
make lit-components && docker compose restart web, then open/developers/design#carousel.npm run test:js— 37 new unit tests, 528 total passing.Verified in the dev app on the 18-item rail (5 columns → pages of 5/5/5/3): snap points at 0/665/1352/1742, every arbitrary scroll position lands exactly on a page boundary, and the last item's right edge measures 0px from the viewport edge on the final page.
Two things I could not verify locally and would like a second pair of eyes on: arrow-click smooth-scroll feel in Safari, especially interrupting one click with another, and RTL — the scroll math still assumes LTR
scrollLeft. RTL was broken before this PR too, but a scroll container makes it tractable in a way the transform track never was. Happy to take it in a follow-up.Screenshot
Visually identical — the layout, peek, arrows and indicators are unchanged. The entire difference is in how it feels to swipe, which a screenshot cannot show. Worth a minute on a real phone rather than a still image.
Stakeholders
@cdrini @RayBB
Supersedes #13101, which spent ~170 lines approximating axis locking, fling velocity and snap selection — all of which the browser does natively here. Happy to close that one if this direction looks right.