Skip to main content

ContentList

A powerful compound React component for organizing and displaying multiple content items in flexible list layouts with sections, horizontal scrolling, and responsive design. Built on React Native FlatList with theme integration and performance optimizations.

Overview

The ContentList component provides a comprehensive solution for displaying structured content collections such as content categories, featured articles, course sections, or any grouped content. It features horizontal scrolling lists, section headers, and responsive card layouts that adapt to different screen sizes.

Features

  • Compound Components: Flexible sub-components for different list sections
  • Horizontal Scrolling: Smooth horizontal content scrolling with responsive card sizing
  • Section Organization: Structured sections with headers, titles, and action buttons
  • Responsive Design: Automatic card sizing based on screen dimensions
  • Theme Integration: Seamless theme adaptation and Material Design 3 support
  • Performance Optimization: Built on FlatList for optimal performance with large datasets
  • Accessibility: Full accessibility support with proper navigation and labeling
  • Internationalization: Built-in localization support for action buttons
  • Customizable Rendering: Flexible rendering options for different content types

Compound Components

ContentList (Root)

Main list container built on FlatList with theme-based spacing and performance optimizations.

ContentList.Section

Section container component that provides consistent spacing and layout for content groups.

ContentList.Header

Header component for sections that supports title and action button layouts.

ContentList.Title

Styled title component for section headers with Material Design 3 typography.

ContentList.HorizontalList

Horizontal scrolling list component with responsive card sizing and performance optimization.

ContentList.ViewAll

Action button component for viewing all items in a section with localization support.

Props

ContentList (Root)

Extends FlatListProps with generic type support:

PropTypeDefaultDescription
dataT[]requiredArray of content section data
renderItemListRenderItem<T>requiredFunction to render each content section
contentContainerStyleViewStyleundefinedAdditional styles for list content container

ContentList.Section

Extends ViewProps:

PropTypeDefaultDescription
childrenReactNoderequiredSection content including header and lists
styleViewStyleundefinedAdditional styling for section container

ContentList.Header

Extends ViewProps:

PropTypeDefaultDescription
childrenReactNoderequiredHeader content (typically title and actions)
styleViewStyleundefinedAdditional styling for header container

ContentList.Title

Extends TextProps from react-native-paper:

PropTypeDefaultDescription
childrenReactNoderequiredTitle text content
variantMD3TypescaleKeytitleLargeMaterial Design 3 typography variant

ContentList.HorizontalList

Extends FlatListProps with additional properties:

PropTypeDefaultDescription
dataT[]requiredArray of content items to display
renderItemListRenderItem<T>undefinedCustom render function (falls back to default)
horizontalPaddingnumber0Horizontal padding for the list

ContentList.ViewAll

Extends TextProps from react-native-paper:

PropTypeDefaultDescription
childrenstringundefinedCustom button text (overrides default i18n)
onPress() => voidundefinedPress handler for view all action
variantMD3TypescaleKeylabelSmallMaterial Design 3 typography variant

Component Behavior

The ContentList component provides efficient content organization with these behaviors:

  • Performance Optimization: Built on FlatList for optimal rendering of large content datasets
  • Responsive Design: Automatically calculates card widths based on screen dimensions
  • Theme Integration: Uses theme-based spacing and colors throughout the component tree
  • Horizontal Scrolling: Provides smooth horizontal scrolling with proper spacing and padding
  • Section Management: Organizes content into logical sections with headers and actions
  • Accessibility: Provides proper accessibility labels and navigation support
  • Internationalization: ViewAll component automatically uses localized text

Localization

The ContentList component uses the following localization key:

{
"content": {
"view-all": "View All"
}
}

Usage with custom localization:

import { ContentCard, ContentList } from "@ovok/native";
import * as React from "react";
import { useTranslation } from "react-i18next";

const sections = [
{
category: {
id: "wellness",
title: "Wellness Tips",
},
contents: [
{
id: "wellness-1",
title: "Daily Exercise Routine",
imageUrl: "https://picsum.photos/200/150?random=wellness1",
},
{
id: "wellness-2",
title: "Healthy Eating Habits",
imageUrl: "https://picsum.photos/200/150?random=wellness2",
},
],
},
{
category: {
id: "education",
title: "Health Education",
},
contents: [
{
id: "education-1",
title: "Understanding Blood Pressure",
imageUrl: "https://picsum.photos/200/150?random=education1",
},
],
},
];

