Read the original article:How to Create an Infinite Horizontal Scroll in HarmonyOS
Requirement Description
How can we implement an infinite horizontal scroll component in HarmonyOS that allows users to swipe left or right indefinitely, with smooth reset logic to create a seamless looping effect.
Background Knowledge
This implementation leverages HarmonyOS' Scroll and Scroller components to create a horizontally scrollable list. The "infinite" effect is achieved by:
- Duplicating the original item set multiple times (5 sets in this example)
- Detecting scroll boundaries and silently resetting the scroll position when getting close to the start or end
- Using a
Scrollerinstance for precise offset control
Implementation Steps
- Initialize the scrollable items with unique IDs, titles, and colors
- Calculate the middle start position for initial scroll offset
- Set up scroll stop detection using onScrollStop of the Scroll component
- Implement reset logic when near the edges of the duplicated content
Code Snippet / Configuration
- Initialize the scrollable items with unique IDs, titles, and colors
initializeItems() {
const originals: ScrollItem[] = [];
for (let i = 0; i < this.originalCount; i++) {
originals.push({
id: i,
title: `Item ${i + 1}`,
color: this.colors[i % this.colors.length]
});
}
this.scrollItems = [];
for (let set = 0; set < this.duplicateSets * 2 + 1; set++) {
for (let i = 0; i < originals.length; i++) {
this.scrollItems.push({
id: set * this.originalCount + i,
title: originals[i].title,
color: originals[i].color,
});
}
}
}
- Calculate the middle start position for initial scroll offset;
aboutToAppear() {
this.initializeItems();
setTimeout(() => {
const startPosition = this.getMiddleStartPosition();
this.scroller.scrollTo({ xOffset: startPosition, yOffset: 0, animation: { duration: 0 } });
}, 100);
}
getMiddleStartPosition(): number {
const setWidth = this.originalCount * (this.itemWidth + this.itemGap);
return setWidth * 2; // Start Exactly at the item 1 of 3rd set. This enables initial left scrolling.
}
- Set up scroll stop detection using onScrollStop of the Scroll component;
.onScrollStop(() => this.handleScrollStop())
- Implement reset logic when near the edges of the duplicated content;
handleScrollStop() {
if (this.isResetting) {
return;
}
const offset = this.scroller.currentOffset().xOffset;
const setWidth = this.originalCount * (this.itemWidth + this.itemGap);
const leftThreshold = setWidth * 2;
const rightThreshold = setWidth * (this.duplicateSets * 2 - 1);
if (offset < leftThreshold || offset > rightThreshold) {
this.isResetting = true;
const newOffset = offset < leftThreshold ? offset + setWidth : offset - setWidth;
this.scroller.scrollTo({ xOffset: newOffset, yOffset: 0, animation: { duration: 0 } });
setTimeout(() => {
this.isResetting = false;
}, 50);
}
}
Full example is below;
interface ScrollItem {
id: number;
title: string;
color: string;
}
@Entry
@Component
struct Index {
@State scrollItems: ScrollItem[] = [];
private scroller: Scroller = new Scroller();
private itemWidth: number = 60;
private itemHeight: number = 60;
private itemGap: number = 8;
private originalCount: number = 10;
private duplicateSets: number = 5;
private isResetting: boolean = false;
private colors: string[] = [
'#ff6b6b', // Coral
'#4ecdc4', // Soft turquoise
'#45aaf2', // Bright sky blue
'#a55eea', // Light purple
'#f7b731', // Golden yellow
'#20bf6b', // Emerald green
'#fd9644', // Orange
'#d1d8e0', // Light gray
'#a5b1c2', // Medium gray
'#8854d0'// Deep purple
];
aboutToAppear() {
this.initializeItems();
setTimeout(() => {
const startPosition = this.getMiddleStartPosition();
this.scroller.scrollTo({ xOffset: startPosition, yOffset: 0, animation: { duration: 0 } });
}, 100);
}
getMiddleStartPosition(): number {
const setWidth = this.originalCount * (this.itemWidth + this.itemGap);
return setWidth * 2; // Start Exactly at the item 1 of 3rd set. This enables initial left scrolling.
}
initializeItems() {
const originals: ScrollItem[] = [];
for (let i = 0; i < this.originalCount; i++) {
originals.push({
id: i,
title: `Item ${i + 1}`,
color: this.colors[i % this.colors.length]
});
}
this.scrollItems = [];
for (let set = 0; set < this.duplicateSets * 2 + 1; set++) {
for (let i = 0; i < originals.length; i++) {
this.scrollItems.push({
id: set * this.originalCount + i,
title: originals[i].title,
color: originals[i].color,
});
}
}
}
handleScrollStop() {
if (this.isResetting) {
return;
}
const offset = this.scroller.currentOffset().xOffset;
const setWidth = this.originalCount * (this.itemWidth + this.itemGap);
const leftThreshold = setWidth * 2;
const rightThreshold = setWidth * (this.duplicateSets * 2 - 1);
if (offset < leftThreshold || offset > rightThreshold) {
this.isResetting = true;
const newOffset = offset < leftThreshold ? offset + setWidth : offset - setWidth;
this.scroller.scrollTo({ xOffset: newOffset, yOffset: 0, animation: { duration: 0 } });
setTimeout(() => {
this.isResetting = false;
}, 50);
}
}
build() {
Column() {
// Main Scroll Area
Column() {
Text('Swipe left or right for infinite loop')
.fontSize(14)
.fontColor(Color.White)
.margin({ bottom: 8 })
.padding({ left: 20, right: 20 })
Scroll(this.scroller) {
Row() {
ForEach(this.scrollItems, (item: ScrollItem) => {
Column() {
Text(item.title)
.fontSize(12)
.fontWeight(FontWeight.Medium)
.fontColor(Color.White)
}
.width(this.itemWidth)
.height(this.itemHeight)
.backgroundColor(item.color)
.borderRadius(16)
.justifyContent(FlexAlign.Center)
.margin({ right: this.itemGap })
}, (item: ScrollItem) => item.id.toString())
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.None)
.onScrollStop(() => this.handleScrollStop())
.width('100%')
.height(this.itemHeight + 15)
.clip(true)
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.backgroundColor(Color.Black)
}
}
Question: Why duplicate the original list 5 times on each side? Can't we use just 1 copy on each side and reset the scroll position to the previous list when getting close to the end?
(Example: Reached item 9 of the last list => Reset position to item 9 of the previous list)
Answer: We need to give enough space to the swipe animation to properly end without reaching the last item. If you can go through the whole list in one fast swipe, you will reach the end before the scroll position properly resets.
Question: Can't we achieve the same infinite scroll effect by constantly removing items from the previous list and adding them to the next list as the user scrolls?
Answer: That approach technically works, but constantly removing and adding whole lists is too costly and causes lags and stutters, especially as the original list get bigger.
Test Results
Limitations or Considerations
- This example supports API Version 18 Release and above.
- This example supports HarmonyOS 5.1.0 Release SDK and above.
- This example requires DevEco Studio 5.1.0 Release and above for compilation and runtime.
Related Documents or Links
HarmonyOS Scroll Component Documentation

Top comments (0)