# Product Listing 01 - Multi Column Grid

> A multi-column layout featuring product cards with images, reviews, and pricing details, helping users scan, compare, and select products quickly.

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

- Page, with a live preview: https://shadcn.exchange/r/shadcn-space/product-listing-01
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcn-space/product-listing-01-light-c6fdddbd8041e6d4.png
- Registry item: https://shadcnspace.com/r/product-listing-01.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcnspace.com/r/product-listing-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`
- Registry items: `button`, `badge`, `card`

## Files

### `src/components/shadcn-space/blocks/product-listing-01/page.tsx`

```tsx
import ProductListing from "@/components/shadcn-space/blocks/product-listing-01"

export default function ProductListingPage() {
  return (
      <ProductListing />
  )
}
```

### `src/components/shadcn-space/blocks/product-listing-01/index.tsx`

```tsx
"use client";

import { ArrowRight } from "lucide-react";
import {
  ProductCard,
  type ProductCardProps,
} from "@/components/shadcn-space/blocks/product-listing-01/product-card";

export const PRODUCTS: ProductCardProps[] = [
  {
    image:
      "https://images.shadcnspace.com/assets/ecommerce/product-category/product-category-03-1.webp",
    category: "Electronics",
    name: "Apple Watch S9",
    rating: 4.5,
    reviews: 105,
    price: 684,
    originalPrice: 855,
  },
  {
    image:
      "https://images.shadcnspace.com/assets/ecommerce/product-category/product-category-02-2.webp",
    category: "Fashion",
    name: "Beige Jacket",
    rating: 4.3,
    reviews: 105,
    price: 479,
    originalPrice: 599,
    badge: { text: "-15%" },
  },
  {
    image:
      "https://images.shadcnspace.com/assets/ecommerce/product-category/product-category-03-3.webp",
    category: "Beauty",
    name: "Glow Serum",
    rating: 4.5,
    reviews: 105,
    price: 46,
    originalPrice: 55,
    badge: { text: "New" },
  },
  {
    image:
      "https://images.shadcnspace.com/assets/ecommerce/product-category/product-category-03-4.webp",
    category: "Electronics",
    name: "Space WH-1000XM5",
    rating: 4.5,
    reviews: 105,
    price: 279,
    originalPrice: 349,
    badge: { text: "Hot" },
  },
];

export interface ProductListingProps {
  products?: ProductCardProps[];
}

export default function ProductListing({
  products = PRODUCTS,
}: ProductListingProps) {
  return (
    <section className="py-12 md:py-16 lg:py-20">
      <div className="max-w-7xl mx-auto px-4 md:px-8 lg:px-16 flex flex-col gap-8">
        <div className="flex items-center justify-between">
          <div className="flex flex-col gap-1">
            <h2 className="text-2xl font-semibold text-foreground">
              Featured products
            </h2>
            <p className="text-base text-muted-foreground">
              Handpicked by our team
            </p>
          </div>
          <a
            href="#"
            className="items-center gap-2 text-sm font-medium flex group cursor-pointer"
          >
            See all
            <ArrowRight className="size-4 group-hover:translate-x-1 transition-all" />
          </a>
        </div>

        <div className="w-full overflow-x-auto xl:[scrollbar-width:none] xl:[-ms-overflow-style:none] xl:[&::-webkit-scrollbar]:hidden">
          <div className="flex gap-6">
            {products.map((product, index) => (
              <div key={index} className="inline-block min-w-67.5 max-w-67.5 w-full whitespace-normal shrink-0">
                <ProductCard {...product} className="w-full" />
              </div>
            ))}
          </div>
        </div>
      </div>
    </section>
  );
}
```

### `src/components/shadcn-space/blocks/product-listing-01/product-card.tsx`

```tsx
"use client";

import { useState } from "react";
import { Heart, Star } from "lucide-react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";

export interface ProductCardProps {
  image: string;
  category: string;
  name: string;
  rating: number;
  reviews: number;
  price: number;
  originalPrice?: number;
  badge?: {
    text: string;
  };
  className?: string;
  onAddToCart?: () => void;
  onWishlist?: () => void;
}

export function ProductCard({
  image,
  category,
  name,
  rating,
  reviews,
  price,
  originalPrice,
  badge,
  className,
  onAddToCart,
  onWishlist,
}: ProductCardProps) {
  const [isWishlisted, setIsWishlisted] = useState(false);

  const getBadgeStyles = (text: string) => {
    const lowercaseText = text.toLowerCase();
    if (lowercaseText.includes("%") || lowercaseText === "sale") {
      return "bg-red-500/10 text-red-500";
    }
    if (lowercaseText === "new") {
      return "bg-blue-500/10 text-blue-500";
    }
    if (lowercaseText === "hot") {
      return "bg-orange-400/10 text-orange-400";
    }
    return "bg-primary/10 text-primary";
  };

  return (
    <Card className={cn("group flex flex-col gap-0 rounded-2xl bg-card p-0 transition-all overflow-hidden ring-0 border", className,)}>
      <div className="relative overflow-hidden bg-muted/50 min-h-55 h-full">
        {badge && (
          <Badge
            variant="outline"
            className={cn(
              "absolute left-4 top-5.5 z-10 rounded-full border-0 px-2 py-0.5 text-xs capitalize",
              getBadgeStyles(badge.text),
            )}
          >
            {badge.text}
          </Badge>
        )}
        <Button
          size="icon-sm"
          className="group/wishlist absolute right-4 top-4 z-10 size-8 rounded-full bg-background transition-transform hover:scale-110 cursor-pointer"
          onClick={() => {
            setIsWishlisted(!isWishlisted);
            onWishlist?.();
          }}
        >
          <Heart
            className={cn(
              "size-4 transition-all duration-300",
              isWishlisted
                ? "fill-red-500 text-red-500 scale-110"
                : "text-foreground",
            )}
          />
          <span className="sr-only">Add to wishlist</span>
        </Button>
        <img
          src={image}
          alt={name}
          className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
        />
      </div>
      <CardContent className="flex flex-col gap-3 p-5">
        <div className="flex flex-col gap-2">
          <span className="text-sm text-muted-foreground capitalize">
            {category}
          </span>
          <p className="line-clamp-1 text-lg font-medium text-foreground">
            {name}
          </p>
          <div className="flex items-center gap-2">
            <div className="flex items-center">
              {[...Array(5)].map((_, i) => (
                <Star
                  key={i}
                  className={cn(
                    "h-3.5 w-3.5",
                    i < Math.floor(rating)
                      ? "fill-orange-400 text-orange-400"
                      : "fill-muted text-orange-400",
                  )}
                />
              ))}
            </div>
            <small className="text-sm text-foreground">{rating}</small>
            <small className="text-sm text-foreground">({reviews})</small>
          </div>
          <div className="flex items-center gap-2">
            <small className="text-lg font-medium text-foreground">
              ${price}
            </small>
            {originalPrice && (
              <small className="text-lg font-medium text-muted-foreground line-through">
                ${originalPrice}
              </small>
            )}
          </div>
        </div>
        <Button
          className="w-full gap-2 h-10 cursor-pointer hover:bg-primary/80"
          onClick={onAddToCart}
        >
          Add to Cart
        </Button>
      </CardContent>
    </Card>
  );
}
```
