---
title: "Programmatic Control"
description: "Control react-native-reanimated-carousel v5 with its ref API"
canonical_url: "https://rn-carousel.dev/programmatic-control"
markdown_url: "https://rn-carousel.dev/programmatic-control.md"
version: "5"
---
# Programmatic Control

Carousel remains uncontrolled: there is no controlled `index` prop. Use `defaultIndex` for the mount position and `CarouselRef` for later navigation.

## Setup

```tsx
import * as React from "react";
import {
  Carousel,
  type CarouselRef,
} from "react-native-reanimated-carousel";

function ProductCarousel() {
  const ref = React.useRef<CarouselRef>(null);

  return (
    <Carousel
      ref={ref}
      data={products}
      keyExtractor={(product) => product.id}
      renderItem={({ item }) => <ProductCard product={item} />}
    />
  );
}
```

## `next` and `prev`

Use these methods for relative movement:

```tsx
ref.current?.next();
ref.current?.next({ count: 3 });
ref.current?.prev({ animated: false });
ref.current?.prev({ count: 2, animated: true });
```

```ts
interface CarouselStepOptions {
  count?: number;    // positive integer, default 1
  animated?: boolean; // default true
}
```

`count={0}` is a no-op. Other invalid counts warn once per Carousel instance in development and are ignored.

With `loop={false}`, a movement that would cross a boundary stops at that boundary. With `loop`, `next` and `prev` continue in the requested logical raw-data direction across cycles.

## `scrollTo`

`scrollTo` performs absolute raw-data index navigation:

```tsx
ref.current?.scrollTo({ index: 3 });
ref.current?.scrollTo({ index: 0, animated: false });
```

```ts
interface CarouselScrollToOptions {
  index: number;      // required in-range raw-data index
  animated?: boolean; // default true
}
```

An invalid index warns once per Carousel instance in development and does nothing. In loop mode, the command chooses the shortest route; an exact distance tie chooses logical forward.

Relative `scrollTo({ count })` and command `onFinished` callbacks were removed in v5. Use `next`/`prev` for relative movement and `onSnapToItem` for settled selection.

## `getCurrentIndex`

```tsx
const index = ref.current?.getCurrentIndex();
```

The result is the last settled raw-data index:

- during a gesture or animation, it remains the previous settled index;
- an accepted non-animated command updates it immediately;
- after settle, it matches the argument passed to `onSnapToItem`;
- for empty data, it returns the neutral value `0`.

  Read `getCurrentIndex()` from an event handler or effect, not during render. The ref is an imperative snapshot and does not trigger React rendering.

## Lifecycle

```tsx
<Carousel
  ref={ref}
  data={products}
  renderItem={renderProduct}
  onScrollStart={() => {
    console.log("movement started");
  }}
  onSnapToItem={(index) => {
    console.log("settled on", index);
  }}
/>
```

An accepted gesture, command, or autoplay transition fires `onScrollStart` once. Settle fires `onSnapToItem`, including when a gesture returns to the same index. An accepted non-animated command emits both synchronously in that order.

Rejected/no-op commands, relayout, data reconciliation, and direct `scrollOffsetValue` writes do not fire these callbacks.

Because autoplay also fires `onScrollStart`, do not use it as a touch detector. Observe `onBegin` through `onConfigurePanGesture` when user contact matters.

## Pagination control

```tsx
import * as React from "react";
import { View } from "react-native";
import { useSharedValue } from "react-native-reanimated";
import {
  Carousel,
  Pagination,
  type CarouselRef,
} from "react-native-reanimated-carousel";

function CarouselWithPagination() {
  const ref = React.useRef<CarouselRef>(null);
  const progress = useSharedValue(0);
  const data = ["One", "Two", "Three"];

  return (
    <View>
      <Carousel
        ref={ref}
        data={data}
        progress={progress}
        renderItem={({ item }) => <Slide title={item} />}
      />
      <Pagination
        count={data.length}
        progress={progress}
        onPress={(index) => ref.current?.scrollTo({ index })}
      />
    </View>
  );
}
```

Pagination receives raw-data indices and never owns the ref or writes progress.

## Data updates

Use `keyExtractor` when the current item should survive insertions and reordering:

```tsx
<Carousel
  ref={ref}
  data={products}
  keyExtractor={(product) => product.id}
  renderItem={renderProduct}
/>
```

If the selected key still exists, it remains selected. If it disappears—or no `keyExtractor` is provided—the old numeric index is clamped into the new range. A data update during movement is reconciled after settle without lifecycle callbacks.

If your application adds an item and wants to select it, navigate after React commits the new data:

```tsx
const [pendingIndex, setPendingIndex] = React.useState<number | null>(null);

function addProduct(product: Product) {
  setProducts((current) => {
    setPendingIndex(current.length);
    return [...current, product];
  });
}

React.useEffect(() => {
  if (pendingIndex === null || pendingIndex >= products.length) return;
  ref.current?.scrollTo({ index: pendingIndex });
  setPendingIndex(null);
}, [pendingIndex, products.length]);
```

## Choosing the right control surface

| Goal | API |
|---|---|
| Start on an item | `defaultIndex` |
| Move relatively | `next` / `prev` |
| Select an absolute item | `scrollTo({ index })` |
| Read settled selection | `getCurrentIndex()` |
| React to settled selection | `onSnapToItem` |
| Drive UI-thread indicators | `progress` |
| Move content continuously at pixel level | `scrollOffsetValue` |

Directly writing `scrollOffsetValue` is advanced two-way control. It moves content but does not commit selection; finish with `scrollTo({ index })` when a semantic selected item is required.
