Skip to main content

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:

PropTypeDefaultDescription
childrenReactNoderequiredCard content and sub-components
styleViewStyleundefinedAdditional styles for the card container
contentStyleViewStyleundefinedStyles for the card content area
testIDstringundefinedTest identifier for automated testing
onPressfunctionundefinedPress handler for card interaction

ContentCard.Cover

Extends OptimizedImageProps:

PropTypeDefaultDescription
uristringrequiredImage URI for the cover
badgestring | (() => ReactNode)undefinedBadge text or custom badge component function
testIDstringundefinedTest identifier
containerStyleViewStyleundefinedAdditional styles for the cover container

ContentCard.CoverBadge

PropTypeDefaultDescription
labelstringundefinedBadge text (required if children not provided)
childrenReactNodeundefinedCustom badge content (required if label not provided)
size"small" | "medium" | "large""small"Badge size variant
styleViewStyleundefinedAdditional badge container styles
textStyleTextStyleundefinedAdditional badge text styles
testIDstringundefinedTest identifier

ContentCard.Title

Extends TextProps from react-native-paper:

PropTypeDefaultDescription
childrenReactNoderequiredTitle text content
variantMD3TypescaleKeytitleLargeMaterial Design 3 typography variant
styleTextStyleundefinedAdditional text styling
testIDstringundefinedTest identifier

Component Behavior

The ContentCard component provides several default behaviors:

  • Default Elevation: Uses Material Design 3 contained mode for proper elevation and shadows
  • Theme Integration: Automatically uses theme.colors.surface for background color
  • Touch Feedback: Provides Material Design ripple effects when onPress is 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 card
  • content.badges.featured - Featured badge text
  • content.badges.new - New content badge text
  • content.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 color
  • theme.colors.onSurface - Title text color
  • theme.colors.primary - Badge text color
  • theme.spacing(4) - Cover padding
  • theme.spacing(1) - Badge vertical padding
  • theme.spacing(6) - Badge positioning offsets
  • theme.borderRadius(3) - Cover image border radius
  • theme.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;
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
  • ContentDetail: Full content detail view component
  • ContentList: List components for organizing multiple content items
  • OptimizedImage: Image optimization and display component
  • Tile: Alternative card-style components