ExpoStartup

CardScroller

A horizontal scrolling container for card-based content with optional title and "See all" link.

CardScroller

The CardScroller component provides a horizontal scrolling container perfect for displaying card elements, product listings, or any horizontally scrollable content. It includes options for a title, "See all" link, and snap-to-grid scrolling behavior.

Features

  • Horizontal Scrolling: Smooth horizontal scrolling for card elements
  • Optional Title: Display a section title above the scrolling content
  • See All Link: Optional link to a view with all content items
  • Snap Scrolling: Optional snap-to-grid scrolling behavior
  • Customizable Spacing: Control the gap between scrolling items
  • Styling Options: Custom classes and styles for flexible design

Import

import { CardScroller } from '@/components/CardScroller';

Props

PropTypeDefaultDescription
titlestringOptional title displayed above the scrolling content
imgstringOptional image URL (currently not used in the component)
allUrlstringURL for the "See all" link, if not provided the link won't be shown
enableSnappingbooleanfalseWhether to enable snap-to-grid scrolling behavior
snapIntervalnumber0Distance between snap points when snapping is enabled
classNamestringAdditional Tailwind classes for the container
styleViewStyleAdditional style object for more customization
spacenumber10Space between items in the scroll view
childrenReactNodeRequiredContent to render within the scroll view

Usage Examples

Basic Card Scroller

import { CardScroller } from '../components/CardScroller';
import Card from '../components/Card';
 
function ProductRow() {
  return (
    <CardScroller title="Featured Products">
      <Card title="Product 1" price="$19.99" />
      <Card title="Product 2" price="$29.99" />
      <Card title="Product 3" price="$39.99" />
      <Card title="Product 4" price="$49.99" />
    </CardScroller>
  );
}
<CardScroller 
  title="Popular Categories" 
  allUrl="/categories"
>
  <CategoryCard name="Electronics" icon="Smartphone" />
  <CategoryCard name="Fashion" icon="ShoppingBag" />
  <CategoryCard name="Home" icon="Home" />
  <CategoryCard name="Beauty" icon="Smile" />
</CardScroller>

With Snap Scrolling

<CardScroller 
  title="Featured Articles"
  enableSnapping={true}
  snapInterval={300} // Card width + spacing
  space={16}
>
  <ArticleCard 
    title="Getting Started" 
    image="https://example.com/image1.jpg" 
    width={284}
  />
  <ArticleCard 
    title="Advanced Tips" 
    image="https://example.com/image2.jpg" 
    width={284}
  />
  <ArticleCard 
    title="Best Practices" 
    image="https://example.com/image3.jpg" 
    width={284}
  />
</CardScroller>

Custom Styling

<CardScroller 
  title="Trending Now"
  className="bg-gray-100 dark:bg-gray-800 rounded-lg py-4"
  space={20}
>
  <ProductCard product={product1} />
  <ProductCard product={product2} />
  <ProductCard product={product3} />
</CardScroller>

Integration Examples

function HomeScreen() {
  const featuredProducts = [/* product data */];
  const newArrivals = [/* product data */];
  const topCategories = [/* category data */];
  
  return (
    <ScrollView className="flex-1">
      <View className="py-4">
        <CardScroller 
          title="Featured Products" 
          allUrl="/products/featured"
        >
          {featuredProducts.map(product => (
            <ProductCard 
              key={product.id} 
              product={product} 
            />
          ))}
        </CardScroller>
        
        <CardScroller 
          title="New Arrivals" 
          allUrl="/products/new"
          className="mt-6"
        >
          {newArrivals.map(product => (
            <ProductCard 
              key={product.id} 
              product={product} 
            />
          ))}
        </CardScroller>
        
        <CardScroller 
          title="Browse Categories" 
          allUrl="/categories"
          className="mt-6"
          enableSnapping={true}
          snapInterval={120}
          space={12}
        >
          {topCategories.map(category => (
            <CategoryBubble 
              key={category.id} 
              category={category} 
            />
          ))}
        </CardScroller>
      </View>
    </ScrollView>
  );
}

E-commerce Product Recommendations

function ProductDetailScreen({ product }) {
  const [relatedProducts, setRelatedProducts] = useState([]);
  const [recentlyViewed, setRecentlyViewed] = useState([]);
  
  useEffect(() => {
    // Fetch related products and recently viewed
    fetchRelatedProducts(product.id).then(setRelatedProducts);
    fetchRecentlyViewed().then(setRecentlyViewed);
  }, [product.id]);
  
  return (
    <ScrollView className="flex-1">
      {/* Product details */}
      <View className="p-4">
        <Text className="text-2xl font-bold">{product.name}</Text>
        <Text className="text-xl font-bold text-blue-500 mt-2">${product.price}</Text>
        {/* More product details */}
      </View>
      
      {/* Related products */}
      {relatedProducts.length > 0 && (
        <CardScroller 
          title="You Might Also Like" 
          allUrl={`/products/related/${product.id}`}
          className="mt-6"
          space={16}
        >
          {relatedProducts.map(relatedProduct => (
            <ProductCard 
              key={relatedProduct.id} 
              product={relatedProduct} 
              size="small"
            />
          ))}
        </CardScroller>
      )}
      
      {/* Recently viewed */}
      {recentlyViewed.length > 0 && (
        <CardScroller 
          title="Recently Viewed" 
          className="mt-6 mb-8"
          space={16}
        >
          {recentlyViewed.map(viewedProduct => (
            <ProductCard 
              key={viewedProduct.id} 
              product={viewedProduct} 
              size="small"
            />
          ))}
        </CardScroller>
      )}
    </ScrollView>
  );
}

