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:
| Prop | Type | Default | Description |
|---|---|---|---|
data | T[] | required | Array of content section data |
renderItem | ListRenderItem<T> | required | Function to render each content section |
contentContainerStyle | ViewStyle | undefined | Additional styles for list content container |
ContentList.Section
Extends ViewProps:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Section content including header and lists |
style | ViewStyle | undefined | Additional styling for section container |
ContentList.Header
Extends ViewProps:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Header content (typically title and actions) |
style | ViewStyle | undefined | Additional styling for header container |
ContentList.Title
Extends TextProps from react-native-paper:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Title text content |
variant | MD3TypescaleKey | titleLarge | Material Design 3 typography variant |
ContentList.HorizontalList
Extends FlatListProps with additional properties:
| Prop | Type | Default | Description |
|---|---|---|---|
data | T[] | required | Array of content items to display |
renderItem | ListRenderItem<T> | undefined | Custom render function (falls back to default) |
horizontalPadding | number | 0 | Horizontal padding for the list |
ContentList.ViewAll
Extends TextProps from react-native-paper:
| Prop | Type | Default | Description |
|---|---|---|---|
children | string | undefined | Custom button text (overrides default i18n) |
onPress | () => void | undefined | Press handler for view all action |
variant | MD3TypescaleKey | labelSmall | Material 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 colortheme.spacing(5)- Gap between list sectionstheme.spacing(3)- Gap within sections and between cardstheme.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 componentsreact-native-paper: Material Design componentsreact-i18next: Internationalization support
Related Components
ContentCard: Content preview card component used in listsContentDetail: Full content detail view componentTile: Alternative card-style components for lists