šŸš€ Big News:Socket Has Acquired Secure Annex.Learn More →
Socket
Book a DemoSign in
Socket

expo-masonry-layout

Package Overview
Dependencies
Maintainers
1
Versions
31
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

expo-masonry-layout

High-performance masonry layout component for React Native and Expo applications

latest
Source
npmnpm
Version
2.1.1
Version published
Weekly downloads
244
-70.78%
Maintainers
1
Weekly downloads
Ā 
Created
Source

expo-masonry-layout

Expo Masonry Layout Demo

A high-performance masonry layout component for React Native and Expo applications

✨ Used in production by WiSaw - a location-based photo sharing app

npm version npm downloads license

✨ Features

  • šŸš€ High Performance: Uses VirtualizedList for optimal performance with large datasets
  • šŸ“± Responsive: Automatically adapts to screen size and orientation changes
  • šŸŽØ Flexible: Supports custom aspect ratios and layout configurations
  • šŸ”„ Interactive: Built-in pull-to-refresh and infinite scroll support
  • šŸ“ Dual Layout Modes: Row-based masonry with justified alignment and column-based masonry with shortest-column placement
  • šŸ“Š Responsive Columns: Column mode supports responsive breakpoints for adaptive column counts
  • šŸ“ Extra Height: getExtraHeight callback for adding dynamic per-item content (captions, badges, buttons) below images
  • šŸ” Inline Expand: Expand items to full-width detail view inline, with multi-expand support and automatic height measurement
  • šŸ“ Dynamic Height Measurement: Expanded items auto-measure their rendered height — supports dynamic content like accordions, comment lists, and reply forms
  • ļæ½ Shadow-Friendly: Item containers don't clip overflow, so shadows, badges, and decorations render correctly
  • ļæ½šŸŽÆ TypeScript: Full TypeScript support with comprehensive types
  • ⚔ Optimized: Minimal re-renders with memoized calculations

🌟 Real-world Usage

This component is actively used in production by:

  • WiSaw - A location-based photo sharing mobile app that displays thousands of user-generated photos in a beautiful masonry layout. WiSaw demonstrates the component's ability to handle large datasets with smooth scrolling and optimal performance.

The screenshot above is taken directly from the WiSaw app, showcasing real-world usage with actual user photos.

šŸš€ Installation

npm install expo-masonry-layout
# or
yarn add expo-masonry-layout


## šŸ“– Quick Start