const LocalizedContentList = () => {
const { t } = useTranslation();

const handleViewAll = (category: (typeof sections)[0]["category"]) => {
console.log("View all for category:", category.title);
};

const renderContentCard = ({
item,
}: {
item: (typeof sections)[0]["contents"][0];
}) => (
<ContentCard onPress={() => console.log("Opening content:", item.title)}>
<ContentCard.Cover uri={item.imageUrl} />
<ContentCard.Content>
<ContentCard.Title numberOfLines={2}>{item.title}</ContentCard.Title>
</ContentCard.Content>
</ContentCard>
);

return (
<ContentList
data={sections}
renderItem={({ item }) => (
<ContentList.Section>
<ContentList.Header>
<ContentList.Title>{item.category.title}</ContentList.Title>
<ContentList.ViewAll onPress={() => handleViewAll(item.category)}>
{t("content.see-more")}
</ContentList.ViewAll>
</ContentList.Header>

<ContentList.HorizontalList
data={item.contents}
renderItem={renderContentCard}
/>
</ContentList.Section>
)}
/>
);
};

export default LocalizedContentList;

Styling

Theme Colors Used

The ContentList component uses the following theme values that can be changed globally:

  • theme.colors.onSurfaceDisabled - ViewAll button text color
  • theme.spacing(5) - Gap between list sections
  • theme.spacing(3) - Gap within sections and between cards
  • theme.borderRadius(3) - Card border radius for default items

Custom Styling

List Container Styling

<ContentList
data={contentSections}
renderItem={renderSection}
contentContainerStyle={{
paddingHorizontal: 16,
paddingVertical: 20,
backgroundColor: "#f9f9f9",
}}
/>

Section Styling

<ContentList.Section
style={{
backgroundColor: "white",
borderRadius: 12,
padding: 16,
marginBottom: 16,
}}
>
<ContentList.Header>
<ContentList.Title>Featured Articles</ContentList.Title>
</ContentList.Header>
</ContentList.Section>

Horizontal List Styling

<ContentList.HorizontalList
data={items}
horizontalPadding={16}
style={{
backgroundColor: "transparent",
}}
contentContainerStyle={{
paddingVertical: 8,
}}
/>

Usage Patterns

Basic Content Sections

import { ContentCard, ContentList } from "@ovok/native";
import * as React from "react";

const contentSections = [
{
category: { title: "Mobile Development" },
contents: [
{
id: "1",
title: "React Native Best Practices",
imageUrl: "https://picsum.photos/200/150?random=1",
},
{
id: "2",
title: "TypeScript for Mobile Apps",
imageUrl: "https://picsum.photos/200/150?random=2",
},
],
},
{
category: { title: "UI/UX Design" },
contents: [
{
id: "3",
title: "Material Design Principles",
imageUrl: "https://picsum.photos/200/150?random=3",
},
],
},
];

const BasicContentSections = () => {
const handleViewAllContent = (category: { title: string }) => {
console.log("View all content for:", category.title);
};

const handleOpenContent = (content: { id: string; title: string }) => {
console.log("Opening content:", content.title);
};

return (
<ContentList
data={contentSections}
renderItem={({ item }) => (
<ContentList.Section>
<ContentList.Header>
<ContentList.Title>{item.category.title}</ContentList.Title>
<ContentList.ViewAll
onPress={() => handleViewAllContent(item.category)}
/>
</ContentList.Header>

<ContentList.HorizontalList
data={item.contents}
renderItem={({ item: content }) => (
<ContentCard onPress={() => handleOpenContent(content)}>
<ContentCard.Cover uri={content.imageUrl} />
<ContentCard.Content>
<ContentCard.Title>{content.title}</ContentCard.Title>
</ContentCard.Content>
</ContentCard>
)}
/>
</ContentList.Section>
)}
/>
);
};

export default BasicContentSections;

Healthcare Content Categories

const healthcareContent = [
{
category: { title: "Patient Education" },
contents: [
{ title: "Managing Diabetes", imageUrl: "...", type: "education" },
{ title: "Heart Health Tips", imageUrl: "...", type: "education" },
],
},
{
category: { title: "Clinical Guidelines" },
contents: [
{ title: "Hypertension Guidelines", imageUrl: "...", type: "guideline" },
{ title: "Medication Protocols", imageUrl: "...", type: "guideline" },
],
},
];

