AniUI

Carousel

Horizontal scrollable carousel with pagination dots and optional auto-play. Uses FlatList for performant scrolling.

Slide 1
Slide 2
Slide 3
Web preview — components render natively on iOS & Android
import { Carousel } from "@/components/ui/carousel";
<Carousel
  data={[
    <View className="h-48 bg-primary rounded-lg items-center justify-center"><Text className="text-primary-foreground text-lg">Slide 1</Text></View>,
    <View className="h-48 bg-secondary rounded-lg items-center justify-center"><Text className="text-secondary-foreground text-lg">Slide 2</Text></View>,
    <View className="h-48 bg-accent rounded-lg items-center justify-center"><Text className="text-accent-foreground text-lg">Slide 3</Text></View>,
  ]}
/>

Installation#

npx @aniui/cli add carousel

Auto-Play#

Set autoPlay to automatically cycle through slides. Configure the interval in milliseconds.

Slide 1
Slide 2
Slide 3
Web preview — components render natively on iOS & Android
<Carousel
  data={slides}
  autoPlay
  interval={3000}
/>

Props#

PropTypeDefault
data
ReactNode[]
itemWidth
number
measured container width
showDots
boolean
true
autoPlay
boolean
false
interval
number
3000
className
string

Accessibility#

  • accessibilityRole="adjustable" with page indicators.
  • Swipe gestures are supplemented with navigation controls for motor accessibility.

Source#

components/ui/carousel.tsx
import React, { useRef, useState } from "react";
import { View, FlatList } from "react-native";
import { cn } from "@/lib/utils";

export interface CarouselProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
  data: React.ReactNode[];
  itemWidth?: number;
  showDots?: boolean;
  autoPlay?: boolean;
  interval?: number;
}
export function Carousel({ className, data, itemWidth, showDots = true, autoPlay, interval = 3000, onLayout, ...props }: CarouselProps) {
  const [active, setActive] = useState(0);
  const [measured, setMeasured] = useState(0);
  // Size each slide to the carousel's OWN width (measured), not the window —
  // otherwise slides overflow when the carousel sits inside padding.
  const width = itemWidth ?? measured;
  const ref = useRef<FlatList>(null);
  React.useEffect(() => {
    if (!autoPlay || data.length <= 1 || !width) return;
    const timer = setInterval(() => {
      const next = (active + 1) % data.length;
      ref.current?.scrollToOffset({ offset: next * width, animated: true });
    }, interval);
    return () => clearInterval(timer);
  }, [autoPlay, active, data.length, interval, width]);
  return (
    <View
      className={cn("", className)}
      onLayout={(e) => {
        setMeasured(e.nativeEvent.layout.width);
        onLayout?.(e);
      }}
      accessibilityRole="adjustable"
      accessibilityLabel={`Carousel, item ${active + 1} of ${data.length}`}
      {...props}
    >
      {width > 0 ? (
        <FlatList
          ref={ref}
          data={data}
          horizontal
          pagingEnabled
          showsHorizontalScrollIndicator={false}
          onMomentumScrollEnd={(e) => setActive(Math.round(e.nativeEvent.contentOffset.x / width))}
          renderItem={({ item }) => <View style={{ width }}>{item}</View>}
          keyExtractor={(_, i) => String(i)}
        />
      ) : null}
      {showDots && data.length > 1 && (
        <View className="flex-row items-center justify-center gap-1.5 mt-3" accessibilityRole="tablist">
          {data.map((_, i) => (
            <View key={i} className={cn("h-2 rounded-full", i === active ? "w-4 bg-primary" : "w-2 bg-muted-foreground/30")} accessibilityRole="tab" accessibilityState={{ selected: i === active }} accessibilityLabel={`Page ${i + 1}`} />
          ))}
        </View>
      )}
    </View>
  );
}