AniUI

Image Gallery

Horizontal image carousel with fullscreen modal and pagination dots.

Installation#

npx @aniui/cli add image-gallery
Web preview — components render natively on iOS & Android
import { ImageGallery, type GalleryImage } from "@/components/ui/image-gallery";

const images: GalleryImage[] = [
  { uri: "https://picsum.photos/400/300?random=1", alt: "Mountain landscape" },
  { uri: "https://picsum.photos/400/300?random=2", alt: "Ocean sunset" },
  { uri: "https://picsum.photos/400/300?random=3", alt: "Forest trail" },
];

export function MyScreen() {
  return (
    <ImageGallery
      images={images}
      showPagination
    />
  );
}

Usage#

app/index.tsx
import { ImageGallery, type GalleryImage } from "@/components/ui/image-gallery";

const images: GalleryImage[] = [
  { uri: "https://picsum.photos/400/300?random=1", alt: "Mountain landscape" },
  { uri: "https://picsum.photos/400/300?random=2", alt: "Ocean sunset" },
  { uri: "https://picsum.photos/400/300?random=3", alt: "Forest trail" },
];

export function MyScreen() {
  return (
    <ImageGallery
      images={images}
      showPagination
    />
  );
}

GalleryImage Type#

PropTypeDefault
uri
string
-
alt
string
-

Props#

PropTypeDefault
images
GalleryImage[]
-
showPagination
boolean
true
className
string
-

Also accepts all View props.

Accessibility#

  • Horizontal gallery with fullscreen modal viewer.
  • Each image has accessibilityRole="image" and close button is labeled for screen readers.

Source#

components/ui/image-gallery.tsx
import React, { useRef, useState } from "react";
import { View, FlatList, Image, Dimensions, Pressable, Modal, Text } from "react-native";
import { cn } from "@/lib/utils";

const { width: SCREEN_WIDTH } = Dimensions.get("window");

export interface GalleryImage {
  uri: string;
  alt?: string;
}

export interface ImageGalleryProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
  images: GalleryImage[];
  showPagination?: boolean;
}

export function ImageGallery({ className, images, showPagination = true, ...props }: ImageGalleryProps) {
  const [activeIndex, setActiveIndex] = useState(0);
  const [fullscreen, setFullscreen] = useState(false);
  const listRef = useRef<FlatList>(null);

  return (
    <View className={cn("", className)} {...props}>
      <FlatList
        ref={listRef}
        data={images}
        horizontal
        pagingEnabled
        showsHorizontalScrollIndicator={false}
        keyExtractor={(_, i) => String(i)}
        onMomentumScrollEnd={(e) => {
          setActiveIndex(Math.round(e.nativeEvent.contentOffset.x / SCREEN_WIDTH));
        }}
        renderItem={({ item, index }) => (
          <Pressable
            onPress={() => setFullscreen(true)}
            accessibilityRole="image"
            accessibilityLabel={item.alt ?? `Image ${index + 1}`}
          >
            <Image source={{ uri: item.uri }} style={{ width: SCREEN_WIDTH, height: SCREEN_WIDTH * 0.75 }} resizeMode="cover" />
          </Pressable>
        )}
      />
      {showPagination && images.length > 1 && (
        <View className="flex-row justify-center gap-1.5 mt-3">
          {images.map((_, i) => (
            <View
              key={i}
              className={cn("h-2 rounded-full", i === activeIndex ? "w-4 bg-primary" : "w-2 bg-muted")}
            />
          ))}
        </View>
      )}
      <Modal visible={fullscreen} transparent animationType="fade" onRequestClose={() => setFullscreen(false)}>
        <View className="flex-1 bg-black items-center justify-center">
          <Pressable
            onPress={() => setFullscreen(false)}
            className="absolute top-14 right-4 z-10 min-h-12 min-w-12 items-center justify-center"
            accessibilityRole="button"
            accessibilityLabel="Close"
          >
            <Text className="text-white text-2xl">×</Text>
          </Pressable>
          <Image
            source={{ uri: images[activeIndex]?.uri }}
            style={{ width: SCREEN_WIDTH, height: SCREEN_WIDTH }}
            resizeMode="contain"
          />
        </View>
      </Modal>
    </View>
  );
}