<ContentList
data={healthcareContent}
renderItem={({ item }) => (
<ContentList.Section>
<ContentList.Header>
<ContentList.Title>{item.category.title}</ContentList.Title>
<ContentList.ViewAll
onPress={() =>
navigation.navigate("CategoryView", {
category: item.category.title,
})
}
>
View All
</ContentList.ViewAll>
</ContentList.Header>

<ContentList.HorizontalList
data={item.contents}
horizontalPadding={16}
renderItem={({ item: content }) => (
<ContentCard
style={styles.healthcareCard}
onPress={() => openHealthcareContent(content)}
>
<ContentCard.Cover
uri={content.imageUrl}
badge={content.type === "guideline" ? "Clinical" : "Education"}
/>
<ContentCard.Content>
<ContentCard.Title numberOfLines={2}>
{content.title}
</ContentCard.Title>
</ContentCard.Content>
</ContentCard>
)}
/>
</ContentList.Section>
)}
/>;

Educational Course Sections

const courseSections = [
{
category: { title: "Featured Courses" },
contents: courses.filter((c) => c.featured),
},
{
category: { title: "Continue Learning" },
contents: courses.filter((c) => c.progress > 0),
},
{
category: { title: "Recommended for You" },
contents: recommendedCourses,
},
];

<ContentList
data={courseSections}
renderItem={({ item }) => (
<ContentList.Section>
<ContentList.Header>
<ContentList.Title>{item.category.title}</ContentList.Title>
{item.contents.length > 3 && (
<ContentList.ViewAll
onPress={() => viewAllCourses(item.category.title)}
/>
)}
</ContentList.Header>

<ContentList.HorizontalList
data={item.contents}
renderItem={({ item: course }) => (
<ContentCard
style={styles.courseCard}
onPress={() => startCourse(course)}
>
<ContentCard.Cover
uri={course.thumbnailUrl}
badge={course.difficulty}
/>
<ContentCard.Content>
<ContentCard.Title>{course.title}</ContentCard.Title>
<Text variant="bodySmall" style={styles.courseInfo}>
{course.duration}{course.lessonsCount} lessons
</Text>
{course.progress > 0 && (
<ProgressBar
progress={course.progress}
style={styles.progressBar}
/>
)}
</ContentCard.Content>
</ContentCard>
)}
/>
</ContentList.Section>
)}
/>;

News Categories

<ContentList
data={newsCategories}
renderItem={({ item }) => (
<ContentList.Section>
<ContentList.Header>
<ContentList.Title>{item.category.title}</ContentList.Title>
<ContentList.ViewAll
onPress={() =>
navigation.navigate("NewsCategory", {
category: item.category,
})
}
>
See More
</ContentList.ViewAll>
</ContentList.Header>

<ContentList.HorizontalList
data={item.contents}
renderItem={({ item: article }) => (
<ContentCard
style={styles.newsCard}
onPress={() => readArticle(article)}
>
<ContentCard.Cover
uri={article.imageUrl}
badge={article.isBreaking ? "Breaking" : undefined}
/>
<ContentCard.Content>
<ContentCard.Title numberOfLines={3}>
{article.headline}
</ContentCard.Title>
<View style={styles.articleMeta}>
<Text variant="bodySmall">{article.source}</Text>
<Text variant="bodySmall">
{formatTimeAgo(article.publishedAt)}
</Text>
</View>
</ContentCard.Content>
</ContentCard>
)}
/>
</ContentList.Section>
)}
/>

Mixed Content Types

<ContentList
data={mixedContent}
renderItem={({ item }) => (
<ContentList.Section>
<ContentList.Header>
<ContentList.Title>{item.category.title}</ContentList.Title>
<ContentList.ViewAll onPress={() => viewAll(item.category)} />
</ContentList.Header>

<ContentList.HorizontalList
data={item.contents}
renderItem={({ item: content }) => {
switch (content.type) {
case "video":
return (
<VideoCard
content={content}
onPress={() => playVideo(content)}
/>
);
case "article":
return (
<ContentCard onPress={() => readArticle(content)}>
<ContentCard.Cover uri={content.imageUrl} />
<ContentCard.Content>
<ContentCard.Title>{content.title}</ContentCard.Title>
</ContentCard.Content>
</ContentCard>
);
case "podcast":
return (
<PodcastCard
content={content}
onPress={() => playPodcast(content)}
/>
);
default:
return (
<ContentCard onPress={() => openContent(content)}>
<ContentCard.Title>{content.title}</ContentCard.Title>
</ContentCard>
);
}
}}
/>
</ContentList.Section>
)}
/>

Empty State Handling

