Need Custom Development?

Get professional help with your Next.js project from our expert team.

Contact Us
Back to Community Uploads

Ecommerce Product Components

A collection of reusable ecommerce product components built with Next.js and Tailwind CSS

Ecommerce Product Components Preview

Project Information

Author

Michael Chen

Technology

Next.js, Tailwind CSS, React

Last Updated

June 10, 2023

Category

UI Components

Components Overview

This collection includes essential components for building modern ecommerce interfaces. Each component is designed to be responsive, accessible, and customizable to fit your brand.

Product Card with hover effects
Product Gallery with thumbnails
Add to Cart button with animation
Product Reviews component
Product Variants selector
Wishlist toggle button
Product Quantity selector
Related Products carousel

Product Card Component

import { Heart } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';

interface ProductCardProps {
  id: string;
  name: string;
  price: number;
  image: string;
  category: string;
  rating: number;
  isNew?: boolean;
  isSale?: boolean;
  discount?: number;
}

export default function ProductCard({
  id,
  name,
  price,
  image,
  category,
  rating,
  isNew = false,
  isSale = false,
  discount = 0,
}: ProductCardProps) {
  return (
    <div className="group relative bg-white dark:bg-gray-900 rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300">
      {/* Product badges */}
      <div className="absolute top-2 left-2 z-10 flex flex-col gap-1">
        {isNew && (
          <span className="px-2 py-1 text-xs font-medium bg-blue-500 text-white rounded">
            New
          </span>
        )}
        {isSale && (
          <span className="px-2 py-1 text-xs font-medium bg-red-500 text-white rounded">
            Sale
          </span>
        )}
      </div>
      
      {/* Wishlist button */}
      <button 
        className="absolute top-2 right-2 z-10 p-1.5 rounded-full bg-white/80 dark:bg-gray-800/80 hover:bg-white dark:hover:bg-gray-800 transition-colors"
        aria-label="Add to wishlist"
      >
        <Heart className="h-4 w-4 text-gray-600 dark:text-gray-400" />
      </button>
      
      {/* Product image */}
      <Link href={`/products/${id}`} className="block relative pt-[100%]">
        <Image
          src={image || "/placeholder.svg"}
          alt={name}
          fill
          className="object-cover object-center group-hover:scale-105 transition-transform duration-300"
        />
      </Link>
      
      {/* Product info */}
      <div className="p-4">
        <span className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
          {category}
        </span>
        <Link href={`/products/${id}`}>
          <h3 className="mt-1 text-sm font-medium text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
            {name}
          </h3>
        </Link>
        
        {/* Price */}
        <div className="mt-2 flex items-center">
          {isSale && discount > 0 ? (
            <>
              <span className="text-sm font-medium text-gray-900 dark:text-white">
                {'$'}{(price * (1 - discount / 100)).toFixed(2)}
              </span>
              <span className="ml-2 text-xs text-gray-500 dark:text-gray-400 line-through">
                {'$'}{price.toFixed(2)}
              </span>
            </>
          ) : (
            <span className="text-sm font-medium text-gray-900 dark:text-white">
              {'$'}{price.toFixed(2)}
            </span>
          )}
        </div>
        
        {/* Rating */}
        <div className="mt-2 flex items-center">
          <div className="flex">
            {[...Array(5)].map((_, i) => (
              <svg
                key={i}
                className={`h-3.5 w-3.5 ${
                  i < rating ? 'text-yellow-400' : 'text-gray-300 dark:text-gray-600'
                }`}
                fill="currentColor"
                viewBox="0 0 20 20"
              >
                <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
              </svg>
            ))}
          </div>
          <span className="ml-1 text-xs text-gray-500 dark:text-gray-400">
            ({rating.toFixed(1)})
          </span>
        </div>
        
        {/* Add to cart button */}
        <button className="mt-4 w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors">
          Add to Cart
        </button>
      </div>
    </div>
  );
}

How to Use

The ProductCard component is designed to display product information in a visually appealing card format. It includes support for badges (new, sale), wishlist functionality, price display with discount calculation, and star ratings.

// Example usage
<ProductCard
  id="prod-123"
  name="Premium Wireless Headphones"
  price={129.99}
  image="/images/headphones.jpg"
  category="Audio"
  rating={4.5}
  isNew={true}
  isSale={true}
  discount={15}
