Skip to content

Commit b41d8d3

Browse files
authored
feat: carousel loop (#327)
* feat: draft carousel looping on web * Fix page change through loop * wip * Finish web implementation * Remove unused code * Fix flicker * Update mobile and tests/docs * Simplify clone count calculation * Use callback for onGoNext/Prev * Drop gap from clone calculations * Simplify logic for looping * Simplify cloning * Fix storybook * Support isVisible * Bump version * Return item offsets early and simplify isLoopingActive logic
1 parent 3d4d559 commit b41d8d3

File tree

22 files changed

+1478
-145
lines changed

22 files changed

+1478
-145
lines changed

apps/docs/docs/components/layout/Carousel/_mobileExamples.mdx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,78 @@ function OverflowCarousel() {
369369
}
370370
```
371371

372+
## Looping
373+
374+
Enable infinite looping with the `loop` prop. You must have at least two pages of content to enable looping.
375+
376+
Looping works with both `snap` and `free` drag modes, and with both `page` and `item` snap modes.
377+
378+
### Looping with Snap
379+
380+
When looping is enabled with snap drag mode, the carousel will snap to the nearest item or page (depending on `snapMode`) after releasing, allowing infinite scrolling in either direction.
381+
382+
```jsx
383+
function LoopingSnapCarousel() {
384+
const theme = useTheme();
385+
const windowWidth = Dimensions.get('window').width;
386+
387+
const horizontalPadding = theme.space[2];
388+
const carouselWidth = windowWidth - horizontalPadding * 2;
389+
const horizontalGap = theme.space[1];
390+
391+
return (
392+
<Carousel
393+
loop
394+
title="Infinite Scroll"
395+
styles={{
396+
root: { paddingHorizontal: horizontalPadding },
397+
carousel: { gap: horizontalGap },
398+
}}
399+
>
400+
{Object.values(assets).map((asset) => (
401+
<CarouselItem key={asset.symbol} id={asset.symbol} accessibilityLabel={asset.name}>
402+
<SquareAssetCard imageUrl={asset.imageUrl} name={asset.symbol} />
403+
</CarouselItem>
404+
))}
405+
</Carousel>
406+
);
407+
}
408+
```
409+
410+
### Looping with Free Drag
411+
412+
When looping with free drag, the carousel scrolls continuously without snapping, creating a smooth endless scrolling experience.
413+
414+
```jsx
415+
function LoopingFreeDragCarousel() {
416+
const theme = useTheme();
417+
const windowWidth = Dimensions.get('window').width;
418+
419+
const horizontalPadding = theme.space[2];
420+
const carouselWidth = windowWidth - horizontalPadding * 2;
421+
const horizontalGap = theme.space[1];
422+
423+
return (
424+
<Carousel
425+
loop
426+
drag="free"
427+
snapMode="item"
428+
title="Free Scroll Loop"
429+
styles={{
430+
root: { paddingHorizontal: horizontalPadding },
431+
carousel: { gap: horizontalGap },
432+
}}
433+
>
434+
{Object.values(assets).map((asset) => (
435+
<CarouselItem key={asset.symbol} id={asset.symbol} accessibilityLabel={asset.name}>
436+
<SquareAssetCard imageUrl={asset.imageUrl} name={asset.symbol} />
437+
</CarouselItem>
438+
))}
439+
</Carousel>
440+
);
441+
}
442+
```
443+
372444
## Accessibility
373445

374446
The carousel is accessible by default.

apps/docs/docs/components/layout/Carousel/_webExamples.mdx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,96 @@ function OverflowCarousel() {
623623
}
624624
```
625625

626+
## Looping
627+
628+
Enable infinite looping with the `loop` prop. You must have at least two pages of content to enable looping.
629+
630+
Looping works with both `snap` and `free` drag modes, and with both `page` and `item` snap modes.
631+
632+
### Looping with Snap
633+
634+
When looping is enabled with snap drag mode, the carousel will snap to the nearest item or page (depending on `snapMode`) after releasing, allowing infinite scrolling in either direction.
635+
636+
```jsx live
637+
function LoopingSnapCarousel() {
638+
function SquareAssetCard({ imageUrl, name }) {
639+
return (
640+
<ContainedAssetCard
641+
description={
642+
<Text as="p" font="label2" color="fgPositive" numberOfLines={2}>
643+
6.37%
644+
</Text>
645+
}
646+
header={<RemoteImage height="32px" source={imageUrl} width="32px" />}
647+
subtitle={name}
648+
title="$0.87"
649+
/>
650+
);
651+
}
652+
return (
653+
<Box marginX={-3}>
654+
<Carousel
655+
loop
656+
title="Infinite Scroll"
657+
styles={{
658+
root: { paddingInline: 'var(--space-3)' },
659+
carousel: { gap: 'var(--space-1)' },
660+
}}
661+
>
662+
{Object.values(assets).map((asset) => (
663+
<CarouselItem key={asset.symbol} id={asset.symbol} accessibilityLabel={asset.name}>
664+
<SquareAssetCard imageUrl={asset.imageUrl} name={asset.symbol} />
665+
</CarouselItem>
666+
))}
667+
</Carousel>
668+
</Box>
669+
);
670+
}
671+
```
672+
673+
### Looping with Free Drag
674+
675+
When looping with free drag, the carousel scrolls continuously without snapping, creating a smooth endless scrolling experience.
676+
677+
```jsx live
678+
function LoopingFreeDragCarousel() {
679+
function SquareAssetCard({ imageUrl, name }) {
680+
return (
681+
<ContainedAssetCard
682+
description={
683+
<Text as="p" font="label2" color="fgPositive" numberOfLines={2}>
684+
6.37%
685+
</Text>
686+
}
687+
header={<RemoteImage height="32px" source={imageUrl} width="32px" />}
688+
subtitle={name}
689+
title="$0.87"
690+
/>
691+
);
692+
}
693+
return (
694+
<Box marginX={-3}>
695+
<Carousel
696+
loop
697+
drag="free"
698+
snapMode="item"
699+
title="Free Scroll Loop"
700+
styles={{
701+
root: { paddingInline: 'var(--space-3)' },
702+
carousel: { gap: 'var(--space-1)' },
703+
}}
704+
>
705+
{Object.values(assets).map((asset) => (
706+
<CarouselItem key={asset.symbol} id={asset.symbol} accessibilityLabel={asset.name}>
707+
<SquareAssetCard imageUrl={asset.imageUrl} name={asset.symbol} />
708+
</CarouselItem>
709+
))}
710+
</Carousel>
711+
</Box>
712+
);
713+
}
714+
```
715+
626716
## Accessibility
627717

628718
The carousel is accessible by default.

packages/common/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
88

99
<!-- template-start -->
1010

11+
## 8.39.0 ((1/27/2026, 11:17 AM PST))
12+
13+
This is an artificial version bump with no new change.
14+
1115
## 8.38.7 ((1/26/2026, 10:28 AM PST))
1216

1317
This is an artificial version bump with no new change.

packages/common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coinbase/cds-common",
3-
"version": "8.38.7",
3+
"version": "8.39.0",
44
"description": "Coinbase Design System - Common",
55
"repository": {
66
"type": "git",

packages/mcp-server/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
88

99
<!-- template-start -->
1010

11+
## 8.39.0 ((1/27/2026, 11:17 AM PST))
12+
13+
This is an artificial version bump with no new change.
14+
1115
## 8.38.7 ((1/26/2026, 10:28 AM PST))
1216

1317
This is an artificial version bump with no new change.

packages/mcp-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coinbase/cds-mcp-server",
3-
"version": "8.38.7",
3+
"version": "8.39.0",
44
"description": "Coinbase Design System - MCP Server",
55
"repository": {
66
"type": "git",

packages/mobile/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.
88

99
<!-- template-start -->
1010

11+
## 8.39.0 (1/27/2026 PST)
12+
13+
#### 🚀 Updates
14+
15+
- Support Carousel looping. [[#327](https://github.com/coinbase/cds/pull/327)]
16+
1117
## 8.38.7 (1/26/2026 PST)
1218

1319
#### 🐞 Fixes

packages/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coinbase/cds-mobile",
3-
"version": "8.38.7",
3+
"version": "8.39.0",
44
"description": "Coinbase Design System - Mobile",
55
"repository": {
66
"type": "git",

0 commit comments

Comments
 (0)