```tsx
import React from 'react';
import { View, Image, Text } from 'react-native';
import ExpoMasonryLayout from 'expo-masonry-layout';

const MyMasonryGrid = () => {
  const data = [
    { id: '1', uri: 'https://example.com/image1.jpg', width: 300, height: 400 },
    { id: '2', uri: 'https://example.com/image2.jpg', width: 400, height: 300 },
    { id: '3', uri: 'https://example.com/image3.jpg', width: 300, height: 300 },
    // ... more items
  ];

  const renderItem = ({ item, dimensions }) => (
    <View style={{ width: dimensions.width, height: dimensions.height }}>
      <Image
        source={{ uri: item.uri }}
        style={{ width: '100%', height: '100%' }}
        resizeMode="cover"
      />
    </View>
  );

  return (
    <ExpoMasonryLayout
      data={data}
      renderItem={renderItem}
      spacing={6}
      keyExtractor={item => item.id}
    />
  );
};

šŸ–¼ļø Using with Expo Cached Image

For better performance with remote images, we recommend using expo-cached-image alongside the masonry layout:

npm install expo-cached-image
# or
yarn add expo-cached-image

Here's how to integrate it:

import React from 'react';
import { View, Dimensions } from 'react-native';
import ExpoMasonryLayout from 'expo-masonry-layout';
import { CachedImage } from 'expo-cached-image';

const CachedMasonryGrid = () => {
  const data = [
    { id: '1', uri: 'https://example.com/image1.jpg', width: 300, height: 400 },
    { id: '2', uri: 'https://example.com/image2.jpg', width: 400, height: 300 },
    { id: '3', uri: 'https://example.com/image3.jpg', width: 300, height: 300 }
    // ... more items
  ];

  const renderItem = ({ item, dimensions }) => (
    <View style={{ width: dimensions.width, height: dimensions.height }}>
      <CachedImage
        source={{ uri: item.uri }}
        style={{ width: '100%', height: '100%' }}
        resizeMode="cover"
        cacheKey={`masonry-${item.id}`} // Unique cache key
        placeholderContent={
          <View
            style={{
              flex: 1,
              backgroundColor: '#f0f0f0',
              justifyContent: 'center',
              alignItems: 'center'
            }}
          />
        }
      />
    </View>
  );

  return (
    <ExpoMasonryLayout
      data={data}
      renderItem={renderItem}
      spacing={6}
      keyExtractor={(item) => item.id}
    />
  );
};

Benefits of Using Expo Cached Image:

  • Automatic Caching: Images are cached locally after first load
  • Placeholder Support: Shows placeholder while loading
  • Better Performance: Reduces network requests for repeated views
  • Memory Management: Efficient image memory handling
  • Progressive Loading: Smooth loading experience

Performance Tips with Cached Images:

  • Use Unique Cache Keys: Ensure each image has a unique cacheKey prop
  • Optimize Image Sizes: Use appropriately sized images for your layout
  • Implement Placeholders: Provide placeholder content for better UX
  • Clear Cache When Needed: Implement cache clearing for updated content
// Example with cache management
import { CachedImage } from 'expo-cached-image';

const clearImageCache = async () => {
  await CachedImage.clearCache();
};

// Clear cache for specific images
const clearSpecificCache = async (imageId) => {
  await CachedImage.clearCache(`masonry-${imageId}`);
};

ļæ½ Column Layout Mode

Column-based masonry places items into columns of equal width, filling the shortest column first — the standard Pinterest-style layout:

import ExpoMasonryLayout from 'expo-masonry-layout';

const ColumnMasonryGrid = () => (
  <ExpoMasonryLayout
    data={data}
    renderItem={renderItem}
    layoutMode="column"
    columns={3}
    spacing={8}
  />
);

Responsive Column Count

Use breakpoints to adapt column count to screen width. Breakpoints are width ceilings evaluated from smallest up:

import ExpoMasonryLayout, { ColumnsConfig } from 'expo-masonry-layout';

// 1 column on phones (≤400), 2 on tablets (≤768), 3 on wider screens
const columns: ColumnsConfig = {
  default: 3,
  768: 2,
  400: 1,
};

const ResponsiveGrid = () => (
  <ExpoMasonryLayout
    data={data}
    renderItem={renderItem}
    layoutMode="column"
    columns={columns}
    spacing={8}
  />
);

šŸ“ Extra Height (getExtraHeight)

Add dynamic content below each item's image area using the getExtraHeight callback. Works in both row and column modes:

import ExpoMasonryLayout, { MasonryItem, MasonryRenderItemInfo } from 'expo-masonry-layout';

// Calculate extra height for caption text
const getExtraHeight = (item: MasonryItem, computedWidth: number): number => {
  if (!item.caption) return 0;
  const charsPerLine = Math.floor(computedWidth / 8);
  const numLines = Math.ceil((item.caption as string).length / charsPerLine);
  return numLines * 18 + 16; // lineHeight * lines + padding
};

const renderItem = ({ item, dimensions, extraHeight }: MasonryRenderItemInfo) => {
  const imageHeight = dimensions.height - extraHeight;
  return (
    <View style={{ width: dimensions.width, height: dimensions.height }}>
      <Image
        source={{ uri: item.imageUrl }}
        style={{ width: dimensions.width, height: imageHeight }}
        resizeMode="cover"
      />
      {item.caption ? (
        <Text style={{ padding: 8 }}>{item.caption as string}</Text>
      ) : null}
    </View>
  );
};

const CaptionGrid = () => (
  <ExpoMasonryLayout
    data={data}
    renderItem={renderItem}
    layoutMode="column"
    columns={2}
    spacing={8}
    getExtraHeight={getExtraHeight}
  />
);

How it works:

  • In row mode: row height becomes max(imageHeight + extraHeight) across all items in the row. Item widths are computed first (two-pass), then getExtraHeight is called with the final width.
  • In column mode: each item's total height is scaledImageHeight + extraHeight, stacked independently per column. Width is known upfront (screenWidth / numColumns).
  • extraHeight is passed in MasonryRenderItemInfo so your renderItem knows the image/extra split.

šŸ” Inline Expand

Expand items to a full-width detail view inline within the layout. Works in both column mode and row mode. Multiple items can be expanded simultaneously — the layout recalculates on each expand/collapse.

Expanded items are automatically measured after rendering, so getExpandedHeight is optional — provide it for a better initial estimate, but the layout self-corrects once the content renders:

import React, { useState, useCallback } from 'react';
import { Image, Text, View, TouchableOpacity } from 'react-native';
import ExpoMasonryLayout, { MasonryItem, MasonryRenderItemInfo } from 'expo-masonry-layout';

const getExpandedHeight = (item: MasonryItem, fullWidth: number): number => {
  const imageHeight = Math.floor(fullWidth / ((item.width ?? 300) / (item.height ?? 300)));
  return imageHeight + 200; // image + details area
};

const InlineExpandGrid = () => {
  const [expandedIds, setExpandedIds] = useState<string[]>([]);

  const toggleExpand = useCallback((id: string) => {
    setExpandedIds((prev) =>
      prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
    );
  }, []);

  const renderItem = useCallback(
    ({ item, dimensions, isExpanded }: MasonryRenderItemInfo) => {
      if (isExpanded) {
        const imageH = Math.floor(
          dimensions.width / ((item.width ?? 300) / (item.height ?? 300))
        );
        return (
          <TouchableOpacity onPress={() => toggleExpand(item.id)}>
            <Image
              source={{ uri: item.imageUrl }}
              style={{ width: dimensions.width, height: imageH }}
            />
            <View style={{ padding: 12 }}>
              <Text style={{ fontSize: 18 }}>{item.title}</Text>
              <Text>{item.description}</Text>
            </View>
          </TouchableOpacity>
        );
      }

      return (
        <TouchableOpacity onPress={() => toggleExpand(item.id)}>
          <Image
            source={{ uri: item.imageUrl }}
            style={{ width: dimensions.width, height: dimensions.height }}
          />
        </TouchableOpacity>
      );
    },
    [toggleExpand]
  );

  return (
    <ExpoMasonryLayout
      data={data}
      renderItem={renderItem}
      layoutMode="column"
      columns={3}
      spacing={8}
      expandedItemIds={expandedIds}
      getExpandedHeight={getExpandedHeight}
      autoScrollOnExpand
    />
  );
};

How it works:

  • In column mode: all columns flush to the waterline (max column height), the item spans full width, then columns resume below.
  • In row mode: the current row is flushed, the expanded item becomes a solo full-width row, then row packing resumes.
  • isExpanded is passed in MasonryRenderItemInfo so renderItem can branch between collapsed/expanded views.
  • When expanded, dimensions.width is the full grid width and dimensions.height comes from auto-measurement (or the getExpandedHeight estimate before measurement).
  • Auto-measurement: Expanded items are rendered without a fixed height constraint and measured via onLayout. The layout automatically recalculates when the measured height differs from the estimate. This means dynamic content (accordions, comment threads, reply forms) triggers re-layout automatically.
  • getExpandedHeight is optional — it provides an initial estimate before measurement. When omitted, screenWidth is used as the default estimate.
  • getExtraHeight is not applied to expanded items — the expanded height replaces the normal layout entirely.
  • autoScrollOnExpand automatically scrolls to items when they are expanded or collapsed. Pass { animated: false } to disable animation, or { viewOffset: 50 } to add padding above the item.
  • Use onExpandedItemLayout for custom scroll logic, or a ref with scrollToItem(id) for full control.

Imperative Height Notification

For cases where you need to programmatically notify the layout of a height change (e.g., after an async data load completes), use the notifyHeightChanged method on the ref:

const ref = useRef<ExpoMasonryLayoutHandle>(null);

// After loading comments, tell the layout the new height
const onCommentsLoaded = (itemId: string, newHeight: number) => {
  ref.current?.notifyHeightChanged(itemId, newHeight);
};

In most cases auto-measurement handles everything — notifyHeightChanged is only needed when you know the height before the next onLayout fires.

ļæ½šŸ”§ Advanced Usage

Here's a comprehensive example inspired by the WiSaw app implementation:

import React, { useState, useCallback } from 'react';
import { TouchableOpacity, Image, Text, View } from 'react-native';
import ExpoMasonryLayout, { MasonryRenderItemInfo } from 'expo-masonry-layout';

// Example data structure similar to WiSaw's photo feed
const PhotoMasonryGrid = () => {
  const [photos, setPhotos] = useState(initialPhotos);
  const [refreshing, setRefreshing] = useState(false);
  const [loading, setLoading] = useState(false);

  // Photo item renderer similar to WiSaw's implementation
  const renderPhotoItem = useCallback(
    ({ item, dimensions }: MasonryRenderItemInfo) => (
      <TouchableOpacity
        style={{
          width: dimensions.width,
          height: dimensions.height,
          borderRadius: 12,
          overflow: 'hidden',
          backgroundColor: '#f0f0f0',
          shadowColor: '#000',
          shadowOffset: { width: 0, height: 2 },
          shadowOpacity: 0.1,
          shadowRadius: 4,
          elevation: 3
        }}
        onPress={() => handlePhotoPress(item)}
        activeOpacity={0.9}
      >
        <Image
          source={{ uri: item.imageUrl }}
          style={{
            width: '100%',
            height: '85%'
          }}
          resizeMode="cover"
          loadingIndicatorSource={require('./placeholder.png')}
        />
        <View
          style={{
            position: 'absolute',
            bottom: 0,
            left: 0,
            right: 0,
            backgroundColor: 'rgba(0,0,0,0.7)',
            padding: 8
          }}
        >
          <Text
            style={{
              color: 'white',
              fontSize: 12,
              fontWeight: '600'
            }}
            numberOfLines={1}
          >
            šŸ“ {item.location}
          </Text>
          <Text
            style={{
              color: 'rgba(255,255,255,0.8)',
              fontSize: 10,
              marginTop: 2
            }}
          >
            ā¤ļø {item.likes} • šŸ‘¤ {item.username}
          </Text>
        </View>
      </TouchableOpacity>
    ),
    []
  );

  const handlePhotoPress = useCallback((photo) => {
    // Navigate to photo detail view (like in WiSaw)
    console.log('Photo pressed:', photo.id);
  }, []);

  const handleRefresh = useCallback(async () => {
    setRefreshing(true);
    try {
      // Fetch fresh photos from your API
      const freshPhotos = await fetchLatestPhotos();
      setPhotos(freshPhotos);
    } catch (error) {
      console.error('Error refreshing photos:', error);
    } finally {
      setRefreshing(false);
    }
  }, []);

  const handleLoadMore = useCallback(async () => {
    if (loading) return;

    setLoading(true);
    try {
      // Load more photos for infinite scroll
      const morePhotos = await fetchMorePhotos(photos.length);
      setPhotos((prevPhotos) => [...prevPhotos, ...morePhotos]);
    } catch (error) {
      console.error('Error loading more photos:', error);
    } finally {
      setLoading(false);
    }
  }, [photos.length, loading]);

  return (
    <ExpoMasonryLayout
      data={photos}
      renderItem={renderPhotoItem}
      spacing={8}
      maxItemsPerRow={2} // WiSaw uses 2 columns for optimal photo viewing
      baseHeight={200}
      keyExtractor={(item) => item.id}
      refreshing={refreshing}
      onRefresh={handleRefresh}
      onEndReached={handleLoadMore}
      onEndReachedThreshold={0.2}
      aspectRatioFallbacks={[0.7, 1.0, 1.3, 1.6]} // Common photo ratios
      style={{ backgroundColor: '#f8f9fa' }}
      contentContainerStyle={{ padding: 8 }}
      showsVerticalScrollIndicator={false}
      initialNumToRender={8}
      maxToRenderPerBatch={10}
      windowSize={15}
    />
  );
};

šŸ”§ VirtualizedList Pass-Through

The component now supports passing any VirtualizedList prop directly to the underlying implementation. This gives you full control over scrolling behavior, performance tuning, and platform-specific features:

import React, { useCallback } from 'react';
import ExpoMasonryLayout from 'expo-masonry-layout';

const AdvancedMasonryGrid = () => {
  const handleScroll = useCallback((event) => {
    console.log('Scroll position:', event.nativeEvent.contentOffset.y);
  }, []);

  const handleScrollBeginDrag = useCallback(() => {
    console.log('User started scrolling');
  }, []);

  return (
    <ExpoMasonryLayout
      data={photos}
      renderItem={renderPhotoItem}
      spacing={8}
      maxItemsPerRow={2}

      {/* VirtualizedList props passed through */}
      onScroll={handleScroll}
      onScrollBeginDrag={handleScrollBeginDrag}
      scrollEventThrottle={16}
      showsVerticalScrollIndicator={true}
      bounces={true}
      scrollEnabled={true}
      nestedScrollEnabled={true} // Android
      maintainVisibleContentPosition={{
        minIndexForVisible: 0,
        autoscrollToTopThreshold: 100,
      }}

      {/* Performance tuning */}
      initialNumToRender={10}
      maxToRenderPerBatch={5}
      windowSize={10}
      removeClippedSubviews={true}
      updateCellsBatchingPeriod={50}

      {/* Infinite scroll */}
      onEndReached={loadMoreData}
      onEndReachedThreshold={0.2}

      {/* Pull to refresh */}
      refreshing={isRefreshing}
      onRefresh={handleRefresh}
    />
  );
};

šŸ“‹ API Reference

Props

The component extends React Native's VirtualizedListProps and accepts all VirtualizedList properties in addition to the masonry-specific props below:

Masonry-Specific Props

PropTypeDefaultDescription
dataMasonryItem[]requiredArray of items to display
renderItem(info: MasonryRenderItemInfo) => ReactElementrequiredFunction to render each item
layoutMode'row' | 'column''row'Layout mode: row-based or column-based masonry
columnsnumber | ColumnsConfig2Number of columns or responsive breakpoint config (column mode only)
getExtraHeight(item: MasonryItem, computedWidth: number) => numberundefinedCalculate extra height below image area per item
expandedItemIdsstring[]undefinedItem IDs currently expanded to full width
getExpandedHeight(item: MasonryItem, fullWidth: number) => numberundefinedOptional initial height estimate for expanded items; auto-measurement corrects after render
spacingnumber6Space between items in pixels
maxItemsPerRownumber6Maximum number of items per row (row mode only)
baseHeightnumber100Base height for layout calculations (row mode only)
aspectRatioFallbacksnumber[][0.56, 0.67, 0.75, 1.0, 1.33, 1.5, 1.78]Fallback aspect ratios
preserveItemDimensionsbooleanfalseWhether to respect exact item dimensions when provided
getItemDimensions(item: MasonryItem, index: number) => { width: number; height: number } | nullundefinedFunction to calculate custom dimensions for items
keyExtractor(item: MasonryItem, index: number) => string(item, index) => item.id || indexExtract unique key for each item
onItemLayout(info: MasonryRenderItemInfo) => voidundefinedCallback when an item's layout dimensions are calculated
autoScrollOnExpandboolean | { animated?: boolean; viewOffset?: number }undefinedAuto-scroll to items when expanded or collapsed (column mode only)
onExpandedItemLayout(info: { item, index, dimensions, isExpanded }) => voidundefinedCallback fired for each item whose expand/collapse state changed

VirtualizedList Props

All VirtualizedList props are supported and passed through to the underlying implementation, including:

  • Performance: initialNumToRender, maxToRenderPerBatch, windowSize, updateCellsBatchingPeriod, removeClippedSubviews
  • Scrolling: onScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd, scrollEventThrottle
  • Interaction: onEndReached, onEndReachedThreshold, refreshing, onRefresh, scrollEnabled, bounces
  • Styling: style, contentContainerStyle, showsVerticalScrollIndicator
  • Platform: nestedScrollEnabled (Android), scrollIndicatorInsets (iOS)

šŸ”· Types

MasonryItem

interface MasonryItem {
  id: string;
  width?: number;
  height?: number;
  preserveDimensions?: boolean;
  [key: string]: any;
}

MasonryRenderItemInfo

interface MasonryRenderItemInfo {
  item: MasonryItem;
  index: number;
  dimensions: {
    width: number;
    height: number;
    left: number;
    top: number;
  };
  extraHeight: number;    // Extra height added below image (0 if no getExtraHeight)
  columnIndex?: number;   // Column index (column mode only, undefined when expanded)
  isExpanded: boolean;    // Whether item is in expanded full-width state
}

ColumnsConfig

type ColumnsConfig = number | { default: number; [breakpoint: number]: number };

ExpoMasonryLayoutHandle (Ref API)

The component supports React.forwardRef. Use a ref to access imperative scroll methods:

import { useRef } from 'react';
import ExpoMasonryLayout, { ExpoMasonryLayoutHandle } from 'expo-masonry-layout';

const ref = useRef<ExpoMasonryLayoutHandle>(null);

<ExpoMasonryLayout ref={ref} ... />

// Scroll to a specific item by ID
ref.current?.scrollToItem('item-5', { animated: true, viewOffset: 50 });

// Scroll to a specific offset
ref.current?.scrollToOffset(500, { animated: true });
MethodSignatureDescription
scrollToItem(id: string, options?: { animated?: boolean; viewOffset?: number }) => voidScroll to an item by ID. No-op if ID not found.
scrollToOffset(offset: number, options?: { animated?: boolean }) => voidScroll to an absolute offset. Works in both layout modes.
notifyHeightChanged(id: string, newHeight: number) => voidProgrammatically update the measured height of an expanded item, triggering re-layout.

šŸ”„ Custom Dimensions

The library supports multiple ways to override the automatic dimension calculation:

1. Per-Item Dimension Preservation

Set preserveDimensions: true on individual items to use their exact width and height:

const dataWithExactSizes = [
  {
    id: '1',
    width: 300,
    height: 200,
    preserveDimensions: true, // This item will be exactly 300x200
    imageUrl: 'https://example.com/image1.jpg'
  },
  {
    id: '2',
    width: 400,
    height: 300,
    // No preserveDimensions flag - will be auto-calculated
    imageUrl: 'https://example.com/image2.jpg'
  }
];

2. Global Dimension Preservation

Use the preserveItemDimensions prop to respect exact dimensions for all items that have width and height:

<ExpoMasonryLayout data={data} preserveItemDimensions={true} renderItem={renderItem} />

3. Custom Dimension Function

Use getItemDimensions for dynamic dimension calculation:

const getCustomDimensions = (item, index) => {
  // Make every 5th item extra wide
  if (index % 5 === 0) {
    return { width: 300, height: 150 };
  }

  // Featured items get special treatment
  if (item.featured) {
    return { width: 250, height: 200 };
  }

  // Return null for auto-calculation
  return null;
};

<ExpoMasonryLayout data={data} getItemDimensions={getCustomDimensions} renderItem={renderItem} />;

4. Mixed Layout Strategy

Combine all approaches for maximum flexibility:

const mixedData = [
  {
    id: '1',
    width: 200,
    height: 300,
    preserveDimensions: true // Exact size
  },
  {
    id: '2',
    featured: true // Will use getItemDimensions
  },
  {
    id: '3',
    width: 400,
    height: 300 // Will be auto-calculated unless preserveItemDimensions=true
  }
];

<ExpoMasonryLayout
  data={mixedData}
  preserveItemDimensions={false}
  getItemDimensions={(item, index) => {
    if (item.featured) return { width: 250, height: 200 };
    return null;
  }}
  renderItem={renderItem}
/>;

Priority Order:

  • getItemDimensions function result (highest priority)
  • preserveDimensions: true on item + item's width/height
  • preserveItemDimensions: true prop + item's width/height
  • Auto-calculated from aspect ratio (lowest priority)

ļæ½šŸŽÆ Performance Tips

  • Provide Image Dimensions: Include width and height in your data items for optimal layout calculation
  • Memoize Render Function: Use useCallback for your renderItem function
  • Optimize Images: Use appropriate image sizes and consider lazy loading
  • Key Extractor: Provide a stable keyExtractor function
  • Batch Size: Adjust maxToRenderPerBatch based on your item complexity

🧮 Layout Algorithm

The component supports two layout algorithms:

Row Mode (default)

  • Row Filling: Items are added to rows based on available width
  • Height Normalization: All items in a row are scaled to the same height
  • Width Justification: The entire row is scaled to fill the available width
  • Extra Height Pass: If getExtraHeight is provided, row height becomes max(imageHeight + extraHeight)
  • Expanded Items: When an item is expanded, the current row is flushed, the expanded item occupies a solo full-width row, and packing resumes in a new row below
  • Vertical Positioning: Items are vertically centered within their row

Column Mode

  • Column Width: Calculated as (screenWidth - spacing) / numColumns
  • Shortest-Column Placement: Each item is placed in the column with the smallest cumulative height
  • Height Calculation: Image height derived from aspect ratio, plus extraHeight if provided
  • Natural Band Boundaries: Items are sliced into horizontal bands for VirtualizedList rendering. Band boundaries are placed at y-coordinates where all columns have a gap between items, ensuring no item is split across bands. This eliminates touch dead zones and visible gaps between bands.
  • Shadow Support: Item containers do not clip overflow, allowing shadows and decorations to render outside item bounds

Both modes ensure:

  • Optimal use of screen space
  • Predictable layout behavior
  • Excellent performance with virtualization

šŸ“„ License

MIT

šŸ¤ Contributing

Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.

šŸ“ž Support

Made with ā¤ļø by Echowaves Corp.
Powering beautiful photo experiences in WiSaw and beyond

Keywords

react-native

FAQs

Package last updated on 03 May 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts