# Product list 01

> Three-column grid layout with product cards featuring centered images, product names with badges, separator lines, pricing with sale discounts, wishlist checkboxes and shopping cart icons

A shadcn/ui block from the [@shadcn-studio](https://shadcnstudio.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcn-studio/product-list-01).

- Page, with a live preview: https://shadcn.exchange/r/shadcn-studio/product-list-01
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcn-studio/product-list-01-light-b3092766887f3234.png
- Registry item: https://shadcnstudio.com/r/new-york-v4/product-list-01.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcnstudio.com/r/new-york-v4/product-list-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `radix-ui`
- Registry items: `badge`, `button`, `card`, `separator`, `utils`

## Files

### `src/registry/new-york/blocks/ecommerce/product-list/product-list-01/page.tsx`

```tsx
import ProductList from '@/registry/new-york/components/blocks/product-list-01/product-list-01'

const productList = [
  {
    image: 'https://cdn.shadcnstudio.com/ss-assets/blocks/ecommerce/product-list/image-6.png',
    imgAlt: 'Samsung Galaxy Watch 6',
    name: 'Samsung Galaxy Watch 6 Classic',
    price: 129,
    badges: ['Watch', 'Samsung']
  },
  {
    image: 'https://cdn.shadcnstudio.com/ss-assets/blocks/ecommerce/product-list/image-5.png',
    imgAlt: 'Samsung Galaxy Watch 7',
    name: 'Samsung Galaxy Watch 7',
    price: 229,
    salePrice: 139,
    badges: ['Watch', 'Samsung']
  },
  {
    image: 'https://cdn.shadcnstudio.com/ss-assets/blocks/ecommerce/product-list/image-4.png',
    imgAlt: 'Samsung Galaxy Watch Ultra',
    name: 'Samsung Galaxy Watch Ultra',
    price: 119,
    badges: ['Watch', 'Samsung']
  },
  {
    image: 'https://cdn.shadcnstudio.com/ss-assets/blocks/ecommerce/product-list/image-3.png',
    imgAlt: 'Samsung Galaxy Watch 7',
    name: 'Samsung Galaxy Watch 7',
    price: 129,
    badges: ['Watch', 'Samsung']
  },
  {
    image: 'https://cdn.shadcnstudio.com/ss-assets/blocks/ecommerce/product-list/image-2.png',
    imgAlt: 'Spigen Rugged Armor Pro',
    name: 'Spigen Rugged Armor Pro',
    price: 239,
    badges: ['Watch', 'Spigen']
  },
  {
    image: 'https://cdn.shadcnstudio.com/ss-assets/blocks/ecommerce/product-list/image-1.png',
    imgAlt: 'Mosmoc Rugged No Gap',
    name: 'Mosmoc Rugged No Gap',
    price: 149,
    badges: ['Watch', 'Samsung']
  }
]

const ProductListPage = () => {
  return <ProductList products={productList} />
}

export default ProductListPage
```

### `src/registry/new-york/components/blocks/product-list-01/product-list-01.tsx`

```tsx
import { HeartIcon, ShoppingCartIcon } from 'lucide-react'

import { Checkbox as CheckboxPrimitive } from 'radix-ui'

import { Badge } from '@/registry/new-york/ui/badge'
import { Button } from '@/registry/new-york/ui/button'
import { Card, CardContent } from '@/registry/new-york/ui/card'
import { Separator } from '@/registry/new-york/ui/separator'

import { cn } from '@/registry/new-york/lib/utils'

export type ProductItem = {
  image: string
  imgAlt: string
  name: string
  price: number
  salePrice?: number
  badges: string[]
}

type ProductProps = {
  products: ProductItem[]
}

const ProductList = ({ products }: ProductProps) => {
  return (
    <section className='bg-muted py-8 sm:py-16 lg:py-24'>
      <div className='mx-auto max-w-7xl space-y-12 px-4 sm:space-y-16 sm:px-6 lg:space-y-24 lg:px-8'>
        <div className='space-y-4'>
          <p className='text-sm font-medium'>Samsung watch</p>
          <h2 className='text-2xl font-semibold sm:text-3xl lg:text-4xl'>All New Collection</h2>
        </div>

        <div className='grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3'>
          {/* Product Cards */}
          {products.map((product, index) => (
            <Card key={index} className={cn('border-none shadow-none', product.salePrice && 'relative')}>
              {/* Sale Badge */}
              {product.salePrice && (
                <Badge className='bg-destructive/10 [a&]:hover:bg-destructive/5 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive absolute top-6 left-6 rounded-sm px-3 py-1 uppercase focus-visible:outline-none'>
                  Sale
                </Badge>
              )}

              <CardContent className='flex flex-1 flex-col justify-between gap-6'>
                {/* Product Image */}
                <a href='#'>
                  <img src={product.image} alt={product.imgAlt} className='mx-auto size-50' />
                </a>

                {/* Product Details */}
                <div className='space-y-4'>
                  <div className='flex flex-col gap-2 text-center'>
                    <a href='#'>
                      <h3 className='text-xl font-semibold'>{product.name}</h3>
                    </a>
                    <div className='flex items-center justify-center gap-2'>
                      {product.badges.map((badge, idx) => (
                        <Badge
                          key={idx}
                          className='rounded-sm bg-green-600/10 text-green-600 focus-visible:ring-green-600/20 focus-visible:outline-none dark:bg-green-400/10 dark:text-green-400 dark:focus-visible:ring-green-400/40'
                        >
                          {badge}
                        </Badge>
                      ))}
                    </div>
                  </div>

                  <Separator />

                  {/* Product Price */}
                  <div className='flex items-center justify-between'>
                    {!product.salePrice && <span className='text-2xl font-semibold'>${product.price.toFixed(2)}</span>}
                    {product.salePrice && (
                      <div className='flex items-center gap-2.5'>
                        <span className='text-2xl font-semibold'>${product.salePrice.toFixed(2)}</span>
                        <span className='text-muted-foreground font-medium line-through'>
                          ${product.price.toFixed(2)}
                        </span>
                      </div>
                    )}

                    <div>
                      <CheckboxPrimitive.Root
                        data-slot='checkbox'
                        className='group focus-visible:ring-ring/50 rounded-sm p-2.5 outline-none focus-visible:ring-3'
                        aria-label='Heart icon'
                      >
                        <span className='group-data-[state=checked]:hidden'>
                          <HeartIcon className='size-4' />
                        </span>
                        <span className='group-data-[state=unchecked]:hidden'>
                          <HeartIcon className='fill-destructive stroke-destructive size-4' />
                        </span>
                      </CheckboxPrimitive.Root>

                      <Button variant='ghost' size='icon'>
                        <ShoppingCartIcon />
                      </Button>
                    </div>
                  </div>
                </div>
              </CardContent>
            </Card>
          ))}
        </div>
      </div>
    </section>
  )
}

export default ProductList
```