/>

Product Gallery Component

import { useState } from 'react';
import Image from 'next/image';

interface ProductGalleryProps {
  images: {
    id: string;
    src: string;
    alt: string;
  }[];
}

export default function ProductGallery({ images }: ProductGalleryProps) {
  const [activeImage, setActiveImage] = useState(images[0]);

  return (
    <div className="w-full">
      {/* Main image */}
      <div className="relative aspect-square w-full overflow-hidden rounded-lg mb-4">
        <Image
          src={activeImage.src || "/placeholder.svg"}
          alt={activeImage.alt}
          fill
          className="object-cover object-center"
        />
      </div>
      
      {/* Thumbnails */}
      <div className="flex space-x-2 overflow-x-auto pb-2">
        {images.map((image) => (
          <button
            key={image.id}
            onClick={() => setActiveImage(image)}
            className={`relative w-20 h-20 rounded-md overflow-hidden ${
              activeImage.id === image.id
                ? 'ring-2 ring-blue-500'
                : 'ring-1 ring-gray-300 dark:ring-gray-700'
            }`}
          >
            <Image
              src={image.src || "/placeholder.svg"}
              alt={`Thumbnail ${image.alt}`}
              fill
              className="object-cover object-center"
            />
          </button>
        ))}
      </div>
    </div>
  );
}

How to Use

The ProductGallery component displays a main product image with clickable thumbnails below. Users can click on thumbnails to change the main image view.

// Example usage
const productImages = [
  {
    id: "img1",
    src: "/images/product-1.jpg",
    alt: "Product front view"
  },
  {
    id: "img2",
    src: "/images/product-2.jpg",
    alt: "Product side view"
  },
  {
    id: "img3",
    src: "/images/product-3.jpg",
    alt: "Product back view"
  }
];

<ProductGallery images={productImages} />

Product Variants Component

import { useState } from 'react';

interface ColorOption {
  id: string;
  name: string;
  value: string;
}

interface SizeOption {
  id: string;
  name: string;
  inStock: boolean;
}

interface ProductVariantsProps {
  colors: ColorOption[];
  sizes: SizeOption[];
  onVariantChange: (color: string, size: string) => void;
}

