List Products Grid Block
The ListProductsGrid Component is a fully customizable and accessible grid layout for displaying products built with React and TypeScript. It provides a complete product listing interface with modern design patterns, flexible customization options, and composable sub-components for titles, items, and cards.
π Installationβ
npm install @nodeblocks/frontend-list-products-grid-block@0.2.0
π Usageβ
import {ListProductsGrid} from '@nodeblocks/frontend-list-products-grid-block';
- Basic Usage
- Advanced Usage
function SimpleListProductsGrid() { const products = [ { id: '1', title: 'Wireless Headphones', description: 'Premium noise-cancelling headphones', price: '$199.99', imageUrl: 'https://placehold.co/600x400', badges: ['New'], }, { id: '2', title: 'Smart Watch', description: 'Fitness tracking and notifications', price: '$299.99', originalPrice: '$349.99', imageUrl: 'https://placehold.co/600x400', badges: ['Sale'], }, { id: '3', title: 'Laptop Stand', description: 'Ergonomic aluminum stand', price: '$79.99', imageUrl: 'https://placehold.co/600x400', }, { id: '4', title: 'Mechanical Keyboard', description: 'RGB backlit gaming keyboard', price: '$149.99', imageUrl: 'https://placehold.co/600x400', badges: ['Popular'], }, ]; const handleNavigate = (href) => console.log('Navigate:', href); return ( <ListProductsGrid listProductsGridTitle="Featured Products" subtitle="Check out our top picks"> <ListProductsGrid.Title /> <ListProductsGrid.Items> {products.map(product => ( <ListProductsGrid.Items.GridCard key={product.id} title={product.title} imageUrl={product.imageUrl} summary={product.description} chips={product.badges?.map(b => ({label: b}))} linkProps={{href: `/products/${product.id}`, onNavigate: handleNavigate}} button={{children: product.price}} /> ))} </ListProductsGrid.Items> </ListProductsGrid> ); }
function AdvancedListProductsGrid() { const [currentPage, setCurrentPage] = useState(1); const [sortBy, setSortBy] = useState('featured'); const [viewMode, setViewMode] = useState('grid'); const products = [ { id: '1', title: 'Pro Wireless Earbuds', description: 'Active noise cancellation with 24h battery life', price: '$249.99', originalPrice: '$299.99', imageUrl: 'https://placehold.co/600x400', badges: ['Best Seller', '-17%'], rating: 4.8, reviewCount: 1234, isFeatured: true, }, { id: '2', title: 'Ultra HD Monitor 32"', description: '4K resolution with HDR support', price: '$599.99', imageUrl: 'https://placehold.co/600x400', badges: ['New Arrival'], rating: 4.6, reviewCount: 567, isNew: true, }, { id: '3', title: 'Ergonomic Office Chair', description: 'Lumbar support and adjustable armrests', price: '$449.99', originalPrice: '$549.99', imageUrl: 'https://placehold.co/600x400', badges: ['Sale'], rating: 4.7, reviewCount: 890, }, { id: '4', title: 'Portable Power Bank', description: '20000mAh fast charging', price: '$69.99', imageUrl: 'https://placehold.co/600x400', rating: 4.5, reviewCount: 2345, }, { id: '5', title: 'Wireless Gaming Mouse', description: 'Ultra-low latency, RGB lighting', price: '$129.99', imageUrl: 'https://placehold.co/600x400', badges: ['Popular'], rating: 4.9, reviewCount: 3456, }, { id: '6', title: 'USB-C Hub 10-in-1', description: 'Universal connectivity solution', price: '$89.99', originalPrice: '$109.99', imageUrl: 'https://placehold.co/600x400', rating: 4.4, reviewCount: 789, }, ]; const handleAddToCart = (productId) => { console.log('Add to cart:', productId); }; const handleAddToWishlist = (productId) => { console.log('Add to wishlist:', productId); }; return ( <ListProductsGrid listProductsGridTitle="Products" subtitle={`Showing ${products.length} of 120 products`} sx={{ maxWidth: 1400, mx: 'auto', px: 3, }} > {({defaultBlocks}) => { const customTitle = ( <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', padding: '16px 0', borderBottom: '1px solid #e2e8f0', }} > <div> <h1 style={{ fontSize: '28px', fontWeight: '700', color: '#1e293b', margin: '0 0 4px 0', }} > Products </h1> <p style={{fontSize: '14px', color: '#64748b', margin: 0}}>Showing {products.length} of 120 products</p> </div> <div style={{display: 'flex', gap: '12px', alignItems: 'center'}}> <select value={sortBy} onChange={e => setSortBy(e.target.value)} style={{ padding: '10px 16px', borderRadius: '8px', border: '1px solid #e2e8f0', fontSize: '14px', color: '#475569', cursor: 'pointer', }} > <option value="featured">Featured</option> <option value="newest">Newest</option> <option value="price-low">Price: Low to High</option> <option value="price-high">Price: High to Low</option> <option value="rating">Highest Rated</option> </select> <div style={{ display: 'flex', borderRadius: '8px', overflow: 'hidden', border: '1px solid #e2e8f0', }} > <button onClick={() => setViewMode('grid')} style={{ padding: '10px 14px', background: viewMode === 'grid' ? '#6366f1' : '#ffffff', color: viewMode === 'grid' ? '#ffffff' : '#64748b', border: 'none', cursor: 'pointer', fontSize: '16px', }} > β </button> <button onClick={() => setViewMode('list')} style={{ padding: '10px 14px', background: viewMode === 'list' ? '#6366f1' : '#ffffff', color: viewMode === 'list' ? '#ffffff' : '#64748b', border: 'none', borderLeft: '1px solid #e2e8f0', cursor: 'pointer', fontSize: '16px', }} > β° </button> </div> </div> </div> ); const customItems = ( <div style={{ display: 'grid', gridTemplateColumns: viewMode === 'grid' ? 'repeat(auto-fill, minmax(280px, 1fr))' : '1fr', gap: '24px', }} > {products.map(product => ( <div key={product.id} style={{ display: viewMode === 'list' ? 'flex' : 'block', gap: viewMode === 'list' ? '24px' : '0', background: '#ffffff', borderRadius: '16px', overflow: 'hidden', border: '1px solid #e2e8f0', transition: 'all 0.2s ease', cursor: 'pointer', }} > <div style={{ position: 'relative', width: viewMode === 'list' ? '200px' : '100%', aspectRatio: '1', background: '#f8fafc', flexShrink: 0, }} > <img src={product.imageUrl} alt={product.title} style={{ width: '100%', height: '100%', objectFit: 'cover', }} /> {product.badges && product.badges.length > 0 && ( <div style={{ position: 'absolute', top: '12px', left: '12px', display: 'flex', flexWrap: 'wrap', gap: '6px', }} > {product.badges.map((badge, i) => ( <span key={i} style={{ padding: '4px 10px', borderRadius: '6px', fontSize: '11px', fontWeight: '600', background: badge.includes('%') ? '#ef4444' : badge === 'New Arrival' ? '#10b981' : '#6366f1', color: '#ffffff', }} > {badge} </span> ))} </div> )} <button onClick={e => { e.stopPropagation(); handleAddToWishlist(product.id); }} style={{ position: 'absolute', top: '12px', right: '12px', width: '36px', height: '36px', borderRadius: '50%', background: '#ffffff', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '16px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)', }} > β‘ </button> </div> <div style={{padding: '16px', flex: 1}}> <h3 style={{ fontSize: '16px', fontWeight: '600', color: '#1e293b', margin: '0 0 8px 0', }} > {product.title} </h3> {product.description && ( <p style={{ fontSize: '14px', color: '#64748b', margin: '0 0 12px 0', lineHeight: '1.5', }} > {product.description} </p> )} {product.rating && ( <div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '12px', }} > <span style={{color: '#f59e0b', fontSize: '14px'}}>{'β '.repeat(Math.floor(product.rating))}</span> <span style={{ fontSize: '13px', color: '#64748b', }} > {product.rating} ({product.reviewCount}) </span> </div> )} <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', }} > <div style={{display: 'flex', alignItems: 'baseline', gap: '8px'}}> <span style={{ fontSize: '20px', fontWeight: '700', color: '#1e293b', }} > {product.price} </span> {product.originalPrice && ( <span style={{ fontSize: '14px', color: '#94a3b8', textDecoration: 'line-through', }} > {product.originalPrice} </span> )} </div> <button onClick={e => { e.stopPropagation(); handleAddToCart(product.id); }} style={{ padding: '10px 20px', borderRadius: '10px', background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', color: '#ffffff', border: 'none', fontSize: '14px', fontWeight: '600', cursor: 'pointer', }} > Add to Cart </button> </div> </div> </div> ))} </div> ); const customPagination = ( <div style={{ display: 'flex', justifyContent: 'center', gap: '8px', marginTop: '32px', paddingTop: '24px', borderTop: '1px solid #e2e8f0', }} > {[1, 2, 3, 4, 5].map(page => ( <button key={page} onClick={() => setCurrentPage(page)} style={{ width: '40px', height: '40px', borderRadius: '10px', border: 'none', background: page === currentPage ? '#6366f1' : '#f1f5f9', color: page === currentPage ? '#ffffff' : '#475569', fontSize: '14px', fontWeight: '600', cursor: 'pointer', }} > {page} </button> ))} </div> ); return { blocks: { ...defaultBlocks, title: customTitle, items: customItems, pagination: customPagination, }, blockOrder: ['title', 'items', 'pagination'], }; }} </ListProductsGrid> ); }
π§ Props Referenceβ
Main Component Propsβ
| Prop | Type | Default | Description |
|---|---|---|---|
listProductsGridTitle | ReactNode | undefined | Main title for the product list |
subtitle | string | undefined | Secondary title displayed above the main title |
spacing | number | 5 | Spacing between child elements |
className | string | undefined | Additional CSS class name for styling the grid container |
children | BlocksOverride | undefined | Custom block components to override default rendering |
Note: The main component extends MUI StackProps. Default styling includes px: 2 and py: 5 padding.
Sub-Componentsβ
The ListProductsGrid component provides several sub-components. All sub-components receive their default values from the main component's context and can override these values through props.
ListProductsGrid.Titleβ
Container for the grid title section with subtitle and main title.
| Prop | Type | Default | Description |
|---|---|---|---|
listProductsGridTitle | ReactNode | From context | Content to display as the main title |
subtitle | string | From context | Content to display as the subtitle (displayed above title) |
spacing | number | 0.5 | Spacing between subtitle and title |
children | ReactNode | Default title layout | Custom content to override the default title layout |
className | string | undefined | Additional CSS class name for styling |
Note: This component extends MUI StackProps. Default styling includes alignItems: 'center'.
ListProductsGrid.Itemsβ
Container for the product grid cards.
| Prop | Type | Default | Description |
|---|---|---|---|
spacing | number | 2 | Spacing between grid items |
children | ReactNode | undefined | Grid card components to render |
className | string | undefined | Additional CSS class name for styling |
Note: This component extends MUI StackProps.
ListProductsGrid.Items.GridCardβ
Individual product card component with rich content options.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | Required | Main title displayed on the card |
subtitle | string | undefined | Secondary text displayed below the title |
subtitleImageUrl | string | undefined | URL of a small image displayed next to the subtitle |
summary | string | undefined | Brief description or summary text |
imageUrl | string | undefined | URL of the main image displayed on the card |
tertiaryText | string | undefined | Additional information displayed above the title |
content | string | undefined | Unrestricted text content for the card |
chips | ChipProps[] | undefined | Array of MUI Chip props with optional leftIcon, rightIcon |
tags | Array<{icon?: ReactElement, label: string | string[]}> | undefined | Array of tag objects with icons and labels |
titleIcon | ReactElement | undefined | Icon displayed before the title |
titleAction | IconButtonProps | undefined | Action button displayed after the title |
button | ButtonProps | undefined | Main call-to-action button for the card |
status | StatusObject | undefined | Status callout with label and optional action |
linkProps | LinkProps | undefined | Navigation properties for making the card clickable |
hasDivider | boolean | false | Add divider between card content sections |
disabled | boolean | false | Set card style to disabled state |
className | string | undefined | Additional CSS class name for styling |
Note: This component extends MUI CardProps.
π¨ Configuration examplesβ
Custom Grid Layout:
<ListProductsGrid
listProductsGridTitle="Featured Products"
subtitle="Best Sellers"
spacing={3}
sx={{ bgcolor: '#f5f5f5', borderRadius: 2 }}
>
<ListProductsGrid.Title sx={{ textAlign: 'left', alignItems: 'flex-start' }} />
<ListProductsGrid.Items
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: 3
}}
>
{products.map((product, i) => (
<ListProductsGrid.Items.GridCard key={i} {...product} />
))}
</ListProductsGrid.Items>
</ListProductsGrid>
Card with Status and Actions:
<ListProductsGrid.Items.GridCard
title="Premium Headphones"
subtitle="Sony Electronics"
imageUrl="/images/headphones.jpg"
chips={[{ label: 'New', color: 'primary' }]}
status={{
label: 'In Stock',
color: 'primary',
action: {
children: 'View Details',
href: '/products/123',
onNavigate: (href) => router.push(href)
}
}}
button={{
children: 'Add to Cart',
variant: 'contained',
color: 'primary'
}}
linkProps={{
href: '/products/123',
onNavigate: (href) => router.push(href)
}}
/>
Block Override Pattern:
<ListProductsGrid
listProductsGridTitle="Shop Collection"
subtitle="Curated Items"
>
{({defaultBlocks}) => ({
blocks: {
...defaultBlocks,
title: customTitle,
items: customItems,
pagination: customPagination,
},
blockOrder: ['title', 'items', 'pagination'],
})}
</ListProductsGrid>
π§ TypeScript Supportβ
Full TypeScript support with comprehensive type definitions:
import {ListProductsGrid} from '@nodeblocks/frontend-list-products-grid-block';
import {ReactElement} from 'react';
import {ChipProps, ButtonProps, IconButtonProps} from '@mui/material';
// Product grid item interface
interface ProductGridItem {
id: string;
title: string;
description?: string;
price: string;
originalPrice?: string;
imageUrl: string;
badges?: string[];
rating?: number;
reviewCount?: number;
isNew?: boolean;
isFeatured?: boolean;
}
// Link props for card navigation
interface LinkProps {
href?: string;
onClick?: (e: React.MouseEvent) => void;
onNavigate: ((href: string) => void) | 'standard-html-link';
openInNewTab?: boolean;
}
// Status object type
interface StatusObject {
label: string;
color?: 'default' | 'primary';
action?: Partial<ButtonProps> & {
href?: string;
onNavigate?: ((href: string) => void) | 'standard-html-link';
openInNewTab?: boolean;
};
}
// Grid card props type
interface GridCardProps {
title: string;
subtitle?: string;
subtitleImageUrl?: string;
summary?: string;
imageUrl?: string;
tertiaryText?: string;
content?: string;
chips?: (ChipProps & {
leftIcon?: ReactElement;
rightIcon?: ReactElement;
label: string;
})[];
tags?: {icon?: ReactElement; label: string | string[]}[];
titleIcon?: ReactElement;
titleAction?: Partial<IconButtonProps>;
button?: Partial<ButtonProps>;
status?: StatusObject;
linkProps?: LinkProps;
hasDivider?: boolean;
disabled?: boolean;
}
function TypedListProductsGrid() {
const productData: ProductGridItem[] = [
{
id: 'prod-1',
title: 'Premium Laptop',
description: '15" display, 16GB RAM, 512GB SSD',
price: '$1,299.99',
imageUrl: 'https://placehold.co/600x400',
badges: ['New'],
rating: 4.8,
reviewCount: 256,
},
{
id: 'prod-2',
title: 'Wireless Charger',
description: 'Fast charging, compatible with all Qi devices',
price: '$39.99',
imageUrl: 'https://placehold.co/600x400',
rating: 4.5,
reviewCount: 1024,
},
];
const handleNavigate = (href: string): void => {
console.log('Navigate to:', href);
};
return (
<ListProductsGrid
listProductsGridTitle="Our Products"
subtitle="Premium selection of electronics"
sx={{
maxWidth: 1200,
mx: 'auto',
p: 3,
}}
>
<ListProductsGrid.Title />
<ListProductsGrid.Items>
{productData.map(product => (
<ListProductsGrid.Items.GridCard
key={product.id}
title={product.title}
imageUrl={product.imageUrl}
summary={product.description}
chips={product.badges?.map(b => ({label: b}))}
linkProps={{href: `/products/${product.id}`, onNavigate: handleNavigate}}
button={{children: product.price}}
/>
))}
</ListProductsGrid.Items>
</ListProductsGrid>
);
}
π Notesβ
- The main component extends MUI
StackPropswith defaultspacing={5}and paddingpx: 2, py: 5 ListProductsGrid.Titledisplays the subtitle above the main title withTypographycomponentsListProductsGrid.Items.GridCardrequires atitleprop; all other props are optional- When
linkPropsis provided, the entire card becomes clickable viaCardActionArea - The
onNavigateprop can be set to'standard-html-link'for native browser navigation status.actionrenders as a button whenlinkPropsis present (to avoid nested<a>tags)chipssupport MUIChipPropsplus optionalleftIconandrightIconelementstagswith array labels are joined with " / " separator (e.g.,['A', 'B']β "A / B")- Card images have a fixed height of
120pxwithobject-fit: cover - Disabled cards have reduced opacity (0.6) and pointer events disabled
Built with β€οΈ using React, TypeScript, and MUI.