<ContentList
data={contentSections}
renderItem={({ item }) => (
<ContentList.Section>
<ContentList.Header>
<ContentList.Title>{item.category.title}</ContentList.Title>
{item.contents.length > 0 && (
<ContentList.ViewAll onPress={() => viewAll(item.category)} />
)}
</ContentList.Header>

{item.contents.length > 0 ? (
<ContentList.HorizontalList
data={item.contents}
renderItem={renderContentItem}
/>
) : (
<EmptyState
title="No content available"
subtitle="Check back later for new content"
icon="content-copy"
/>
)}
</ContentList.Section>
)}
ListEmptyComponent={() => (
<EmptyState
title="No content found"
subtitle="Try refreshing or check your connection"
action={<Button onPress={refresh}>Refresh</Button>}
/>
)}
/>

Responsive Design

The HorizontalList component automatically calculates card widths based on screen size:

// Automatic card sizing calculation
const cardWidth = Math.min(
(SCREEN_WIDTH - horizontalPadding * 2 - CARD_MARGIN) / 1.5,
200, // Maximum width
);

Custom Card Sizing

<ContentList.HorizontalList
data={items}
renderItem={({ item }) => (
<ContentCard
style={{
width: useWindowDimensions().width * 0.8, // 80% of screen width
maxWidth: 300,
}}
>
<ContentCard.Title>{item.title}</ContentCard.Title>
</ContentCard>
)}
/>

Performance Optimization

Large Dataset Handling

<ContentList
data={largeSections}
renderItem={renderSection}
initialNumToRender={3}
maxToRenderPerBatch={2}
windowSize={5}
removeClippedSubviews={true}
getItemLayout={(data, index) => ({
length: SECTION_HEIGHT,
offset: SECTION_HEIGHT * index,
index,
})}
/>

Image Loading Optimization

<ContentList.HorizontalList
data={items}
renderItem={({ item }) => (
<ContentCard>
<ContentCard.Cover
uri={item.imageUrl}
priority={item.featured ? "high" : "normal"}
placeholder={item.placeholder}
/>
</ContentCard>
)}
/>

Accessibility

Screen Reader Support

<ContentList
accessible
accessibilityLabel="Content sections"
accessibilityHint="Swipe to navigate between content sections"
>
<ContentList.Section>
<ContentList.Header>
<ContentList.Title accessibilityRole="header" accessibilityLevel={2}>
Featured Articles
</ContentList.Title>
<ContentList.ViewAll
accessibilityLabel="View all featured articles"
accessibilityHint="Opens full list of featured articles"
/>
</ContentList.Header>
</ContentList.Section>
</ContentList>

Focus Management

<ContentList.HorizontalList
data={items}
renderItem={({ item }) => (
<ContentCard
accessible
accessibilityRole="button"
accessibilityLabel={`Article: ${item.title}`}
accessibilityHint="Tap to read full article"
onPress={() => openContent(item)}
/>
)}
/>

Internationalization

Localized Action Buttons

// The ViewAll component automatically uses i18n
<ContentList.ViewAll onPress={viewAll} />
// Renders localized "View All" text

// Custom localized text
<ContentList.ViewAll onPress={viewAll}>
{t('content.see-more')}
</ContentList.ViewAll>

RTL Support

<ContentList.HorizontalList
data={items}
horizontal
inverted={isRTL} // Automatic RTL support
renderItem={renderItem}
/>

Testing

Unit Testing

import { render, fireEvent, screen } from "@testing-library/react-native";
import { ContentList } from "../content-list";

const mockData = [
{
category: { title: "Test Category" },
contents: [{ title: "Test Article", imageUrl: "test.jpg" }],
},
];

describe("ContentList", () => {
it("renders sections correctly", () => {
render(
<ContentList
data={mockData}
renderItem={({ item }) => (
<ContentList.Section>
<ContentList.Title>{item.category.title}</ContentList.Title>
</ContentList.Section>
)}
/>,
);

expect(screen.getByText("Test Category")).toBeTruthy();
});

it("handles view all press", () => {
const onViewAll = jest.fn();

render(
<ContentList.ViewAll onPress={onViewAll}>View All</ContentList.ViewAll>,
);

fireEvent.press(screen.getByText("View All"));
expect(onViewAll).toHaveBeenCalled();
});
});

Dependencies

  • react-native: FlatList and core components
  • react-native-paper: Material Design components
  • react-i18next: Internationalization support
  • ContentCard: Content preview card component used in lists
  • ContentDetail: Full content detail view component
  • Tile: Alternative card-style components for lists