export default function ProductVariants({
  colors,
  sizes,
  onVariantChange,
}: ProductVariantsProps) {
  const [selectedColor, setSelectedColor] = useState<string>(colors[0]?.id || '');
  const [selectedSize, setSelectedSize] = useState<string>('');

  const handleColorChange = (colorId: string) => {
    setSelectedColor(colorId);
    onVariantChange(colorId, selectedSize);
  };

  const handleSizeChange = (sizeId: string) => {
    setSelectedSize(sizeId);
    onVariantChange(selectedColor, sizeId);
  };

  return (
    <div className="space-y-6">
      {/* Color options */}
      <div>
        <h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3">Color</h3>
        <div className="flex flex-wrap gap-2">
          {colors.map((color) => (
            <button
              key={color.id}
              onClick={() => handleColorChange(color.id)}
              className={`relative w-9 h-9 rounded-full ${
                selectedColor === color.id
                  ? 'ring-2 ring-offset-2 ring-gray-900 dark:ring-gray-100'
                  : ''
              }`}
              style={{ backgroundColor: color.value }}
              aria-label={color.name}
            >
              <span className="sr-only">{color.name}</span>
            </button>
          ))}
        </div>
      </div>

      {/* Size options */}
      <div>
        <div className="flex items-center justify-between">
          <h3 className="text-sm font-medium text-gray-900 dark:text-white">Size</h3>
          <a href="#size-guide" className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500">
            Size guide
          </a>
        </div>
        <div className="grid grid-cols-4 gap-2 mt-3">
          {sizes.map((size) => (
            <button
              key={size.id}
              onClick={() => size.inStock && handleSizeChange(size.id)}
              disabled={!size.inStock}
              className={`py-2 px-3 text-sm font-medium rounded-md border ${
                selectedSize === size.id
                  ? 'bg-gray-900 dark:bg-gray-700 text-white border-gray-900 dark:border-gray-700'
                  : size.inStock
                  ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-white border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
                  : 'bg-gray-100 dark:bg-gray-900 text-gray-400 dark:text-gray-500 border-gray-200 dark:border-gray-800 cursor-not-allowed'
              }`}
            >
              {size.name}
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

How to Use

The ProductVariants component allows users to select color and size options for a product. It handles the state of selected variants and provides callbacks when selections change.

// Example usage
const colorOptions = [
  { id: "color1", name: "Black", value: "#000000" },
  { id: "color2", name: "White", value: "#ffffff" },
  { id: "color3", name: "Red", value: "#ef4444" }
];

const sizeOptions = [
  { id: "size1", name: "XS", inStock: true },
  { id: "size2", name: "S", inStock: true },
  { id: "size3", name: "M", inStock: true },
  { id: "size4", name: "L", inStock: false },
  { id: "size5", name: "XL", inStock: true }
];

const handleVariantChange = (colorId, sizeId) => {
  console.log('Selected color:', colorId);
  console.log('Selected size:', sizeId);
  // Update product price, availability, etc.
};

<ProductVariants 
  colors={colorOptions} 
  sizes={sizeOptions} 
  onVariantChange={handleVariantChange} 
/>

Implementation Guide

1. Set Up Your Project

Start with a Next.js project using Tailwind CSS. If you don't have one, create it using:

npx create-next-app@latest my-ecommerce --typescript --tailwind --eslint

2. Create Component Directory

Organize your components in a structured way:

mkdir -p components/products

3. Add Components

Copy the component code into separate files in your components/products directory:

  • components/products/ProductCard.tsx
  • components/products/ProductGallery.tsx
  • components/products/ProductVariants.tsx

4. Create a Product Page

Combine the components to create a complete product page:

// app/products/[id]/page.tsx
import ProductGallery from '@/components/products/ProductGallery';
import ProductVariants from '@/components/products/ProductVariants';
import { ShoppingCart } from 'lucide-react';

// This would typically come from an API or database
const getProductData = (id: string) => ({
  id,
  name: 'Premium Wireless Headphones',
  description: 'Experience crystal-clear audio with our premium wireless headphones. Features noise cancellation and 20-hour battery life.',
  price: 129.99,
  images: [
    { id: 'img1', src: '/placeholder.svg?height=600&width=600', alt: 'Headphones front view' },
    { id: 'img2', src: '/placeholder.svg?height=600&width=600', alt: 'Headphones side view' },
    { id: 'img3', src: '/placeholder.svg?height=600&width=600', alt: 'Headphones back view' },
  ],
  colors: [
    { id: 'color1', name: 'Black', value: '#000000' },
    { id: 'color2', name: 'White', value: '#ffffff' },
    { id: 'color3', name: 'Blue', value: '#3b82f6' },
  ],
  sizes: [
    { id: 'size1', name: 'One Size', inStock: true },
  ],
});

export default function ProductPage({ params }: { params: { id: string } }) {
  const product = getProductData(params.id);
  
  return (
    <div className="max-w-7xl mx-auto px-4 py-10">
      <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
        {/* Product Gallery */}
        <ProductGallery images={product.images} />
        
        {/* Product Info */}
        <div className="space-y-6">
          <h1 className="text-2xl font-bold">{product.name}</h1>
          <p className="text-xl font-medium">{'$'}{product.price.toFixed(2)}</p>
          
          <div className="border-t border-gray-200 dark:border-gray-800 pt-6">
            <p className="text-gray-600 dark:text-gray-300">{product.description}</p>
          </div>
          
          {/* Product Variants */}
          <ProductVariants 
            colors={product.colors} 
            sizes={product.sizes} 
            onVariantChange={(color, size) => {
              console.log('Selected:', color, size);
            }} 
          />
          
          {/* Add to Cart */}
          <button className="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md flex items-center justify-center">
            <ShoppingCart className="mr-2 h-5 w-5" />
            Add to Cart
          </button>
        </div>
      </div>
    </div>
  );
}

Best Practices

Accessibility

Ensure all interactive elements have proper ARIA attributes and keyboard navigation support.

Performance

Use Next.js Image component with proper sizing and formats to optimize image loading.

Responsive Design

Test components on various screen sizes and ensure they adapt appropriately.

State Management

For larger applications, consider using React Context or a state management library like Zustand for cart functionality.

Ready to Use These Components?

Download the complete component collection and start building your ecommerce site today!

View on GitHub Live Demo