Blog
React Nativenpm packageImage CollageReact Native Image CollageReact Native Photo GridReact Native Image GridReact Native GalleryReact Native UIReact Native ComponentsExpoexpo-imageTypeScriptMobile DevelopmentiOSAndroidReact Native Open SourceSocial Feed UIFacebook LayoutInstagram Layout

react-native-image-collage: Build Facebook-Style Photo Grids in React Native

A complete guide to building Facebook and Instagram-style image collage layouts in React Native using the react-native-image-collage npm package — automatic grids, +N overflow, full-screen viewer, Expo support, and TypeScript.

FK

Faisal Khawaj

Author

8 min read

If you have ever built a social feed, a chat UI with photo attachments, or a gallery screen in React Native, you know the problem: laying out multiple images cleanly is tedious. One image is easy. Two side by side is manageable. Three, four, or five with a +N overflow badge on the last tile — that's where things get messy fast.

react-native-image-collage is an open-source npm package I built and maintain that handles this automatically. Drop in an array of image URLs, and the component picks the right layout — no manual flex math needed.

This guide walks through how it works, how to set it up, and how to use every feature — from basic grids to pinch-to-zoom full-screen viewers with Expo.


What it does

The package gives you Facebook and Instagram-style collage layouts that auto-select based on how many images you pass:

ImagesLayout
1Full width
2Side by side
3Large left, two stacked right
42×2 grid
5+Grid + +N overlay on the last tile

Width comes from the parent container automatically via onLayout. Height is derived from container width and image aspect ratios. You don't need to hardcode dimensions.

Three entry points cover different needs:

Entry pointRequiresUse when
react-native-image-collageDefault — RN CLI or Expo, no extras
react-native-image-collage/viewerreact-native-image-viewingFull-screen viewer, pinch/pan, double-tap
react-native-image-collage/expoexpo-imageBlurhash, advanced caching, pinch zoom

Install

Terminal
npm install react-native-image-collage
# or
yarn add react-native-image-collage

For Expo apps (recommended — uses expo-image for blurhash and caching):

Terminal
npx expo install react-native-image-collage expo-image

For the full-screen viewer (RN CLI — pinch / pan / double-tap):

Terminal
npm install react-native-image-viewing

No native linking required. The package uses only React Native core APIs.


Quick start

TypeScript
import { ImageCollage } from "react-native-image-collage";

export default function FeedPost() {
  const images = [
    { uri: "https://picsum.photos/seed/a/900/600", aspectRatio: 1.5 },
    { uri: "https://picsum.photos/seed/b/900/700", aspectRatio: 1.29 },
    { uri: "https://picsum.photos/seed/c/900/900", aspectRatio: 1 },
    { uri: "https://picsum.photos/seed/d/900/500", aspectRatio: 1.8 },
  ];

  return (
    <ImageCollage
      images={images}
      spacing={4}
      borderRadius={12}
      onImagePress={(index) => console.log("Tapped image", index)}
    />
  );
}

Four images → automatic 2×2 grid. Change the array length and the layout adjusts itself.


The images prop accepts three formats

You can pass images in whichever format suits your data:

TypeScript
// 1. Plain URL string
images={["https://example.com/photo.jpg"]}

// 2. React Native ImageSource object
images={[require("./assets/photo.jpg")]}

// 3. Object with optional aspect ratio (best performance — no async measurement)
images={[{ uri: "https://example.com/photo.jpg", aspectRatio: 1.5 }]}

When aspectRatio is omitted, the component measures it from the image before layout. Providing it upfront avoids a layout shift on first render.


Core props

PropTypeDefaultDescription
imagesCollageImageInput[]requiredURLs, sources, or { uri, aspectRatio }
spacingnumber6Gap between tiles in pixels
borderRadiusnumber12Corner radius on each tile
maxVisibleImagesnumber4Max tiles before +N overflow
layoutMinHeightnumber200Min layout height
layoutMaxHeightnumber520Max layout height
onImagePress(index) => voidTap handler per tile
placeholderColorstring#E8E8E8Loading tile background
renderImageCollageImageRendererRN ImageSwap in a custom renderer

Controlling overflow

By default, up to 4 tiles are visible. When you have more images, the last tile shows a +N badge.

TypeScript
// 6 images, only 3 visible → third tile shows "+3"
<ImageCollage
  images={sixPhotos}
  maxVisibleImages={3}
  onImagePress={(index) => openViewer(index)}
/>

The +N count is calculated automatically from images.length - maxVisibleImages.


Adding a full-screen viewer (RN CLI)

The /viewer entry wraps the collage with react-native-image-viewing — a popular swipeable, pinch-to-zoom full-screen modal.

TypeScript
import { ImageCollageWithViewer } from "react-native-image-collage/viewer";

export default function PhotoPost() {
  return (
    <ImageCollageWithViewer
      images={photos}
      spacing={4}
      borderRadius={10}
      viewerProps={{
        pinchToZoomEnabled: true,
        doubleTapToZoomEnabled: true,
        swipeToCloseEnabled: true,
      }}
    />
  );
}

Tapping any tile opens the full-screen viewer at the correct image index. Swipe down to close.


Expo entry — blurhash, caching, and pinch zoom

