Product List
E-commerce product grid with category filter pills, a 2-column card layout, heart wishlist toggle, and per-product price and rating display.
Shop
Classic T-Shirt
$29.99
4.5
Denim Jacket
$89.99
4.8
Running Shoes
$119.99
4.6
Leather Bag
$149.99
4.7
Installation
npx @aniui/cli add text card badgeAlso requires react-native-safe-area-context for SafeAreaView.
Usage
Copy this screen into your app and wire up your product data source. Works with React Navigation or Expo Router.
import { ProductListScreen } from "@/screens/product-list";
// In your navigator:
<Stack.Screen name="ProductList" component={ProductListScreen} />Source
screens/product-list.tsx
import React, { useState } from "react";
import { View, ScrollView, Pressable, FlatList } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Text } from "@/components/ui/text";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
const CATEGORIES = ["All", "Clothing", "Shoes", "Accessories"];
const PRODUCTS = [
{ id: "1", name: "Classic T-Shirt", price: "$29.99", rating: 4.5, bg: "bg-primary/5 to bg-primary/10" },
{ id: "2", name: "Denim Jacket", price: "$89.99", rating: 4.8, bg: "bg-accent to bg-accent/50" },
{ id: "3", name: "Running Shoes", price: "$119.99", rating: 4.6, bg: "bg-secondary to bg-secondary/70" },
{ id: "4", name: "Leather Bag", price: "$149.99", rating: 4.7, bg: "bg-muted to bg-muted/60" },
];
function StarIcon() {
return (
<View style={{ width: 12, height: 12 }}>
{/* Inline SVG not available in RN — use a text star or react-native-svg */}
</View>
);
}
function HeartIcon({ filled }: { filled: boolean }) {
return null; // Replace with react-native-svg Heart
}
function FilterIcon() {
return null; // Replace with react-native-svg SlidersHorizontal
}
export function ProductListScreen() {
const [activeCategory, setActiveCategory] = useState("All");
const [liked, setLiked] = useState<string[]>([]);
const toggleLike = (id: string) => {
setLiked((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
);
};
return (
<SafeAreaView className="flex-1 bg-background">
{/* Header */}
<View className="flex-row items-center justify-between px-6 py-4">
<Text variant="h3" className="font-bold">Shop</Text>
<Pressable
accessible={true}
accessibilityRole="button"
accessibilityLabel="Filter products"
className="min-h-12 min-w-12 items-center justify-center rounded-full bg-muted"
>
{/* SlidersHorizontal icon via react-native-svg */}
</Pressable>
</View>
{/* Category pills */}
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
className="px-6 mb-4"
contentContainerStyle={{ gap: 8 }}
>
{CATEGORIES.map((cat) => (
<Pressable
key={cat}
accessible={true}
accessibilityRole="button"
accessibilityLabel={`Filter by ${cat}`}
onPress={() => setActiveCategory(cat)}
className={`rounded-full px-4 py-1.5 ${
activeCategory === cat
? "bg-primary"
: "bg-muted"
}`}
>
<Text
variant="small"
className={`font-medium ${
activeCategory === cat
? "text-primary-foreground"
: "text-muted-foreground"
}`}
>
{cat}
</Text>
</Pressable>
))}
</ScrollView>
{/* Product grid */}
<FlatList
data={PRODUCTS}
keyExtractor={(item) => item.id}
numColumns={2}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 32, gap: 12 }}
columnWrapperStyle={{ gap: 12 }}
renderItem={({ item }) => (
<Pressable
accessible={true}
accessibilityRole="button"
accessibilityLabel={`${item.name}, ${item.price}, rated ${item.rating} stars`}
className="flex-1"
>
<Card className="flex-1 rounded-2xl overflow-hidden border border-border shadow-sm">
<View className={`h-32 relative ${item.bg}`}>
<Pressable
accessible={true}
accessibilityRole="button"
accessibilityLabel="Save to wishlist"
onPress={() => toggleLike(item.id)}
className="absolute top-2 right-2 h-8 w-8 items-center justify-center rounded-full bg-background/80"
>
{/* Heart icon via react-native-svg */}
</Pressable>
</View>
<CardContent className="py-2.5 px-3">
<Text variant="small" className="font-medium text-foreground mb-0.5" numberOfLines={1}>
{item.name}
</Text>
<Text variant="small" className="font-semibold text-foreground mb-1">
{item.price}
</Text>
<View className="flex-row items-center gap-1">
{/* Star icon via react-native-svg */}
<Text variant="small" className="text-muted-foreground">{item.rating}</Text>
</View>
</CardContent>
</Card>
</Pressable>
)}
/>
</SafeAreaView>
);
}