View Product Block
ViewProduct is a product-detail view block built on MUI with a header (organization, image, chips, title), tags, primary and optional secondary description sections, optional footer, and action buttons.
Installationโ
- npm
- yarn
- pnpm
- bun
npm install @nodeblocks/frontend-view-product-block
yarn add @nodeblocks/frontend-view-product-block
pnpm add @nodeblocks/frontend-view-product-block
bun add @nodeblocks/frontend-view-product-block
What You Needโ
| Item | Why it matters |
|---|---|
imageUrl | Source URL for the product image |
headerTitle | Title for the product view |
chips | Status or category chips under the image (label required per chip) |
tags | Tag chips in the content area (label can be text or breadcrumb-style string[], optional icon as a ReactElement) |
sections | Primary description blocks (label, value, optional icon) |
actions (optional) | Footer buttons; wire onClick to cart, checkout, or navigation |
ViewProduct is presentational: pass product fields as props and implement actions[].onClick in the parent. Optional secondarySections, footerLogo, and footerText extend the default layout when provided.
Code Examplesโ
- Quick Start
- Secondary Sections
- Compound Components
- Block Override
function Example() { const [lastAction, setLastAction] = React.useState(''); const sellerName = 'TechStore'; const chips = [ { label: 'Smartphone' }, { label: '5G' }, { label: 'Unlocked' }, { label: 'Ships today', variant: 'filled' }, ]; const tags = [ { label: 'Premium' }, { label: ['Electronics', 'Phones', 'Smartphones', 'Apple'] }, { label: 'In stock' }, ]; const sections = [ { label: 'Product Description', value: 'The iPhone 15 Pro features a titanium design, A17 Pro chip, and an advanced Pro camera system with a 48MP main sensor.', }, { label: 'Shipping & Warranty', value: 'Free standard shipping in 5โ7 business days. One-year limited warranty on manufacturing defects.', }, ]; const actions = [ { label: 'Add to Cart', onClick: () => setLastAction('Add to Cart') }, { label: 'Buy Now', onClick: () => setLastAction('Buy Now') }, ]; return ( <> {lastAction ? ( <Typography variant="body2" sx={{ mb: 2, textAlign: 'center' }}> Last action: <strong>{lastAction}</strong> </Typography> ) : null} <ViewProduct organizationName={sellerName} headerLogo="https://docs.nodeblocks.dev/img/icon.png" imageUrl="https://docs.nodeblocks.dev/img/undraw_docusaurus_react.svg" imageAlt="iPhone 15 Pro" chips={chips} headerTitle="iPhone 15 Pro - Latest Model" tags={tags} sections={sections} footerLogo="https://docs.nodeblocks.dev/img/icon.png" footerText={`${sellerName} ยท 127 products from this seller`} actions={actions} /> </> ); }
Pass secondarySections to render extra description blocks in a muted card style below the primary sections.
function Example() { const [lastAction, setLastAction] = React.useState(''); const sellerName = 'TechStore'; const chips = [ { label: 'Smartphone' }, { label: '5G' }, { label: 'Unlocked' }, ]; const tags = [ { label: 'Premium' }, { label: ['Electronics', 'Phones', 'Smartphones'] }, { label: 'In stock' }, ]; const sections = [ { label: 'Product Description', value: 'The iPhone 15 Pro features a titanium design, A17 Pro chip, and an advanced Pro camera system with a 48MP main sensor.', }, { label: 'Shipping & Warranty', value: 'Free standard shipping in 5โ7 business days. One-year limited warranty on manufacturing defects.', }, ]; const secondarySections = [ { label: 'Payment options', value: 'Pay securely with major credit cards, Apple Pay, or financing plans subject to approval. All transactions are encrypted.', }, { label: 'Returns & exchanges', value: 'Eligible items may be returned within 30 days in original condition. Open-box items may incur a restocking fee. Contact support to start a return.', }, ]; const actions = [ { label: 'Add to Cart', onClick: () => setLastAction('Add to Cart') }, { label: 'Buy Now', onClick: () => setLastAction('Buy Now') }, ]; return ( <> {lastAction ? ( <Typography variant="body2" sx={{ mb: 2, textAlign: 'center' }}> Last action: <strong>{lastAction}</strong> </Typography> ) : null} <ViewProduct organizationName={sellerName} headerLogo="https://docs.nodeblocks.dev/img/icon.png" imageUrl="https://docs.nodeblocks.dev/img/undraw_docusaurus_react.svg" imageAlt="iPhone 15 Pro" chips={chips} headerTitle="iPhone 15 Pro - Latest Model" tags={tags} sections={sections} secondarySections={secondarySections} footerLogo="https://docs.nodeblocks.dev/img/icon.png" footerText={`${sellerName} ยท 127 products from this seller`} actions={actions} /> </> ); }
Each secondarySections entry may include an icon as an MUI SvgIconComponent (for example SupportAgentOutlined), same as primary sections.
Use child blocks to customize layout and styling. Pass section-specific props on sub-components instead of only on the root.
function Example() { const [lastAction, setLastAction] = React.useState(''); const chips = [{ label: 'New' }, { label: 'Featured', color: 'success' }]; const tags = [{ label: 'Premium' }, { label: 'Free shipping' }]; const sections = [ { label: 'Overview', value: 'Lightweight titanium design with all-day battery life.' }, ]; const actions = [ { label: 'Add to Cart', onClick: () => setLastAction('Add to Cart') }, { label: 'Buy Now', onClick: () => setLastAction('Buy Now') }, ]; return ( <> {lastAction ? ( <Typography variant="body2" sx={{ mb: 2, textAlign: 'center' }}> Last action: <strong>{lastAction}</strong> </Typography> ) : null} <ViewProduct imageUrl="https://docs.nodeblocks.dev/img/undraw_docusaurus_react.svg" chips={chips} headerTitle="iPhone 15 Pro" tags={tags} sections={sections} > <ViewProduct.Header> <ViewProduct.CompanyName organizationName="TechStore" headerLogo="https://docs.nodeblocks.dev/img/icon.png" sx={{ bgcolor: 'grey.50', borderRadius: 2, px: 2 }} /> <ViewProduct.Image imageAlt="Product hero" sx={{ mb: 2 }} /> <ViewProduct.Chips chips={chips} sx={{ px: 2 }} /> <ViewProduct.Title headerTitle="iPhone 15 Pro" sx={{ color: 'primary.main', px: 2 }} /> </ViewProduct.Header> <ViewProduct.Content> <ViewProduct.Tags tags={tags} sx={{ columnGap: 2 }} /> <ViewProduct.Sections sections={sections} /> </ViewProduct.Content> <ViewProduct.Actions actions={actions} sx={{ py: 2, bgcolor: 'grey.50', borderRadius: 2 }} /> </ViewProduct> </> ); }
Use function children with defaultBlocks and defaultBlockOrder to inject blocks, replace defaults, and control order.
function Example() { const [promoVisible, setPromoVisible] = React.useState(true); const [lastAction, setLastAction] = React.useState(''); const sellerName = 'TechStore'; const chips = [ { label: '20% off', variant: 'filled', color: 'error', size: 'small' }, { label: 'Express shipping', variant: 'filled', color: 'primary' }, { label: 'Certified reseller', variant: 'outlined' }, ]; const tags = [ { label: 'Premium' }, { label: ['Electronics', 'Phones', 'iPhone 15 Pro'] }, { label: 'In stock ยท ships today' }, ]; const sections = [ { label: 'Why customers choose this', value: 'Titanium design, all-day battery, and Pro camera system โ tuned for creators who need reliable 4K capture on the go.', }, { label: 'Delivery', value: 'Order in the next 2h 14m for same-day dispatch. Free returns within 30 days.', }, ]; const actions = [ { label: 'Add to cart', onClick: () => setLastAction('Add to cart') }, { label: 'Buy now', onClick: () => setLastAction('Buy now') }, ]; const trustSignals = [ { title: 'Free returns', detail: '30-day window' }, { title: 'Secure checkout', detail: '256-bit encryption' }, { title: '24/7 support', detail: 'Live chat & phone' }, ]; return ( <> {lastAction ? ( <Typography variant="body2" sx={{ mb: 2, textAlign: 'center' }}> Last action: <strong>{lastAction}</strong> </Typography> ) : null} <ViewProduct organizationName={sellerName} headerLogo="https://docs.nodeblocks.dev/img/icon.png" imageUrl="https://docs.nodeblocks.dev/img/undraw_docusaurus_react.svg" imageAlt="iPhone 15 Pro" chips={chips} headerTitle="iPhone 15 Pro โ Weekend drop" tags={tags} sections={sections} footerLogo="https://docs.nodeblocks.dev/img/icon.png" footerText={`${sellerName} ยท 127 products from this seller`} actions={actions} > {({ defaultBlocks }) => ({ blocks: { ...defaultBlocks, promoRibbon: promoVisible ? ( <Box sx={{ mx: { xs: 2, sm: 0 }, mb: 2, px: 2, py: 1.5, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 2, borderRadius: 2, color: 'common.white', background: (theme) => `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 55%, #0f172a 100%)`, boxShadow: (theme) => `0 12px 32px ${theme.palette.primary.main}33`, }} > <Box sx={{ minWidth: 0 }}> <Typography variant="overline" sx={{ display: 'block', lineHeight: 1.2, opacity: 0.9, letterSpacing: 0.8 }} > Limited offer </Typography> <Typography variant="subtitle2" sx={{ fontWeight: 700 }}> Free express shipping + 20% off at checkout </Typography> </Box> <Box component="button" type="button" onClick={() => setPromoVisible(false)} aria-label="Dismiss promotion" sx={{ flexShrink: 0, border: '1px solid rgba(255,255,255,0.35)', bgcolor: 'rgba(255,255,255,0.12)', color: 'inherit', borderRadius: '50%', width: 32, height: 32, cursor: 'pointer', fontSize: 18, lineHeight: 1, }} > ร </Box> </Box> ) : null, content: ( <ViewProduct.Content> <Box sx={{ mx: { xs: 2, sm: 0 }, mb: 3, p: 2, borderRadius: 2, bgcolor: 'grey.50', border: '1px solid', borderColor: 'divider', }} > <ViewProduct.Tags tags={tags} sx={{ columnGap: 1, rowGap: 1, '& .MuiChip-root': { height: 'auto', py: 0.75, bgcolor: 'background.paper', border: '1px solid', borderColor: 'grey.200', borderRadius: 999, transition: 'border-color 0.2s, box-shadow 0.2s', '&:hover': { borderColor: 'primary.light', boxShadow: (theme) => `0 4px 12px ${theme.palette.primary.main}22`, }, }, '& .MuiChip-label': { px: 1.25, fontWeight: 600, color: 'text.primary', }, '& .MuiChip-root:nth-of-type(1) .MuiChip-label': { color: 'primary.main', }, '& .MuiChip-root:nth-of-type(3) .MuiChip-label': { color: 'success.main', }, }} /> </Box> <ViewProduct.Sections sections={sections} /> </ViewProduct.Content> ), trustBar: ( <Box sx={{ mx: { xs: 2, sm: 0 }, mb: { xs: 2, sm: 3 }, display: 'grid', gridTemplateColumns: { xs: '1fr', sm: 'repeat(3, 1fr)' }, gap: 1.5, }} > {trustSignals.map((item) => ( <Box key={item.title} sx={{ p: 1.75, borderRadius: 2, border: '1px solid', borderColor: 'divider', bgcolor: 'background.paper', boxShadow: '0 1px 2px rgba(15, 23, 42, 0.06)', }} > <Typography variant="subtitle2" sx={{ fontWeight: 700 }}> {item.title} </Typography> <Typography variant="caption" color="text.secondary"> {item.detail} </Typography> </Box> ))} </Box> ), actions: ( <ViewProduct.Actions actions={actions} sx={{ py: 0, px: 0, '& .MuiButton-root:first-of-type': { bgcolor: 'grey.100', color: 'text.primary', boxShadow: 'none', '&:hover': { bgcolor: 'grey.200', boxShadow: 'none' }, }, '& .MuiButton-root:last-of-type': { boxShadow: (theme) => `0 8px 20px ${theme.palette.primary.main}40`, }, }} /> ), }, blockOrder: [ ...(promoVisible ? ['promoRibbon'] : []), 'header', 'content', 'trustBar', 'footer', 'actions', ], })} </ViewProduct> </> ); }
When to use block overrides
Default blockOrder is header, content, footer, actions. This example shows four common patterns together: a dismissible promo block (promoRibbon), replacing content to restyle ViewProduct.Tags (pill chips, highlight panel, accent label colors), a custom block between content and footer (trustBar), and replacing actions with a sticky glass-style CTA bar. Drop promoRibbon from blockOrder when dismissed so layout does not reserve empty space.
Important Propsโ
Core Propsโ
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
imageUrl | string | Yes | โ | Product image URL; ViewProduct.Image renders only when set |
headerTitle | string | Yes | โ | Product title in the header |
chips | (ChipProps & { label: string })[] | Yes | โ | Header chips; each item must include label (plus any MUI Chip props such as variant, color, size) |
tags | { label: string | string[]; icon?: ReactElement }[] | Yes | โ | Tag row in content; label as string[] renders breadcrumb-style segments separated by / |
sections | { icon?: SvgIconComponent; label: string; value: string }[] | Yes | โ | Primary content sections with optional MUI icon component |
Content Propsโ
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
organizationName | string | No | undefined | Seller or organization name in the header |
headerLogo | string | No | undefined | Image URL for the circular logo beside the organization name |
imageAlt | string | No | undefined | alt text for the product image |
secondarySections | { icon?: SvgIconComponent; label: string; value: string }[] | No | undefined | Additional sections in a muted card style below primary sections |
footerLogo | string | No | undefined | Footer image URL (or data URI) |
footerText | string | No | undefined | Footer caption (for example seller product count) |
actions | { label: ReactNode; onClick: () => void }[] | No | undefined | Equal-width action buttons in the bottom action row |
There is no root labels prop. Override copy via headerTitle, chip labels, section label/value, footerText, and actions[].label.
Layout and Composition Propsโ
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | BlocksOverride | ReactNode | No | undefined | Compound JSX children or function override returning blocks and blockOrder |
className | string | No | undefined | Class name on the root stack (nbb-view-product) |
sx | SxProps | No | undefined | MUI system styles for the root stack |
ViewProduct inherits StackProps (except children). Default block order without overrides: header, content, footer, actions (defaultBlockOrder).
Sub-component propsโ
Sub-components read from context and accept the same content keys as props to override locally.
| Sub-component | Main Props | Inherits | Built on |
|---|---|---|---|
ViewProduct.CompanyName | organizationName, headerLogo, children, className, sx | StackProps | Stack + Box (img) + Typography |
ViewProduct.Image | imageUrl, imageAlt, children, className | BoxProps | Box (img, aspect ratio 1368/540) |
ViewProduct.Chips | chips, children, className, sx | StackProps | Stack + Chip (variant="outlined", color="primary") |
ViewProduct.Title | headerTitle, children, className, sx | TypographyProps | Typography (component="h4") |
ViewProduct.Header | organizationName, headerLogo, imageUrl, imageAlt, chips, headerTitle, children, className, sx | StackProps | Stack + Divider |
ViewProduct.Tags | tags, children, className, sx | StackProps | Stack + Chip (transparent background) |
ViewProduct.Sections | sections, children, className | StackProps | Stack + Typography |
ViewProduct.SecondarySections | secondarySections, children, className | StackProps | Stack + card-styled section blocks |
ViewProduct.Content | tags, sections, secondarySections, children, className, sx | StackProps | Stack composing Tags, Sections, SecondarySections |
ViewProduct.Footer | footerLogo, footerText, children, className, sx | BoxProps | Box + Stack |
ViewProduct.Actions | actions, children, className, sx | StackProps | Stack + Button (variant="contained", size="medium") |
Default UI Blocksโ
| Block | Built on | Notes |
|---|---|---|
ViewProduct (root) | Stack | Centered column layout (nbb-view-product) |
header (ViewProduct.Header) | Stack | Company name, image, chips, title, divider |
content (ViewProduct.Content) | Stack | Tags, sections, optional secondary sections |
footer (ViewProduct.Footer) | Box | Renders only when footerLogo, footerText, or custom children is set |
actions (ViewProduct.Actions) | Stack + Button | Equal-width buttons when actions is provided |
Default root render order: header โ content โ footer โ actions.
Company logo alt defaults to company logo. Footer logo alt defaults to footer logo.
TypeScriptโ
import * as React from 'react';
import { ViewProduct } from '@nodeblocks/frontend-view-product-block';
import type { ChipProps } from '@mui/material';
import type { SvgIconComponent } from '@mui/icons-material';
import type { ReactElement, ReactNode } from 'react';
type ProductChip = ChipProps & { label: string };
type ProductTag = {
label: string | string[];
icon?: ReactElement;
};
type ProductSection = {
icon?: SvgIconComponent;
label: string;
value: string;
};
type ProductAction = {
label: ReactNode;
onClick: () => void;
};
export function ProductDetailView() {
const chips: ProductChip[] = [{ label: 'New' }, { label: 'In stock', variant: 'filled' }];
const tags: ProductTag[] = [{ label: 'Premium' }];
const sections: ProductSection[] = [
{ label: 'Description', value: 'Product details go here.' },
];
const actions: ProductAction[] = [
{ label: 'Add to Cart', onClick: () => undefined },
];
return (
<ViewProduct
organizationName="TechStore"
imageUrl="https://example.com/product.jpg"
imageAlt="Product"
chips={chips}
headerTitle="Example product"
tags={tags}
sections={sections}
actions={actions}
/>
);
}