Carousel
The Carousel component implements a single row of content metadata tiles in a Vega app's UI screen. Carousels are typically used in home screen grids, in More like this or Related content rows, among other use cases.
Version 3.0.0 updates
The following improvements have been made from the previous version of Carousel:
- Added a contract to the Carousel between the JavaScript and native sides to give the Carousel complete control over how many item frames it contains and the lifecycle of those items. This fix issues with recycling that were present in Carousel 2.0.0.
- Removed layout props like
getItemLayoutandlayoutIdbecause the Carousel now determines the layout automatically. - Expanded the list of props to allow for greater customization of the Carousel.
- Added a new callback prop to notify you whenever the current selected item on the Carousel changes.
- The library is now app bundled rather than system bundled (see the following section for more details).
App bundled
This version of Carousel is app bundled, which requires an npm update before releases to make sure you have the latest version. App bundling gives you more control of when you bring in new Carousel changes to your app.
App bundling increases the size of your app VPKG because the library is now bundled with the app. Expect an increase the size of your app bundle by approximately 300K for release VPKGs and 500K for debug VPKGs.
Usage
This version of Carousel is included in @amzn/kepler-ui-components ^3.0.0. Check your package.json to confirm the version is set as shown below.
"dependencies": {
// ...
"@amzn/kepler-ui-components": "^3.0.0"
}
Import
import { Carousel } from '@amzn/kepler-ui-components';
Examples
Horizontal Carousel
import React, {memo} from 'react';
import {Image, Pressable, View} from 'react-native';
import {
Carousel,
CarouselRenderInfo,
} from '@amzn/kepler-ui-components';
import {useState} from 'react';
import {ItemType, ScrollableProps} from './Types';
import {CAROUSEL_STYLE} from './Style';
export const HorizontalScrollable = ({data}: ScrollableProps) => {
function ItemView({item}: CarouselRenderInfo<ItemType>) {
const [focus, setFocus] = useState<boolean>(false);
const onFocusHandler = () => {
setFocus(true);
};
const onBlurHandler = () => {
setFocus(false);
};
return (
<Pressable
style={[
CAROUSEL_STYLE.itemContainer,
focus && CAROUSEL_STYLE.itemFocusContainer,
]}
onFocus={onFocusHandler}
onBlur={onBlurHandler}>
<Image style={CAROUSEL_STYLE.imageContainer} source={{uri: item.url}} />
</Pressable>
);
}
const renderItemHandler = ({item, index}: CarouselRenderInfo<ItemType>) => {
return <ItemView index={index} item={item} />;
};
const getItemKeyHandler **=** (info: CarouselRenderInfo) **=>**
`${info.index}-${info.item.url}`;
const getItem = useCallback((index: number) => {
console.info('getItem called for index:', index);
if (index >= 0 && index < data.length) {
return data[index];
}
return undefined;
}, [data]);
const getItemCount = useCallback(() => {
const count = data.length;
console.info('getItemCount called, returning:', count);
return count;
}, [data]);
const notifyDataError = (error: any) => {
return false; // Don't retry
};
return (
<View style={CAROUSEL_STYLE.container}>
<Carousel
orientation={'horizontal'}
containerStyle={CAROUSEL_STYLE.horizontalCarouselContainerStyle}
itemPadding={20}
renderItem={renderItemHandler}
getItemKey={getItemKeyHandler}
getItem={getItem}
getItemCount={getItemCount}
notifyDataError={notifyDataError}
hasPreferredFocus
hideItemsBeforeSelection={false}
numOffsetItems={2}
selectionStrategy={'anchored'}
renderedItemsCount={10}
navigableScrollAreaMargin={100}
initialStartIndex={0}
trapSelectionOnOrientation={false}
selectionBorder={{
borderColor: '#FFFFFF',
borderWidth: 2,
borderRadius: 0,
borderStrokeRadius: 0
}}
/>
</View>
);
};
export const HorizontalMemoScrollable = memo(HorizontalScrollable);
Vertical Carousel
import {Image, Pressable, View} from 'react-native';
import {
Carousel,
CarouselRenderInfo,
} from '@amzn/kepler-ui-components';
import {memo, useState} from 'react';
import React from 'react';
import {ItemType, ScrollableProps} from './Types';
import {CAROUSEL_STYLE} from './Style';
export const VerticalScrollable = ({data}: ScrollableProps) => {
function ItemView({item}: CarouselRenderInfo<ItemType>) {
const [focus, setFocus] = useState<boolean>(false);
const onFocusHandler = () => {
setFocus(true);
};
const onBlurHandler = () => {
setFocus(false);
};
return (
<Pressable
style={[
CAROUSEL_STYLE.itemContainer,
focus && CAROUSEL_STYLE.itemFocusContainer,
]}
onFocus={onFocusHandler}
onBlur={onBlurHandler}>
<Image style={CAROUSEL_STYLE.imageContainer} source={{uri: item.url}} />
</Pressable>
);
}
const renderItemHandler = ({item, index}: CarouselRenderInfo<ItemType>) => {
return <ItemView index={index} item={item} />;
};
const getItemKeyHandler = (info: CarouselRenderInfo) =>
`${info.index}-${info.item.url}`;
const getItem = useCallback((index: number) => {
console.info('getItem called for index:', index);
if (index >= 0 && index < data.length) {
return data[index];
}
return undefined;
}, [data]);
const getItemCount = useCallback(() => {
const count = data.length;
console.info('getItemCount called, returning:', count);
return count;
}, [data]);
const notifyDataError = (error: any) => {
return false; // Don't retry
};
return (
<View style={[CAROUSEL_STYLE.container]}>
<Carousel
containerStyle={CAROUSEL_STYLE.verticalCarouselContainerStyle}
orientation={'vertical'}
itemPadding={10}
renderItem={renderItemHandler}
getItemKey={getItemKeyHandler}
getItem={getItem}
getItemCount={getItemCount}
notifyDataError={notifyDataError}
hasPreferredFocus
hideItemsBeforeSelection={false}
selectionStrategy={'anchored'}
numOffsetItems={2}
renderedItemsCount={11}
itemScrollDelay={0.2}
/>
</View>
);
};
export const VerticalMemoScrollable = memo(VerticalScrollable);
Heterogeneous Carousel
import {Image, Pressable} from 'react-native';
import {
Carousel,
CarouselRenderInfo,
} from '@amzn/kepler-ui-components';
import {useCallback, useState} from 'react';
import React from 'react';
import {ItemType, ScrollableProps} from './Types';
import {CAROUSEL_STYLE} from './Style';
export const HeterogeneousItemViewScrollable = ({data}: ScrollableProps) => {
function ItemViewType1({item}: CarouselRenderInfo<ItemType>) {
const [focus, setFocus] = useState<boolean>(false);
const onFocusHandler = useCallback(() => setFocus(true), []);
const onBlurHandler = useCallback(() => setFocus(false), []);
return (
<Pressable
style={[
CAROUSEL_STYLE.itemContainerType1,
focus && CAROUSEL_STYLE.itemFocusContainer,
]}
onFocus={onFocusHandler}
onBlur={onBlurHandler}>
<Image style={CAROUSEL_STYLE.imageContainer} source={{uri: item.url}} />
</Pressable>
);
}
function ItemViewType2({item}: CarouselRenderInfo<ItemType>) {
const [focus, setFocus] = useState<boolean>(false);
const onFocusHandler = useCallback(() => setFocus(true), []);
const onBlurHandler = useCallback(() => setFocus(false), []);
return (
<Pressable
style={[
CAROUSEL_STYLE.itemContainerType2,
focus && CAROUSEL_STYLE.itemFocusContainer,
]}
onFocus={onFocusHandler}
onBlur={onBlurHandler}>
<Image
style={CAROUSEL_STYLE.imageContainer}
resizeMode="cover"
source={{uri: item.url}}
/>
</Pressable>
);
}
const renderItemHandler = ({item, index}: CarouselRenderInfo<ItemType>) => {
return index % 2 === 0 ? (
<ItemViewType1 index={index} item={item} />
) : (
<ItemViewType2 index={index} item={item} />
);
};
const getItemKeyHandler = (info: CarouselRenderInfo) =>
`${info.index}-${info.item.url}`;
const getItem = useCallback((index: number) => {
console.info('getItem called for index:', index);
if (index >= 0 && index < data.length) {
return data[index];
}
return undefined;
}, [data]);
const getItemCount = useCallback(() => {
const count = data.length;
console.info('getItemCount called, returning:', count);
return count;
}, [data]);
const notifyDataError = (error: any) => {
return false; // Don't retry
};
return (
<Carousel
containerStyle={CAROUSEL_STYLE.horizontalCarouselContainerStyle}
orientation={'horizontal'}
itemPadding={40}
renderItem={renderItemHandler}
getItemKey={getItemKeyHandler}
getItem={getItem}
getItemCount={getItemCount}
notifyDataError={notifyDataError}
hasPreferredFocus
hideItemsBeforeSelection={false}
selectionStrategy={'anchored'}
numOffsetItems={3}
renderedItemsCount={11}
navigableScrollAreaMargin={100}
/>
);
};
Style.ts
import {StyleSheet} from 'react-native';
export const CAROUSEL_STYLE = StyleSheet.create({
container: {
backgroundColor: '#000000',
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
imageContainer: {
flex: 1,
margin: '3%',
},
itemContainer: {
flex: 1,
},
itemFocusContainer: {
borderWidth: 2,
borderColor: '#FFFFFF',
borderRadius: 4,
},
itemContainerType1: {
height: 350,
width: 200,
justifyContent: 'center',
alignContent: 'center',
},
itemContainerType2: {
height: 350,
width: 500,
justifyContent: 'center',
alignContent: 'center',
},
horizontalCarouselContainerStyle: {
width: '100%',
},
verticalCarouselContainerStyle: {
height: '100%',
justifyContent: 'center',
},
});
Types.ts
export type ItemType = {
url: string;
};
export type ScrollableProps = {
data: ItemType[];
};
Features
Data adapter
Carousel 3.0.0 supports more customization by allowing you to provide a dataAdapter containing various callback functions. You can see the details for the functions in the Props section below.
Assuming you have an array called data containing the Carousel item information, you can implement the dataAdapter functions as shown in the following example.
const getItem = useCallback((index: number) => {
if (index >= 0 && index < data.length) {
return data[index];
}
return undefined;
}, [data]);
const getItemCount = useCallback(() => {
return data.length;
}, [data]);
const getItemKey = (info: CarouselRenderInfo) =>
`${info.url} ${info.item.index}`
const notifyDataError = (error: any) => {
return false; // Don't retry
};
<Carousel
dataAdapter={{
getItem,
getItemCount,
getItemKey,
notifyDataError
}}
...
/>
Selection Strategy
The SelectionStrategy prop controls how a list scrolls and repositions its items when you navigate through them using the D-Pad.
Anchored
The anchored style causes the selected item to stay locked on the initial item’s position. When scrolling, the items reposition so that the selected item remains anchored to that position.

Natural
The natural style causes the selected item to move in the direction of the Carousel's orientation until it reaches the start or end of the list.

Pinned
The pinned style pins the selected item at a specified position during scrolling for larger sets of items. As your user approaches the beginning or end of the list, the scroll behavior transitions smoothly into the natural flow.
The optional prop pinnedSelectedItemOffset allows you to define the pin location for the selected item when the SelectionStrategy is pinned.
The pinnedSelectedItemOffset prop determines where the selected item stays fixed (or 'pinned') in the Carousel's viewport. You can specify this position in two ways:
- As a percentage (0-100%) of the viewport's size:
- For vertical Carousels: percentage of height
- For horizontal Carousels: percentage of width
- Using preset values:
- start (equals 0%)
- center (equals 50%)
- end (equals 100%)
In horizontal mode, measurements are from the left edge, except for right-to-left languages.
pinnedSelectedItemOffset does not handle 0% and 100% values correctly. Use the anchored SelectionStrategy for 0%, and natural for 100%.Variable Scroll Speed
The Carousel component introduces a novel feature for controlling scroll speed through several AnimationDurationProps. Unlike existing components, such as Flatlist or Flashlist, this feature offers enhanced flexibility, so you can fine-tune the scrolling experience.
The itemPressedDuration prop controls the animation duration when pressing on a Carousel item. The itemScrollDuration prop controls the animation duration used to scroll to each Carousel item. The containerSelectionChangeDuration prop controls the animation duration when changing the selected Carousel container.
Selection Border
The Carousel component supports displaying a border around a UI item when selected. You can style the border and choose how to draw the content and border for the selected item. You can enable the border by defining the selectionBorder prop. The default props for the selection border are shown in the following example.
{
borderStrategy: 'outset',
borderColor: 'white',
borderWidth: 8,
borderRadius: 8,
borderStrokeWidth: 3,
borderStrokeRadius: 4,
borderStrokeColor: 'black',
}
Carousel 3.0.0 supports the borderStrategy prop. The prop supports two types of strategies:
outset- where the border is drawn outside of the original bounds of the item, so the item’s content size remains the same but the overall item size gets bigger.inset- where the border is drawn within the bounds of the item over the item's content, so while the item's content size remains the same, part of the item's content is cropped by the border. The overall item size remains the same.
Props
| Prop | Type | Default | Required | Details |
|---|---|---|---|---|
dataAdapter |
CarouselItemDataAdapter<ItemT, KeyT> |
- | TRUE |
The object used to retrieve carousel items and other information about them. |
renderItem |
(info: CarouselRenderInfo) => React.ReactElement |
null | TRUE |
Method to render the carousel item. |
testID |
string | undefined | FALSE |
An unique identifer to locate this carousel in end-to-end tests. |
uniqueId |
string | undefined | FALSE |
An arbitrary and unique identifier for this carousel that will be used to determine when to recycle the carousel and its items. Commonly used by horizontal carousels within a vertical carousel (for example, nested carousels). |
orientation |
horizontal, vertical | horizontal | FALSE |
Specifies the direction and layout for rendering items in a carousel. |
renderedItemsCount |
number | 8 | FALSE |
The number of items to render at a given time when recycling through the carousel for performance optimization. |
numOffsetItems |
number | 2 | FALSE |
Number of items to keep to the top/left of the carousel before recycling the item component to the end. |
navigableScrollAreaMargin |
number | 100 | FALSE |
The margin space (in density-independent pixels) on either side of the carousel's navigable content area in the direction of the carousel's orientation (for example, left and right for horizontal carousels, top and bottom for vertical carousels). |
hasPreferredFocus |
boolean | FALSE |
FALSE |
Determines whether this component should automatically receive focus when rendered. |
initialStartIndex |
number | 0 | FALSE |
Indicates the first item index in the carousel to be selected. |
hideItemsBeforeSelection |
boolean | FALSE |
FALSE |
Determines whether items before the selected one should be hidden. |
trapSelectionOnOrientation |
boolean | FALSE |
FALSE |
This flag will prevent selection from progressing to the nearest component outside the carousel alongside its orientation. If the carousel is horizontal and the user is at the start item and presses to move backward, or at the end item and presses to move forward, this flag will prevent selection from escaping the carousel. |
containerStyle |
StyleProp<ViewStyle> |
undefined | FALSE |
Style applied to the carousel container. |
itemStyle |
CarouselItemStyleProps |
- | FALSE |
Style applied to the carousel items. |
animationDuration |
AnimationDurationProps |
- | FALSE |
The durations of various animations related to the carousel and its items. |
selectionStrategy |
anchored, natural, pinned |
anchored |
FALSE |
Specifies how the selected item moves in the carousel in response to the D-pad key presses. anchored - Causes the selected item to stay anchored on the initial item’s position when scrolling in either direction along the orientation.natural - Causes the selected item to float along the carousel's orientation until it reaches either end of the list.pinned - Enables the selected item to follow natural scrolling behavior at the start and end of the list, while keeping it pinned to a specific position defined by the pinnedSelectedItemOffset prop during the rest of the scrolling. |
pinnedSelectedItemOffset |
Percentage start, center, end | 0% | FALSE |
The value determines the relative position from the leading edge of the carousel where the selected item is pinned. |
onSelectionChanged |
(event: CarouselSelectionChangeEvent) => void |
undefined | FALSE |
Function called whenever the current selected item on the list changes (it's not called when the carousel is selected or unselected). |
selectionBorder |
SelectionBorderProps |
undefined | FALSE |
When this value is not undefined, the selected item will have a style-able border enveloping the selected item. All the style related props in selectionBorder property defines the look-and-feel of the border surrounding the selected item when it is enabled. |
ref |
React.Ref<ShovelerRef<Key>> |
undefined | FALSE |
Ref to access the Carousel component's ScrollTo and EnableDPad methods. |
Selection border props
| Prop | Type | Default | Required | Details |
|---|---|---|---|---|
borderStrategy |
inset, outset | outset | FALSE |
Specifies how the content and the border are drawn for the selected item. |
borderWidth |
number | 8 | FALSE |
Specifies the thickness of the border around the selected item. |
borderColor |
string | white | FALSE |
Specifies the color of the border around the selected item. |
borderRadius |
number | 8 | FALSE |
Determines the corner radius of the border for rounded edges. |
borderStrokeWidth |
number | 3 | FALSE |
Specifies the thickness of the outline stroke that separates the border from the item's content. |
borderStrokeRadius |
number | 4 | FALSE |
Defines the corner radius for the outline stroke that separates the border from the item's content. |
borderStrokeColor |
string | black | FALSE |
Specifies the color of the outline stroke for the selection border. |
Animation duration props
| Prop | Type | Default | Required | Details |
|---|---|---|---|---|
itemPressedDuration |
number | 0.15 | FALSE |
Amount of time, in seconds, used when pressing on an item. |
itemScrollDuration |
number | 0.2 | FALSE |
Amount of time, in seconds, used to scroll each item. |
containerSelectionChangeDuration |
number | itemPressedDuration |
FALSE |
Amount of time, in seconds, used for changing the selected container. |
Carousel item style props
| Prop | Type | Default | Required | Details |
|---|---|---|---|---|
itemPadding |
number | 20 | FALSE |
The space between the adjacent items along the carousel's orientation, in pixels. |
itemPaddingOnSelection |
number | itemPadding | FALSE |
The space between the adjacent items along the carousel's orientation, in pixels when the carousel is selected. |
pressedItemScaleFactor |
number | 0.8 | FALSE |
A scale multiplier to be applied to the item when it is pressed by the user. |
selectedItemScaleFactor |
number | 1 | FALSE |
A scale multiplier to be applied to the item when it is selected. When this value is 1.0, it means there will be no scaling happening to the selected item while the user scrolls through the list. |
getSelectedItemOffset |
(info: CarouselRenderInfo) => ShiftFactor |
undefined | FALSE |
A function to retrieve the offset value to be applied for an item relative to its current position. |
Carousel item data adapter
| Prop | Type | Default | Required | Details |
|---|---|---|---|---|
getItem |
(index: number) => ItemT |
undefined | TRUE |
A function that will receive an index, and return an item's data object. The object returned will be used to call other data-access functions to retrieve information about specific items. |
getItemCount |
() => number; |
- | TRUE |
A function that will return the item count for the list. |
getItemKey |
(info: CarouselRenderInfo) => KeyT |
undefined | TRUE |
Function to provide a unique key for each item based on the data and index. |
notifyDataError |
(error: CarouselDataError) => boolean; |
- | TRUE |
This function is called by the list component when an item cannot be used in the list. |
Carousel ref
The Carousel supports following methods through its ShovelerRef<KeyT>:
| Prop | Type | Default | Required | Details |
|---|---|---|---|---|
scrollTo |
(index: number, animated : boolean) : void; |
- | FALSE |
Method to scroll to give Indexed Item on the Carousel |
scrollToKey |
(key: Key, animated: boolean) : void; |
- | FALSE |
Method to scroll to the item belonging to a unique key in the Carousel |
enableDpad |
(enable: boolean) : void; |
- | FALSE |
Support HW Key events on the Carousel |
Troubleshooting
Vertical scroll only works with a set height
In the case of vertical scrolling (specifically natural scrolling), you need to set containerStyle with a fixed height value.
Usage
This version of Carousel is included in @amazon-devices/kepler-ui-components ^2.0.0. Check your package.json to confirm the version is set as shown below.
"dependencies": {
// ...
"@amazon-devices/kepler-ui-components": "^2.0.0"
}
Import
import { Carousel } from '@amazon-devices/kepler-ui-components';
Examples
Horizontal Carousel
import React, {memo} from 'react';
import {Image, Pressable, View} from 'react-native';
import {
Carousel,
CarouselRenderInfo,
ItemInfo,
} from '@amazon-devices/kepler-ui-components';
import {useState} from 'react';
import {ItemType, ScrollableProps} from '../../Types';
import {CAROUSEL_STYLE} from './Style';
export const HorizontalScrollable = ({data}: ScrollableProps) => {
function ItemView({item}: CarouselRenderInfo<ItemType>) {
const [focus, setFocus] = useState<boolean>(false);
const onFocusHandler = () => {
setFocus(true);
};
const onBlurHandler = () => {
setFocus(false);
};
return (
<Pressable
style={[
CAROUSEL_STYLE.itemContainer,
focus && CAROUSEL_STYLE.itemFocusContainer,
]}
onFocus={onFocusHandler}
onBlur={onBlurHandler}>
<Image style={CAROUSEL_STYLE.imageContainer} source={{uri: item.url}} />
</Pressable>
);
}
const renderItemHandler = ({item, index}: CarouselRenderInfo<ItemType>) => {
return <ItemView index={index} item={item} />;
};
const itemInfo: ItemInfo[] = [
{
view: ItemView,
dimension: {
width: 250,
height: 420,
},
},
];
const getItemForIndexHandler = (index: number) => ItemView;
const keyProviderHandler = (item: ItemType, index: number) =>
`${index}-${item.url}`;
return (
<View style={CAROUSEL_STYLE.container}>
<Carousel
data={data}
orientation={'horizontal'}
containerStyle={CAROUSEL_STYLE.horizontalCarouselContainerStyle}
itemDimensions={itemInfo}
itemPadding={20}
renderItem={renderItemHandler}
getItemForIndex={getItemForIndexHandler}
keyProvider={keyProviderHandler}
hasTVPreferredFocus
hideItemsBeforeSelection={false}
itemSelectionExpansion={{
widthScale: 1.2,
heightScale: 1.2,
}}
numOffsetItems={2}
focusIndicatorType={'fixed'}
maxToRenderPerBatch={10}
firstItemOffset={100}
dataStartIndex={0}
initialStartIndex={0}
shiftItemsOnSelection={true}
trapFocusOnAxis={false}
selectionBorder={{
enabled: true,
borderColor: '#FFFFFF',
borderWidth: 2,
borderRadius: 0,
borderStrokeRadius: 0
}}
/>
</View>
);
};
export const HorizontalMemoScrollable = memo(HorizontalScrollable);
Vertical Carousel
import {Image, Pressable, View} from 'react-native';
import {
Carousel,
CarouselRenderInfo,
ItemInfo,
} from '@amazon-devices/kepler-ui-components';
import {memo, useState} from 'react';
import React from 'react';
import {ItemType, ScrollableProps} from '../../Types';
import {CAROUSEL_STYLE} from './Style';
export const VerticalScrollable = ({data}: ScrollableProps) => {
function ItemView({item}: CarouselRenderInfo<ItemType>) {
const [focus, setFocus] = useState<boolean>(false);
const onFocusHandler = () => {
setFocus(true);
};
const onBlurHandler = () => {
setFocus(false);
};
return (
<Pressable
style={[
CAROUSEL_STYLE.itemContainer,
focus && CAROUSEL_STYLE.itemFocusContainer,
]}
onFocus={onFocusHandler}
onBlur={onBlurHandler}>
<Image style={CAROUSEL_STYLE.imageContainer} source={{uri: item.url}} />
</Pressable>
);
}
const renderItemHandler = ({item, index}: CarouselRenderInfo<ItemType>) => {
return <ItemView index={index} item={item} />;
};
const itemInfo: ItemInfo[] = [
{
view: ItemView,
dimension: {
width: 250,
height: 420,
},
},
];
const getItemForIndexHandler = (index: number) => ItemView;
const keyProviderHandler = (item: ItemType, index: number) =>
`${index}-${item.url}`;
return (
<View style={[CAROUSEL_STYLE.container]}>
<Carousel
containerStyle={CAROUSEL_STYLE.verticalCarouselContainerStyle}
data={data}
orientation={'vertical'}
itemDimensions={itemInfo}
itemPadding={10}
renderItem={renderItemHandler}
getItemForIndex={getItemForIndexHandler}
keyProvider={keyProviderHandler}
hasTVPreferredFocus
hideItemsBeforeSelection={false}
itemSelectionExpansion={{
widthScale: 1.2,
heightScale: 1.2,
}}
focusIndicatorType="fixed"
numOffsetItems={2}
maxToRenderPerBatch={11}
itemScrollDelay={0.2}
/>
</View>
);
};
export const VerticalMemoScrollable = memo(VerticalScrollable);
Heterogeneous Carousel
import {Image, Pressable} from 'react-native';
import {
Carousel,
CarouselRenderInfo,
ItemInfo,
} from '@amazon-devices/kepler-ui-components';
import {useCallback, useState} from 'react';
import React from 'react';
import {ItemType, ScrollableProps} from '../../Types';
import {CAROUSEL_STYLE} from './Style';
export const HeterogeneousItemViewScrollable = ({data}: ScrollableProps) => {
function ItemViewType1({item}: CarouselRenderInfo<ItemType>) {
const [focus, setFocus] = useState<boolean>(false);
const onFocusHandler = useCallback(() => setFocus(true), []);
const onBlurHandler = useCallback(() => setFocus(false), []);
return (
<Pressable
style={[
CAROUSEL_STYLE.itemContainerType1,
focus && CAROUSEL_STYLE.itemFocusContainer,
]}
onFocus={onFocusHandler}
onBlur={onBlurHandler}>
<Image style={CAROUSEL_STYLE.imageContainer} source={{uri: item.url}} />
</Pressable>
);
}
function ItemViewType2({item}: CarouselRenderInfo<ItemType>) {
const [focus, setFocus] = useState<boolean>(false);
const onFocusHandler = useCallback(() => setFocus(true), []);
const onBlurHandler = useCallback(() => setFocus(false), []);
return (
<Pressable
style={[
CAROUSEL_STYLE.itemContainerType2,
focus && CAROUSEL_STYLE.itemFocusContainer,
]}
onFocus={onFocusHandler}
onBlur={onBlurHandler}>
<Image
style={CAROUSEL_STYLE.imageContainer}
resizeMode="cover"
source={{uri: item.url}}
/>
</Pressable>
);
}
const renderItemHandler = ({item, index}: CarouselRenderInfo<ItemType>) => {
return index % 2 === 0 ? (
<ItemViewType1 index={index} item={item} />
) : (
<ItemViewType2 index={index} item={item} />
);
};
const itemInfo: ItemInfo[] = [
{
view: ItemViewType1,
dimension: {
width: CAROUSEL_STYLE.itemContainerType1.width,
height: CAROUSEL_STYLE.itemContainerType1.height,
},
},
{
view: ItemViewType2,
dimension: {
width: CAROUSEL_STYLE.itemContainerType2.width,
height: CAROUSEL_STYLE.itemContainerType2.height,
},
},
];
const getItemForIndexHandler = (index: number) => {
return index % 2 === 0 ? ItemViewType1 : ItemViewType2;
};
const keyProviderHandler = (item: ItemType, index: number) =>
`${index}-${item.url}`;
return (
<Carousel
data={data}
containerStyle={CAROUSEL_STYLE.horizontalCarouselContainerStyle}
orientation={'horizontal'}
itemDimensions={itemInfo}
itemPadding={40}
renderItem={renderItemHandler}
getItemForIndex={getItemForIndexHandler}
keyProvider={keyProviderHandler}
hasTVPreferredFocus
hideItemsBeforeSelection={false}
itemSelectionExpansion={{
widthScale: 1.1,
heightScale: 1.1,
}}
focusIndicatorType="fixed"
numOffsetItems={3}
maxToRenderPerBatch={11}
firstItemOffset={100}
/>
);
};
Features
FocusIndicator
The FocusIndicator style specifies how the focus indicator moves in a list of items in response to the D-Pad left and right key presses.
Natural
The natural style causes the focus indicator to float in the scroll direction until it reaches the start or end of the list.

Fixed
The fixed style causes the focus indicator to stay locked on the initial item’s position. When scrolling, the items reposition so that the focused item remains fixed.

Pinned focus style
The pinned focus style pins the focused item at a position during scrolling for larger sets of items. As the user approaches the beginning or end of the list, the scroll behavior transitions smoothly into the natural flow.
The optional prop pinnedFocusOffset allows users to define the pin location for the selected item and the FocusIndicatorType includes the focus style PINNED.
The pinnedFocusOffset ranges from 0% to 100%. This is the percentage of the size of the viewport. The height is used for vertical carousels and width is used for horizontal carousels. It defines the position where the selected item should be pinned relative to the start (in horizontal mode, except for languages written from right to left) or the top edge (in vertical mode) of the viewport.
| Prop | Type | Default | Required | Details |
|---|---|---|---|---|
| focusIndicatorType | focusIndicatorType = fixed | natural | pinned |
fixed |
FALSE | Specifies how the focus indicator moves in a list of items in response to the DPad Left and Right key presses. fixed - focus is fixed on the far left position of the item. natural - focus floats in the direction of the scroll. pinned - focus behavior mimics natural scrolling at the beginning and end of the list. As the user scrolls through the items, the focused item is pinned to a specific position along the scrolling axis, determined by the pinnedFocusOffset. |
| pinnedFocusOffset | Percentage | undefined |
undefined |
FALSE | The pinnedFocusOffset value determines the relative position, in percentage, from the leading edge of the Carousel where the focus of the selected or highlighted item is pinned. This value is a string Percentage type, for example, "50%". Note: When focusIndicatorType is set to pinned and pinnedFocusOffset is not defined, the Carousel defaults to the 'natural' focusIndicatorType behavior. |
pinnedFocusOffset does not handle 0% and 100% values correctly. Use the fixed focusIndicatorType for 0%, and natural for 100%.Demo videos
The following videos shows the Pinned focus style in horizontal and vertical scrolling.
Horizontal scrolling
Vertical scrolling
Recycling
The recycling technique involves reusing existing list item views to display new data as the user scrolls, rather than creating new views for each item. This reduces memory usage and optimizes rendering.
The Recycling View does not allocate an item view for every item in your data source. Instead, it allocates only the number of item views that fit on the screen and it reuses those item layouts as the user scrolls. When the view first scrolls out of sight, it goes through the recycling process, shown in the following diagram with detailed steps below.

- When a view scrolls out of sight and is no longer displayed, it becomes a Scrap View.
- The Recycler has a Recycle View Heap caching system for these views.
- When a new item is to be displayed, a view is taken from the recycle pool for reuse. Because this view must be re-bound by the adapter before being displayed, it is called a dirty view.
- The dirty view is recycled: the adapter locates the data for the next item to be displayed and copies this data to the views for this item. References for these views are retrieved from the recycler view’s view holder.
- The recycled view is added to the list of items in the Carousel that is about to go on-screen.
- The recycled view goes on-screen as the user scrolls the Carousel to the next item in the list. Meanwhile, another view scrolls out of sight and is recycled according to the above steps.
Variable Scroll speed
The Carousel component introduces a novel feature for controlling scroll speed through scrollDuration prop. Unlike existing components such as Flatlist or Flashlist, this feature offers enhanced flexibility, empowering end-users to finely tune their scrolling experience to suit their preferences.
The scrollableFocusScaleDuration prop controls the animation duration for item selection change when focus enters or exists the scrollable area. Adjust this value as needed for smoother transitions.
The itemScrollDelay is in effect when the tiles are transitioning, such as when they move. If only the focus is moving then itemScrollDelay does not apply.
Props
| Prop | Type | Default | Required | Details |
|---|---|---|---|---|
| testID | string |
kepler-shoveler |
FALSE | An unique identifer to locate this scrollable in end-to-end tests. |
| Data | Item[] |
- | TRUE | Data is a plain array of items of a given type. |
| itemPadding | number |
- | TRUE | The space between the adjacent items in scroll direction, in pixels. |
| orientation | Orientation = Horizontal | Vertical |
horizontal | FALSE | The direction for rendering the scrollable items. |
| itemDimensions | ItemInfo[] |
- | TRUE | Contains all the Views and their sizes which will be used to render the children of this scrollable. |
| maxToRenderPerBatch | number |
8 | FALSE | Maximum numbers of items to render at a time when recycling through the scrollable. |
| numOffsetItems | number |
2 | FALSE | Number of items to keep to the top/left of the scrollable before recycling the component to the end. |
| firstItemOffset | number |
0 | FALSE | Amount of top/left padding to put on the scrollable. |
| itemSelectionExpansion | Dimension = { width: number; height: number } |
(0,0) | FALSE | Space to allocate for the selected item depending on the item's dimension |
| shiftItemsOnSelection | boolean |
TRUE | FALSE | This flag will determine if items should be shifted in a row as selection is shifted. |
| focusIndicatorType | FocusStyle = fixed | natural | pinned |
fixed |
FALSE | Specifies how the focus indicator moves in a list of items in response to the DPad Left and Right key presses. fixed - focus is fixed on the far left position of the item. natural - focus floats in the direction of the scroll. pinned - focus behavior mimics natural scrolling at the beginning and end of the list. As the user scrolls through the items, the focused item is pinned to a specific position along the scrolling axis, determined by the pinnedFocusOffset. |
| pinnedFocusOffset | Percentage | undefined |
undefined |
FALSE | The pinnedFocusOffset value determines the relative position, in percentage, from the leading edge of the Carousel where the focus of the selected or highlighted item is pinned. This value is a string Percentage type, for example, "50%". Note: When focusIndicatorType is set to pinned and pinnedFocusOffset is not defined, the Carousel defaults to the 'natural' focusIndicatorType behavior. |
| hasTVPreferredFocus | boolean |
FALSE | FALSE | Whether the scrollable should try to obtain initial focus. |
| initialStartIndex | number |
0 | FALSE | Initial focused selected index for the scrollable |
| dataStartIndex | number |
0 | FALSE | Indicates the first data index that is available to the scrollable. |
| hideItemsBeforeSelection | boolean |
FALSE | If set to true, then all items before the pivot/selected items will be hidden | |
| itemScrollDelay | number |
0.2 | FALSE | Amount of time, in seconds, used to scroll each item |
| trapFocusOnAxis | boolean |
FALSE | FALSE | This flag will prevent the focus from progressing to the nearest component outside the scrollable alongside its axis. Meaning, if the scrollable is horizontal and the user is at the first item and presses left, or last item and presses right, this flag will prevent the focus from escaping the scrollable. |
| containerStyle | StyleProp<ViewStyle> |
undefined | FALSE | View style for the shovler container |
| selectionBorder | SelectionBorderProps |
{ enabled: false, borderColor: 'white', borderWidth: 8, borderRadius: 8, borderStrokeRadius: 4, borderStrokeColor: 'black', borderStrokeWidth: 3, } |
FALSE | When this flag is set to true, the selected item has a style-able border enveloping the selected item. |
| ref | React.Ref<ShovelerRef<Key>> |
undefined | FALSE | Ref to access the Carousel component's ScrollTo and EnableDPad methods. |
| Prop | function signature | Default | isRequired | Details |
|---|---|---|---|---|
| renderItem | (info: ShovlerRenderInfo) => React.ReactElement | null |
- | TRUE | Method to render the scrollable Item. |
| getItemForIndex | (index: number) => React.FC |
- | TRUE | Either a constant step size for all the elements or a method to provide the step size per item index. |
| keyProvider | (data: ItemT, index: number) => keyT |
- | TRUE | provider used to extract a key from an object |
The Carousel supports following methods through its ShovelerRef<KeyT>:
| Prop | function signature | Default | Details |
|---|---|---|---|
| scrollTo | (index: number, animated : boolean) : void; |
- | Method to scroll to give Indexed Item on the Carousel |
| enableDpad | (enable: boolean) : void; |
- | Support HW Key events on the Carousel |
type Dimension = { width: number; height: number }
type ItemInfo<Prop = any> = {
view: React.FC<Prop>;
dimension: Dimension
}
Customizations
- Variable Scroll Speed - The scroll speed between each item on the carousel can be modified using the
itemScrollDelayprop, which defaults to 0.2 seconds. - Focus Indicator Style - The caursel supports two types of focus indicators:
- Natural - Causes the focus indicator to float in the scroll direction until it reaches the start/end of the list
- Fixed - Causes the focus indicator stays locked on the initial item’s position, and when scrolling the items re-position so that the focused item remains fixed.
- Item Scale on selection - The selected or focused item on the carousel can expand in height and width by a specified factor compared to its original size.
- Show/Hide Items before selection - Allows the option to hide or show items preceding the selected one during fixed scrolling. Displaying an item before the selected one provides a visual cue to the user that more items are available to the left.
- Container Style - Allows setting the style for the entire carousel view. Common styles include background color, opacity, width, and height.
-
Selection Border - The Carousel component now supports displaying a border around a UI item when focused. This change introduces new props to configure the border style and a boolean that allows you to choose between using the native border or providing a custom one. Common use cases for selection border include:
-
Default border style with
selctionBorder.enabledastrue.selectionBorder={{ enabled: true }}
-
Default border style with
selectionBorder.enabledasfalse.selectionBorder={{ enabled: false }}
-
Border style with
borderPropsandborderStrokeProps.selectionBorder={{ enabled: true, borderColor: 'white', borderWidth: 4, borderRadius: 4, borderStrokeRadius: 0, borderStrokeColor: 'transparent', borderStrokeWidth: 0 }}
-
Troubleshooting
getItemForIndex is not a function
You may encounter an error like this example below.
TypeError: getItemForIndex is not a function (it is undefined)
This error is located at:
in ItemRenderer
in KeplerScrollable
in KeplerScrollableWithRef
in Scrollable (created by App)
in RCTView (created by View)
in View (created by App)
...
This error is due to the changes to Carousel in the 2.0.0 version of VUIC. The enhancements required changes to the input props and behavior of the component.
Verify which version of the package you are using. Go to your app's package.json file and find the "@amazon-devices/kepler-ui-components" dependency.
If the package version is 2.0.0 or higher, then you have the latest version of Carousel. You have two options to resolve the error: use the new version and update your implementation, or revert to the older version.
Option 1: Update your implementation
If you want to continue using VUIC version 2.0.0, you need to update your implementation of the Carousel component. See Migrate to the latest version above for more information.
Option 2: Revert to the previous Carousel version
If you want to continue with the previous version of Carousel, change the version of "@amazon-devices/kepler-ui-components" to ^1.0.0 in your package.json file.
Vertical scroll only works with a set height
In the case of vertical scrolling (specifically natural scrolling), you need to set containerStyle with a fixed height value.
When setting shiftItemsOnSelection to false, an unintended overlapping behavior can be observed
If you set shiftItemsOnSelection to false and do not set the correct height and width, an overlapping behavior can be observed in the carousel. You need to set the correct height and width to troubleshoot this issue. The shiftItemsOnSelection prop will let the carousel component make additional room while its selected to expand itself. If you set this to false, the carousel doesn't expand.
ItemDimension should be equal to or greater than the Card’s dimensions
If you do not set the itemDimension equal to or greater than the card dimensions, alignment may not happen properly.
Last updated: Oct 24, 2025