Multiple CardScrollers with Different Content Types

function MediaLibraryScreen() {
  return (
    <ScrollView className="flex-1">
      <CardScroller 
        title="Continue Watching" 
        allUrl="/media/continue-watching"
        enableSnapping={true}
        snapInterval={280}
        space={16}
      >
        {/* Video cards with progress indicator */}
        <VideoCard 
          title="Episode 1" 
          thumbnail="https://example.com/ep1.jpg" 
          progress={0.7} 
        />
        <VideoCard 
          title="Episode 2" 
          thumbnail="https://example.com/ep2.jpg" 
          progress={0.3} 
        />
        <VideoCard 
          title="Movie Title" 
          thumbnail="https://example.com/movie.jpg" 
          progress={0.1} 
        />
      </CardScroller>
      
      <CardScroller 
        title="Trending Now" 
        allUrl="/media/trending"
        enableSnapping={true}
        snapInterval={200}
        space={16}
        className="mt-6"
      >
        {/* Poster cards */}
        <PosterCard 
          title="Show 1" 
          poster="https://example.com/show1.jpg" 
        />
        <PosterCard 
          title="Show 2" 
          poster="https://example.com/show2.jpg" 
        />
        <PosterCard 
          title="Show 3" 
          poster="https://example.com/show3.jpg" 
        />
      </CardScroller>
      
      <CardScroller 
        title="Genres" 
        allUrl="/media/genres"
        className="mt-6"
        space={12}
      >
        {/* Genre bubbles */}
        <GenreBubble name="Action" />
        <GenreBubble name="Comedy" />
        <GenreBubble name="Drama" />
        <GenreBubble name="Science Fiction" />
        <GenreBubble name="Horror" />
        <GenreBubble name="Romance" />
      </CardScroller>
    </ScrollView>
  );
}

Best Practices

Consistent Card Sizes

For the best visual appearance and scroll behavior, maintain consistent sizes for cards within a CardScroller:

// Good: Consistent card sizing
<CardScroller title="Featured Products">
  <ProductCard width={160} height={240} />
  <ProductCard width={160} height={240} />
  <ProductCard width={160} height={240} />
</CardScroller>
 
// Avoid: Inconsistent card sizes
<CardScroller title="Featured Products">
  <ProductCard width={160} height={240} />
  <ProductCard width={200} height={180} />
  <ProductCard width={140} height={260} />
</CardScroller>

Use Appropriate Spacing

Adjust spacing based on card size and content density:

// For small cards, use smaller spacing
<CardScroller 
  title="Categories" 
  space={8}
>
  {/* Small category bubbles */}
</CardScroller>
 
// For larger cards, use more spacing
<CardScroller 
  title="Featured Articles" 
  space={16}
>
  {/* Larger article cards */}
</CardScroller>

Enable Snapping for Better UX

When cards have a consistent width, enable snapping for a more polished user experience:

const cardWidth = 240;
const cardMargin = 16;
const snapInterval = cardWidth + cardMargin;
 
<CardScroller 
  title="Featured Products"
  enableSnapping={true}
  snapInterval={snapInterval}
  space={cardMargin}
>
  <ProductCard width={cardWidth} />
  <ProductCard width={cardWidth} />
  <ProductCard width={cardWidth} />
</CardScroller>

Implementation Details

The CardScroller component is built using React Native's ScrollView with horizontal scrolling enabled. It provides a container that handles horizontal overflow with proper padding and spacing between items.

The component renders an optional title and "See all" link at the top, which are aligned on opposite sides of the container. The "See all" link includes an underline animation for better visual feedback.

The scroll container uses negative margin and padding to ensure the scrolling content aligns properly with the rest of the UI while maintaining padding within the scroll area. This approach prevents the need for additional wrapper components.

For snapping behavior, the component uses ScrollView's snapToInterval and decelerationRate properties to create a satisfying scroll-and-snap experience when enabled.

Notes

  • The component adds a small empty view at the end of the scroll content to provide trailing padding
  • Although there's an img prop in the component interface, it's not currently used in the implementation
  • The "See all" link uses Expo Router's Link component for navigation
  • To ensure proper snapping behavior, make sure all cards have a consistent width
  • For best performance with many items, consider using a FlatList-based version for very long lists