If your app uses Expo, the /expo entry uses expo-image as the renderer. You get blurhash placeholders, better caching, and built-in pinch-to-zoom without needing react-native-image-viewing.

TypeScript
import { ImageCollageWithViewer } from "react-native-image-collage/expo";

export default function GalleryPost() {
  return (
    <ImageCollageWithViewer
      images={photos}
      spacing={4}
      borderRadius={12}
      blurhash="LEHV6nWB2yk8pyo0adR*.7kCMdnj"
      prioritizeFirstImage
      viewerProps={{
        pinchToZoomEnabled: true,
        doubleTapToZoomEnabled: true,
        minScale: 1,
        maxScale: 3,
        doubleTapScale: 2.5,
      }}
    />
  );
}

prioritizeFirstImage tells expo-image to load the first tile at high priority and the rest at normal priority — a good default for feed posts where the first image is most important.


Using a custom image renderer

The renderImage prop lets you swap in any image component. This is useful if you want to use FastImage, a custom shimmer, or a CDN-optimized image component.

TypeScript
import FastImage from "react-native-fast-image";

<ImageCollage
  images={photos}
  renderImage={({ source, style, priority }) => (
    <FastImage
      source={source as FastImage.ImageSource}
      style={style}
      resizeMode="cover"
      priority={priority === "high" ? FastImage.priority.high : FastImage.priority.normal}
    />
  )}
/>

Real-world example: social feed post

Here is what a full feed post component looks like in practice:

TypeScript
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { ImageCollageWithViewer } from "react-native-image-collage/viewer";

type FeedPost = {
  author: string;
  avatar: string;
  body: string;
  images: string[];
  timestamp: string;
};

export function FeedPostCard({ post }: { post: FeedPost }) {
  return (
    <View style={styles.card}>
      <View style={styles.header}>
        <Image source={{ uri: post.avatar }} style={styles.avatar} />
        <View>
          <Text style={styles.author}>{post.author}</Text>
          <Text style={styles.time}>{post.timestamp}</Text>
        </View>
      </View>

      <Text style={styles.body}>{post.body}</Text>

      {post.images.length > 0 && (
        <ImageCollageWithViewer
          images={post.images}
          spacing={3}
          borderRadius={8}
          maxVisibleImages={4}
          viewerProps={{ pinchToZoomEnabled: true }}
        />
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  card: { backgroundColor: "#fff", borderRadius: 12, padding: 14, marginBottom: 12 },
  header: { flexDirection: "row", alignItems: "center", gap: 10, marginBottom: 10 },
  avatar: { width: 38, height: 38, borderRadius: 19 },
  author: { fontWeight: "700", fontSize: 15 },
  time: { color: "#888", fontSize: 12 },
  body: { fontSize: 15, lineHeight: 22, marginBottom: 10 },
});

Handling chat attachments

For a chat UI where messages may have one, two, or multiple images:

TypeScript
import { ImageCollage } from "react-native-image-collage";

function ChatBubbleImages({ uris }: { uris: string[] }) {
  if (uris.length === 0) return null;

  return (
    <View style={{ maxWidth: 260 }}>
      <ImageCollage
        images={uris}
        spacing={2}
        borderRadius={10}
        maxVisibleImages={4}
        layoutMaxHeight={300}
        onImagePress={(i) => openLightbox(uris, i)}
      />
    </View>
  );
}

The component width follows the parent View width — for a chat bubble that constrains itself to maxWidth: 260, the collage fills that space correctly.


TypeScript types

The package ships full TypeScript definitions. Key types:

TypeScript
import type {
  CollageImageInput,      // string | ImageSourcePropType | { uri, aspectRatio? }
  ImageCollageProps,      // full props interface for <ImageCollage>
  CollageImageRenderer,   // (props: CollageImageRenderProps) => ReactElement
  CollageViewerRenderer,  // (props: CollageViewerRenderProps) => ReactElement | null
} from "react-native-image-collage";

Compatibility

React Native0.72+ (including 0.86+ / Expo SDK 57)
React18+ / 19+
ExpoOptional — use /expo entry with expo-image
ViewerOptional — use /viewer entry with react-native-image-viewing
TypeScriptFull types included

FAQ

Does react-native-image-collage work with Expo?

Yes. You can use the base react-native-image-collage import in any Expo project. For blurhash placeholders and better caching, use the /expo entry with expo-image installed via npx expo install expo-image.

Does it need native linking?

No. The package uses only React Native core APIs — View, Image, Pressable, onLayout. No native modules and no pod install changes. The optional /expo entry uses expo-image, which Expo manages automatically.

How does the +N overflow work?

Set maxVisibleImages={3} with 6 images — you get a 3-tile layout where the third tile shows the last image dimmed with a +3 text overlay. Tapping it triggers onImagePress at that index so you can open a viewer at the right position.

Can I use a custom image component like FastImage?

Yes — pass a renderImage function. It receives { source, style, priority, transition } and you return any image element.

What happens with a single image?

A single image renders full width at a height derived from its aspect ratio, clamped between layoutMinHeight and layoutMaxHeight.


Links


Have questions or a bug to report? Open an issue on GitHub.

Building a social or chat app in React Native? Book a 15-min call or start a project — I ship production React Native apps for iOS and Android.

Keep reading

Published Aug 19, 2026 · 8 min read