ContentCard
A compound React component for displaying content previews in a card format with cover images, badges, titles, and custom content areas. Built on react-native-paper Card component with theme integration and optimized image handling.
Overview
The ContentCard component provides a flexible, reusable solution for displaying content previews such as articles, videos, courses, or any media content. It features a compound component architecture that allows for maximum customization while maintaining design consistency.
Features
- Compound Components: Flexible sub-components for different card sections
- Cover Images: Optimized image covers with badge overlay support
- Theme Integration: Automatic adaptation to app themes and Material Design 3
- Touch Interactions: Built-in press handling with ripple effects
- Accessibility: Full accessibility support with proper labeling
- Image Optimization: Automatic image optimization through OptimizedImage
- Customizable Styling: Extensive styling options for all components
- Badge System: Overlay badges for highlighting featured content
Compound Components
ContentCard (Root)
The main card container that provides the base styling and interaction handling.
ContentCard.Cover
Cover image component with badge support and optimized image loading.
ContentCard.CoverBadge
Overlay badge component for displaying labels on cover images.
ContentCard.Title
Styled title component with truncation support and custom typography.
ContentCard.Content
Content container that extends Card.Content from react-native-paper.
Props
ContentCard (Root)
Extends all CardProps from react-native-paper except elevation:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Card content and sub-components |
style | ViewStyle | undefined | Additional styles for the card container |
contentStyle | ViewStyle | undefined | Styles for the card content area |
testID | string | undefined | Test identifier for automated testing |
onPress | function | undefined | Press handler for card interaction |
ContentCard.Cover
Extends OptimizedImageProps:
| Prop | Type | Default | Description |
|---|---|---|---|
uri | string | required | Image URI for the cover |
badge | string | (() => ReactNode) | undefined | Badge text or custom badge component function |
testID | string | undefined | Test identifier |
containerStyle | ViewStyle | undefined | Additional styles for the cover container |
ContentCard.CoverBadge
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | undefined | Badge text (required if children not provided) |
children | ReactNode | undefined | Custom badge content (required if label not provided) |
size | "small" | "medium" | "large" | "small" | Badge size variant |
style | ViewStyle | undefined | Additional badge container styles |
textStyle | TextStyle | undefined | Additional badge text styles |
testID | string | undefined | Test identifier |
ContentCard.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 |
style | TextStyle | undefined | Additional text styling |
testID | string | undefined | Test identifier |
Component Behavior
The ContentCard component provides several default behaviors:
- Default Elevation: Uses Material Design 3
containedmode for proper elevation and shadows - Theme Integration: Automatically uses
theme.colors.surfacefor background color - Touch Feedback: Provides Material Design ripple effects when
onPressis provided - Accessibility: Automatically sets
accessibilityRole="button"when interactive - Image Optimization: Cover images are automatically optimized through OptimizedImage integration
- Badge Positioning: Badges are positioned absolutely at the top-right corner of cover images
Localization
The ContentCard component does not include built-in text that requires localization. However, when used with localized content, ensure proper text handling:
import * as React from "react";
import { useTranslation } from "react-i18next";
import { ContentCard } from "@ovok/native";
const content = {
id: "localized-1",
title: "Localized Content Example",
imageUrl: "https://picsum.photos/300/200?random=localized",
featured: true,
};
const LocalizedContentCard = () => {
const { t } = useTranslation();
const handleOpenContent = () => {
console.log("Opening localized content:", content.title);
};
return (
<ContentCard
accessibilityLabel={t("content.card.accessibility", {
title: content.title,
})}
onPress={handleOpenContent}
>
<ContentCard.Cover
uri={content.imageUrl}
badge={content.featured ? t("content.badges.featured") : undefined}
/>
<ContentCard.Content>
<ContentCard.Title>{content.title}</ContentCard.Title>
</ContentCard.Content>
</ContentCard>
);
};
export default LocalizedContentCard;
Translation keys:
content.card.accessibility- Accessibility label for the cardcontent.badges.featured- Featured badge textcontent.badges.new- New content badge textcontent.badges.premium- Premium content badge text
Styling
Theme Colors Used
The ContentCard component uses the following theme values that can be changed globally:
theme.colors.surface- Card background colortheme.colors.onSurface- Title text colortheme.colors.primary- Badge text colortheme.spacing(4)- Cover paddingtheme.spacing(1)- Badge vertical paddingtheme.spacing(6)- Badge positioning offsetstheme.borderRadius(3)- Cover image border radiustheme.borderRadius(9999)- Badge border radius (fully rounded)
Custom Styling
Card Container Styling
<ContentCard
style={{
marginHorizontal: 16,
elevation: 4,
}}
contentStyle={{
paddingHorizontal: 12,
}}
>
<ContentCard.Title>Custom Styled Card</ContentCard.Title>
</ContentCard>
Cover Image Styling
<ContentCard.Cover
uri="https://example.com/image.jpg"
containerStyle={{
backgroundColor: "#f0f0f0",
borderRadius: 12,
}}
style={{
borderRadius: 8,
}}
/>
Badge Styling
<ContentCard.CoverBadge
label="Featured"
size="medium"
style={{
backgroundColor: "red",
borderRadius: 16,
}}
textStyle={{
color: "white",
fontWeight: "bold",
}}
/>
Title Styling
<ContentCard.Title
variant="titleMedium"
numberOfLines={3}
style={{
color: "#333",
fontSize: 18,
lineHeight: 24,
}}
>
Custom Styled Title
</ContentCard.Title>
Usage Patterns
Basic Content Card
import { ContentCard } from "@ovok/native";
import { useRouter } from "expo-router";
import * as React from "react";
const content = {
id: "1",
title: "Understanding React Native Development",
imageUrl: "https://picsum.photos/300/200?random=1",
};
const BasicContentCard = () => {
const router = useRouter();
return (
<ContentCard
onPress={() => router.navigate("/content-detail", { id: content.id })}
>
<ContentCard.Cover uri={content.imageUrl} />
<ContentCard.Content>
<ContentCard.Title>{content.title}</ContentCard.Title>
</ContentCard.Content>
</ContentCard>
);
};
export default BasicContentCard;
Featured Content Card
import { ContentCard } from "@ovok/native";
import * as React from "react";
const content = {
id: "2",
title: "Advanced Mobile Development Techniques",
imageUrl: "https://picsum.photos/300/200?random=2",
};
const FeaturedContentCard = () => {
const handleOpenContent = (content: typeof content) => {
console.log("Opening featured content:", content.title);
};
return (
<ContentCard onPress={() => handleOpenContent(content)}>
<ContentCard.Cover uri={content.imageUrl} badge="Featured" />
<ContentCard.Content>
<ContentCard.Title numberOfLines={2}>{content.title}</ContentCard.Title>
</ContentCard.Content>
</ContentCard>
);
};
export default FeaturedContentCard;
Custom Badge Content
import { ContentCard } from "@ovok/native";
import { Star } from "iconsax-react-nativejs";
import * as React from "react";
import { StyleSheet, View } from "react-native";
import { Text } from "react-native-paper";
const content = {
id: "3",
title: "Premium Mobile Development Course",
imageUrl: "https://picsum.photos/300/200?random=3",
};
const styles = StyleSheet.create({
customBadge: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "purple",
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
},
badgeText: {
color: "white",
fontSize: 12,
marginLeft: 4,
},
});
const CustomBadgeContentCard = () => {
const handleOpenContent = () => {
console.log("Opening premium content:", content.title);
};
return (
<ContentCard onPress={handleOpenContent}>
<ContentCard.Cover
uri={content.imageUrl}
badge={() => (
<View style={styles.customBadge}>
<Star size="32" color="#FF8A65" />
<Text style={styles.badgeText}>Premium</Text>
</View>
)}
/>
<ContentCard.Content>
<ContentCard.Title>{content.title}</ContentCard.Title>
</ContentCard.Content>
</ContentCard>
);
};
export default CustomBadgeContentCard;
Healthcare Content Card
import { ContentCard } from "@ovok/native";
import * as React from "react";
import { Text } from "react-native-paper";
const article = {
id: "health-1",
title: "Managing Hypertension in Primary Care",
imageUrl: "https://picsum.photos/300/200?random=health",
category: "Cardiology",
readingTime: "8 min read",
isLatest: true,
};
const HealthcareContentCard = () => {
const handleOpenHealthContent = () => {
console.log("Opening healthcare content:", article.title);
};
return (
<ContentCard onPress={handleOpenHealthContent}>
<ContentCard.Cover
uri={article.imageUrl}
badge={article.isLatest ? "Latest" : undefined}
/>
<ContentCard.Content>
<ContentCard.Title numberOfLines={2}>{article.title}</ContentCard.Title>
<Text variant="bodySmall" style={{ marginTop: 8 }}>
{article.category} • {article.readingTime}
</Text>
</ContentCard.Content>
</ContentCard>
);
};
export default HealthcareContentCard;
Educational Content Card
import * as React from "react";
import { View, StyleSheet } from "react-native";
import { Text, ProgressBar } from "react-native-paper";
import { ContentCard } from "@ovok/native";
const styles = StyleSheet.create({
courseCard: {
marginHorizontal: 8,
},
courseMetadata: {
flexDirection: "row",
alignItems: "center",
marginTop: 4,
gap: 4,
},
});
const course = {
id: "course-1",
title: "Advanced React Native Development",
thumbnailUrl: "https://picsum.photos/300/200?random=course",
difficulty: "Advanced" as const,
duration: "12 hours",
lessonsCount: 24,
progress: 0.35,
};
const EducationalContentCard = () => {
const handleStartCourse = () => {
console.log("Starting course:", course.title);
};
return (
<ContentCard onPress={handleStartCourse} style={styles.courseCard}>
<ContentCard.Cover uri={course.thumbnailUrl} badge={course.difficulty} />
<ContentCard.Content>
<ContentCard.Title>{course.title}</ContentCard.Title>
<View style={styles.courseMetadata}>
<Text variant="bodySmall">{course.duration}</Text>
<Text variant="bodySmall">•</Text>
<Text variant="bodySmall">{course.lessonsCount} lessons</Text>
</View>
<ProgressBar progress={course.progress} style={{ marginTop: 8 }} />
</ContentCard.Content>
</ContentCard>
);
};
export default EducationalContentCard;
News Article Card
import { ContentCard } from "@ovok/native";
import { formatDistanceToNow } from "date-fns";
import * as React from "react";
import { StyleSheet, View } from "react-native";
import { Text } from "react-native-paper";
const styles = StyleSheet.create({
articleMeta: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 8,
},
source: {
fontWeight: "600",
},
timestamp: {
opacity: 0.7,
},
});
const article = {
id: "news-1",
headline: "Breaking: New React Native Performance Updates Released",
imageUrl: "https://picsum.photos/300/200?random=news",
source: "Tech News",
publishedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
isBreaking: true,
};
const NewsArticleCard = () => {
const handleReadArticle = () => {
console.log("Reading article:", article.headline);
};
return (
<ContentCard onPress={handleReadArticle}>
<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" style={styles.source}>
{article.source}
</Text>
<Text variant="bodySmall" style={styles.timestamp}>
{formatDistanceToNow(article.publishedAt)}
</Text>
</View>
</ContentCard.Content>
</ContentCard>
);
};
export default NewsArticleCard;
List Integration
import { ContentCard } from "@ovok/native";
import * as React from "react";
import { FlatList, StyleSheet } from "react-native";
const contentItems = [
{
id: "1",
title: "React Native Fundamentals",
imageUrl: "https://picsum.photos/200/150?random=1",
category: "Development",
},
{
id: "2",
title: "Mobile UI Design Patterns",
imageUrl: "https://picsum.photos/200/150?random=2",
category: "Design",
},
{
id: "3",
title: "Performance Optimization",
imageUrl: "https://picsum.photos/200/150?random=3",
category: "Performance",
},
{
id: "4",
title: "Testing Strategies",
imageUrl: "https://picsum.photos/200/150?random=4",
category: "Testing",
},
];
const styles = StyleSheet.create({
gridCard: {
flex: 1,
margin: 8,
},
gridContainer: {
padding: 16,
},
});
const ContentCardGrid = () => {
const handleOpenContent = (item: (typeof contentItems)[0]) => {
console.log("Opening content:", item.title);
};
return (
<FlatList
data={contentItems}
numColumns={2}
renderItem={({ item }) => (
<ContentCard
style={styles.gridCard}
onPress={() => handleOpenContent(item)}
>
<ContentCard.Cover uri={item.imageUrl} badge={item.category} />
<ContentCard.Content>
<ContentCard.Title numberOfLines={2}>
{item.title}
</ContentCard.Title>
</ContentCard.Content>
</ContentCard>
)}
contentContainerStyle={styles.gridContainer}
keyExtractor={(item) => item.id}
/>
);
};
export default ContentCardGrid;
Badge Variants
Size Variants
import { ContentCard } from "@ovok/native";
import * as React from "react";
import { StyleSheet, View } from "react-native";
const styles = StyleSheet.create({
container: {
flexDirection: "column",
flexWrap: "wrap",
gap: 75,
},
});
const BadgeSizeVariants = () => {
return (
<View style={styles.container}>
{/* Small badge (default) */}
<View>
<ContentCard.CoverBadge label="New" size="small" />
</View>
{/* Medium badge */}
<View>
<ContentCard.CoverBadge label="Featured" size="medium" />
</View>
{/* Large badge */}
<View>
<ContentCard.CoverBadge label="Premium" size="large" />
</View>
</View>
);
};
export default BadgeSizeVariants;
Custom Badge Colors
import * as React from "react";
import { ContentCard, useAppTheme } from "@ovok/native";
const CustomBadgeColors = () => {
const theme = useAppTheme();
return (
<ContentCard.CoverBadge
label="Urgent"
style={{
backgroundColor: theme.colors.error,
}}
textStyle={{
color: theme.colors.onError,
}}
/>
);
};
export default CustomBadgeColors;
Performance Considerations
- Image Optimization: Uses OptimizedImage for automatic image optimization
- Memoization: Components are optimized for re-rendering performance
- Touch Handling: Efficient touch event handling with proper debouncing
- List Performance: Optimized for use in FlatList and other list components
Testing
Unit Testing
import { render, fireEvent, screen } from "@testing-library/react-native";
import { ContentCard } from "../content-card";
describe("ContentCard", () => {
it("renders correctly with title", () => {
render(
<ContentCard testID="content-card">
<ContentCard.Title>Test Title</ContentCard.Title>
</ContentCard>,
);
expect(screen.getByText("Test Title")).toBeTruthy();
expect(screen.getByTestId("content-card")).toBeTruthy();
});
it("handles press events", () => {
const onPress = jest.fn();
render(
<ContentCard testID="content-card" onPress={onPress}>
<ContentCard.Title>Test Title</ContentCard.Title>
</ContentCard>,
);
fireEvent.press(screen.getByTestId("content-card"));
expect(onPress).toHaveBeenCalled();
});
it("applies custom styles", () => {
const customStyle = { backgroundColor: "red" };
render(
<ContentCard style={customStyle} testID="content-card">
<ContentCard.Title>Test Title</ContentCard.Title>
</ContentCard>,
);
expect(screen.getByTestId("content-card")).toHaveStyle(customStyle);
});
});
Dependencies
react-native-paper: Card component and Material Design integration
Related Components
ContentDetail: Full content detail view componentContentList: List components for organizing multiple content itemsOptimizedImage: Image optimization and display componentTile: Alternative card-style components