Product Cards

A collection of product card designs, featuring various layouts for showcasing products

Minimal Card

Installation

Install the required dependencies:

npm install lucide-react react
pnpm install lucide-react react
yarn add lucide-react react
bun add lucide-react react
/**
 * @author: @fridsonfirmino
 * @description: Product Card Minimal - MVP Development Theme
 * @version: 1.1.0
 * @date: 2026-07-27
 * @license: MIT
 * @github: https://github.com/fridsonfirmino
 */

'use client';

import { cn } from '@/lib/utils';
import { Heart, ShoppingCart } from 'lucide-react';
import { useState } from 'react';

interface ProductCardMinimalProps {
  className?: string;
  title?: string;
  category?: string;
  price?: number;
  image?: string;
  isNew?: boolean;
}

export default function ProductCardMinimal({
  className,
  title = 'Wireless Headphones',
  category = 'Audio',
  price = 79.99,
  image = 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=600&q=80',
  isNew = true,
}: ProductCardMinimalProps) {
  const [isSaved, setIsSaved] = useState(false);
  const [isAdding, setIsAdding] = useState(false);
  const [imgLoaded, setImgLoaded] = useState(false);

  const handleAddToCart = () => {
    setIsAdding(true);
    window.setTimeout(() => setIsAdding(false), 400);
  };

  return (
    <article
      className={cn(
        'group bg-card w-full max-w-sm rounded-xl border shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg',
        className,
      )}
    >
      <div className="relative overflow-hidden rounded-t-xl">
        {/* Placeholder pulse while image loads */}
        <div
          className={cn(
            'bg-muted absolute inset-0 animate-pulse transition-opacity duration-300',
            imgLoaded ? 'opacity-0' : 'opacity-100',
          )}
        />
        <img
          src={image}
          alt={title}
          onLoad={() => setImgLoaded(true)}
          className={cn(
            'h-64 w-full object-cover transition-all duration-500 group-hover:scale-105',
            imgLoaded ? 'opacity-100' : 'opacity-0',
          )}
          loading="lazy"
        />

        {isNew && (
          <span className="bg-primary text-primary-foreground absolute top-3 left-3 rounded-md px-2.5 py-1 text-xs font-medium shadow-sm">
            New
          </span>
        )}

        {/* Wishlist toggle — appears on hover, stays visible once saved */}
        <button
          type="button"
          onClick={() => setIsSaved((v) => !v)}
          aria-label={isSaved ? 'Remove from wishlist' : 'Add to wishlist'}
          className={cn(
            'absolute top-3 right-3 flex h-8 w-8 items-center justify-center rounded-full bg-white/90 shadow-sm backdrop-blur transition-all duration-300',
            'opacity-0 group-hover:opacity-100',
            isSaved && 'opacity-100',
          )}
        >
          <Heart
            className={cn(
              'h-4 w-4 transition-all duration-200',
              isSaved
                ? 'scale-110 fill-red-500 text-red-500'
                : 'text-neutral-600',
            )}
          />
        </button>
      </div>

      <div className="space-y-3 p-4">
        <div className="flex items-center justify-between">
          <span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
            {category}
          </span>
          <span className="text-foreground text-lg font-bold">
            ${price.toFixed(2)}
          </span>
        </div>

        <h3 className="text-foreground text-base font-semibold">{title}</h3>

        <button
          type="button"
          onClick={handleAddToCart}
          className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring flex w-full items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium transition-all duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none active:scale-95"
        >
          <ShoppingCart
            className={cn(
              'h-4 w-4 transition-transform duration-300',
              isAdding && 'scale-125 -rotate-6',
            )}
            aria-hidden="true"
          />
          {isAdding ? 'Added' : 'Add to Cart'}
        </button>
      </div>
    </article>
  );
}

Usage

import ProductCardMinimal from "@/components/mvpblocks/cards/product/product-card-minimal";

export default function MyComponent() {
  return (
    <div className="flex items-center justify-center min-h-screen p-8">
      <ProductCardMinimal
        title="Wireless Headphones"
        category="Audio"
        price={79.99}
        image="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=600&q=80"
        isNew
      />
    </div>
  );
}

Minimal Card API

PropTypeDefault
className?
string
undefined
title?
string
"Wireless Headphones"
category?
string
"Audio"
price?
number
79.99
image?
string
"https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=600&q=80"
isNew?
boolean
true

Features

  • Wishlist toggle — Heart button appears on hover and persists once activated
  • "New" badge — Conditional badge in the top-left corner
  • Add to Cart — Animated button with visual feedback on click
  • Image lazy loading — Smooth reveal with a skeleton pulse placeholder

Styling and Animation Details

  • Card lifthover:-translate-y-1 with hover:shadow-lg for a subtle elevation effect
  • Image zoomgroup-hover:scale-105 on the image for a gentle zoom
  • Wishlist reveal — Button transitions from opacity-0 to opacity-100 on card hover
  • Heart fillscale-110 and fill-red-500 on toggle
  • Cart feedback — Cart icon scales and rotates (scale-125 -rotate-6) on add
  • Skeleton loadinganimate-pulse placeholder fades out as the image loads

Modern Card

Installation

Install the required dependencies:

npm install lucide-react react
pnpm install lucide-react react
yarn add lucide-react react
bun add lucide-react react
/**
 * @author: @fridsonfirmino
 * @description: Product Card Modern - MVP Development Theme
 * @version: 1.1.0
 * @date: 2026-07-27
 * @license: MIT
 * @github: https://github.com/fridsonfirmino
 */

'use client';

import { cn } from '@/lib/utils';
import { Heart, ShoppingCart, Star } from 'lucide-react';
import { useState } from 'react';

interface ProductCardModernProps {
  className?: string;
  title?: string;
  category?: string;
  description?: string;
  price?: number;
  previousPrice?: number;
  image?: string;
  rating?: number;
  reviewCount?: number;
  discount?: number;
}

export default function ProductCardModern({
  className,
  title = 'Running Shoes',
  category = 'Footwear',
  description = 'Lightweight and responsive running shoes with breathable mesh upper and cushioned sole for maximum comfort.',
  price = 89.99,
  previousPrice = 129.99,
  image = 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=600&q=80',
  rating = 4,
  reviewCount = 128,
  discount = 31,
}: ProductCardModernProps) {
  const [isWishlisted, setIsWishlisted] = useState(false);
  const [isAdding, setIsAdding] = useState(false);

  const handleAddToCart = () => {
    setIsAdding(true);
    window.setTimeout(() => setIsAdding(false), 400);
  };

  return (
    <article
      className={cn(
        'group bg-card w-full max-w-sm rounded-xl border shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg',
        className,
      )}
    >
      <div className="relative overflow-hidden rounded-t-xl">
        <img
          src={image}
          alt={title}
          className="h-72 w-full object-cover transition-transform duration-500 group-hover:scale-105"
          loading="lazy"
        />
        <div className="absolute inset-0 bg-linear-to-t from-black/20 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />

        {/* Wishlist — sole occupant of the image overlay now */}
        <button
          type="button"
          onClick={() => setIsWishlisted((prev) => !prev)}
          className="focus-visible:ring-ring absolute top-3 right-3 flex h-9 w-9 items-center justify-center rounded-full bg-white/80 backdrop-blur-sm transition-all duration-200 hover:scale-105 hover:bg-white focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none dark:bg-zinc-900/80 dark:hover:bg-zinc-900"
          aria-label={isWishlisted ? 'Remove from wishlist' : 'Add to wishlist'}
        >
          <Heart
            className={cn(
              'h-5 w-5 transition-all duration-200',
              isWishlisted
                ? 'scale-110 fill-red-500 text-red-500'
                : 'text-zinc-700 dark:text-zinc-300',
            )}
          />
        </button>
      </div>

      <div className="space-y-3 p-4">
        <span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
          {category}
        </span>

        <h3 className="text-foreground text-base font-semibold">{title}</h3>

        <p className="text-muted-foreground line-clamp-2 text-sm leading-relaxed text-ellipsis">
          {description}
        </p>

        <div className="flex items-center gap-1.5">
          {[1, 2, 3, 4, 5].map((star) => (
            <Star
              key={star}
              className={cn(
                'h-4 w-4 transition-transform duration-200 group-hover:scale-110',
                star <= rating
                  ? 'fill-yellow-400 text-yellow-400'
                  : 'fill-none text-zinc-300 dark:text-zinc-600',
              )}
              style={{ transitionDelay: `${star * 40}ms` }}
            />
          ))}
          <span className="text-muted-foreground ml-1 text-xs">
            ({reviewCount} reviews)
          </span>
        </div>

        {/* Price + discount now share one visual unit, same accent color */}
        <div className="flex items-baseline gap-2">
          <span className="text-foreground text-xl font-bold">
            ${price.toFixed(2)}
          </span>
          {previousPrice && (
            <span className="text-muted-foreground text-sm line-through">
              ${previousPrice.toFixed(2)}
            </span>
          )}
          {discount > 0 && (
            <span className="text-xs font-semibold text-rose-600 dark:text-rose-400">
              -{discount}%
            </span>
          )}
        </div>

        <button
          type="button"
          onClick={handleAddToCart}
          className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring flex w-full items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium transition-all duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none active:scale-95"
        >
          <ShoppingCart
            className={cn(
              'h-4 w-4 transition-transform duration-300',
              isAdding && 'scale-125 -rotate-6',
            )}
            aria-hidden="true"
          />
          {isAdding ? 'Added' : 'Add to Cart'}
        </button>
      </div>
    </article>
  );
}

Usage

import ProductCardModern from "@/components/mvpblocks/cards/product/product-card-modern";

export default function MyComponent() {
  return (
    <div className="flex items-center justify-center min-h-screen p-8">
      <ProductCardModern
        title="Running Shoes"
        category="Footwear"
        price={89.99}
        previousPrice={129.99}
        image="https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=600&q=80"
        rating={4}
        reviewCount={128}
        discount={31}
      />
    </div>
  );
}

Modern Card API

PropTypeDefault
className?
string
undefined
title?
string
"Running Shoes"
category?
string
"Footwear"
description?
string
"Lightweight and responsive running shoes..."
price?
number
89.99
previousPrice?
number
129.99
image?
string
"https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=600&q=80"
rating?
number
4
reviewCount?
number
128
discount?
number
31

Features

  • Star rating display — Five interactive stars with staggered hover animation
  • Discount badge — Percentage-off label next to the strikethrough original price
  • Product description — Truncated to 2 lines with line-clamp-2
  • Wishlist toggle — Heart button with backdrop blur and dark mode support
  • Add to Cart — Animated button with visual feedback

Styling and Animation Details

  • Card lifthover:-translate-y-1 with hover:shadow-lg elevation
  • Image zoomgroup-hover:scale-105 on hover
  • Gradient overlaybg-linear-to-t from-black/20 fades in on the image area
  • Staggered stars — Each star scales up with a transitionDelay of star * 40ms
  • Wishlist — Backdrop blur (backdrop-blur-sm) with dark mode variants
  • Heart fillscale-110 and fill-red-500 on activation
  • Cart feedback — Icon scale-125 -rotate-6 on click

Premium Card

Installation

Install the required dependencies:

npm install lucide-react react
pnpm install lucide-react react
yarn add lucide-react react
bun add lucide-react react
/**
 * @author: @fridsonfirmino
 * @description: Product Card Premium - MVP Development Theme
 * @version: 1.1.0
 * @date: 2026-07-27
 * @license: MIT
 * @github: https://github.com/fridsonfirmino
 */

'use client';

import { cn } from '@/lib/utils';
import { ArrowUpRight } from 'lucide-react';
import { useState } from 'react';

interface ProductCardPremiumProps {
  className?: string;
  title?: string;
  reference?: string;
  category?: string;
  description?: string;
  price?: number;
  image?: string;
  isPremium?: boolean;
}

export default function ProductCardPremium({
  className,
  title = 'Heritage Chronograph',
  reference = 'Ref. 5172G-001',
  category = 'Luxury Watches',
  description = 'Swiss-made automatic movement with sapphire crystal, genuine leather strap, and a refined sunburst dial. Water-resistant to 100 meters.',
  price = 1250.0,
  image = 'https://images.unsplash.com/photo-1524592094714-0f0654e20314?w=600&q=80',
  isPremium = true,
}: ProductCardPremiumProps) {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <article
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      className={cn(
        'group w-full max-w-sm overflow-hidden rounded-sm border border-zinc-200 bg-white transition-all duration-700 ease-out hover:border-zinc-300 hover:shadow-[0_20px_60px_-15px_rgba(0,0,0,0.15)] dark:border-zinc-800 dark:bg-zinc-950 dark:hover:border-zinc-700',
        className,
      )}
    >
      <div className="relative overflow-hidden">
        <img
          src={image}
          alt={title}
          className="h-80 w-full object-cover transition-transform duration-[1200ms] ease-out group-hover:scale-[1.04]"
          loading="lazy"
        />
        <div className="pointer-events-none absolute inset-0 bg-linear-to-t from-black/40 via-transparent to-transparent opacity-0 transition-opacity duration-700 group-hover:opacity-100" />

        {isPremium && (
          <span className="absolute top-5 left-5 border border-white/40 px-3 py-1 text-[10px] font-light tracking-[0.25em] text-white uppercase">
            Limited Edition
          </span>
        )}

        {/* Reference number surfaces only on hover — a quiet, catalog-like detail */}
        <span
          className={cn(
            'absolute right-5 bottom-5 text-[11px] font-light tracking-wider text-white/90 transition-all duration-700',
            isHovered ? 'translate-y-0 opacity-100' : 'translate-y-1 opacity-0',
          )}
        >
          {reference}
        </span>
      </div>

      <div className="space-y-5 px-7 py-8">
        <div className="space-y-2">
          <span className="text-[10px] font-medium tracking-[0.25em] text-zinc-400 uppercase dark:text-zinc-500">
            {category}
          </span>
          <h3 className="font-serif text-2xl leading-snug font-medium tracking-tight text-zinc-900 dark:text-zinc-50">
            {title}
          </h3>
        </div>

        <div className="h-px w-8 bg-zinc-300 transition-all duration-700 group-hover:w-16 dark:bg-zinc-700" />

        <p className="text-[13px] leading-relaxed text-zinc-500 dark:text-zinc-400">
          {description}
        </p>

        <div className="flex items-end justify-between pt-3">
          <span className="text-lg font-light tracking-tight text-zinc-900 dark:text-zinc-50">
            $
            {price.toLocaleString('en-US', {
              minimumFractionDigits: 2,
              maximumFractionDigits: 2,
            })}
          </span>

          <button
            type="button"
            className="group/btn flex items-center gap-1.5 border-b border-zinc-900 pb-1 text-[13px] font-medium tracking-wide text-zinc-900 transition-all duration-300 hover:gap-2.5 hover:border-zinc-400 focus-visible:outline-none dark:border-zinc-50 dark:text-zinc-50"
          >
            Discover
            <ArrowUpRight
              className="h-3.5 w-3.5 transition-transform duration-300 group-hover/btn:translate-x-0.5 group-hover/btn:-translate-y-0.5"
              aria-hidden="true"
            />
          </button>
        </div>
      </div>
    </article>
  );
}

Usage

import ProductCardPremium from "@/components/mvpblocks/cards/product/product-card-premium";

export default function MyComponent() {
  return (
    <div className="flex items-center justify-center min-h-screen p-8">
      <ProductCardPremium
        title="Heritage Chronograph"
        reference="Ref. 5172G-001"
        category="Luxury Watches"
        price={1250}
        image="https://images.unsplash.com/photo-1524592094714-0f0654e20314?w=600&q=80"
        isPremium
      />
    </div>
  );
}

Premium Card API

PropTypeDefault
className?
string
undefined
title?
string
"Heritage Chronograph"
reference?
string
"Ref. 5172G-001"
category?
string
"Luxury Watches"
description?
string
"Swiss-made automatic movement..."
price?
number
1250
image?
string
"https://images.unsplash.com/photo-1524592094714-0f0654e20314?w=600&q=80"
isPremium?
boolean
true

Features

  • "Limited Edition" badge — Premium border badge in the top-left
  • Reference number — Slides up on hover for a catalog-like detail
  • Discover CTA — Button with animated arrow that translates on hover
  • Serif typographyfont-serif for the product title, light font weights throughout
  • Dark mode — Full dark mode support with dark: variants

Styling and Animation Details

  • Image zoomgroup-hover:scale-[1.04] over 1200ms for a slow, cinematic reveal
  • Gradient overlay — Fades in over 700ms on hover, from black/40 to transparent
  • Reference number — Translates translate-y-1 to translate-y-0 with opacity-0 to opacity-100
  • Divider line — Expands from w-8 to w-16 on hover over 700ms
  • Arrow animationgroup-hover/btn:translate-x-0.5 group-hover/btn:-translate-y-0.5 for a diagonal nudge
  • Button spacinghover:gap-2.5 widens the gap between text and arrow
  • Rounded corners — Uses rounded-sm for a sharper, more refined look

Carrossel Card

Installation

Install the required dependencies:

npm install lucide-react react
pnpm install lucide-react react
yarn add lucide-react react
bun add lucide-react react
/**
 * @author: @fridsonfirmino
 * @description: Product Card Carousel - MVP Development Theme
 * @version: 1.1.0
 * @date: 2026-07-27
 * @license: MIT
 * @github: https://github.com/fridsonfirmino
 */

'use client';

import { cn } from '@/lib/utils';
import { ChevronLeft, ChevronRight, ShoppingCart, Star } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';

interface ProductCardCarrosselProps {
  className?: string;
  title?: string;
  category?: string;
  price?: number;
  rating?: number;
  reviewCount?: number;
  images?: string[];
}

const AUTO_PLAY_INTERVAL = 1600;

export default function ProductCardCarrossel({
  className,
  title = 'Mechanical Keyboard',
  category = 'Accessories',
  price = 149.99,
  rating = 4.5,
  reviewCount = 324,
  images = [
    'https://images.unsplash.com/photo-1587829741301-dc798b83add3?w=600&q=80',
    'https://images.unsplash.com/photo-1541140532154-b024d705b90a?w=600&q=80',
  ],
}: ProductCardCarrosselProps) {
  const [index, setIndex] = useState(0);
  const [isHovered, setIsHovered] = useState(false);
  const [tilt, setTilt] = useState({ x: 0, y: 0 });
  const [isAdding, setIsAdding] = useState(false);
  const cardRef = useRef<HTMLElement>(null);
  const dragStartX = useRef<number | null>(null);
  const autoplayRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const goTo = (i: number) => {
    setIndex((i + images.length) % images.length);
  };

  // Auto-advance only while hovered — the carousel "introduces itself"
  useEffect(() => {
    if (isHovered) {
      autoplayRef.current = setInterval(() => {
        setIndex((prev) => (prev + 1) % images.length);
      }, AUTO_PLAY_INTERVAL);
    }
    return () => {
      if (autoplayRef.current) clearInterval(autoplayRef.current);
    };
  }, [isHovered, images.length]);

  const handleMouseMove = (e: React.MouseEvent<HTMLElement>) => {
    if (!cardRef.current) return;
    const rect = cardRef.current.getBoundingClientRect();
    const px = (e.clientX - rect.left) / rect.width - 0.5;
    const py = (e.clientY - rect.top) / rect.height - 0.5;
    setTilt({ x: py * -4, y: px * 6 });
  };

  const resetTilt = () => {
    setIsHovered(false);
    setTilt({ x: 0, y: 0 });
  };

  const handlePointerDown = (e: React.PointerEvent) => {
    dragStartX.current = e.clientX;
  };

  const handlePointerUp = (e: React.PointerEvent) => {
    if (dragStartX.current === null) return;
    const delta = e.clientX - dragStartX.current;
    if (Math.abs(delta) > 40) {
      goTo(delta > 0 ? index - 1 : index + 1);
    }
    dragStartX.current = null;
  };

  const handleAddToCart = () => {
    setIsAdding(true);
    window.setTimeout(() => setIsAdding(false), 400);
  };

  return (
    <article
      ref={cardRef}
      onMouseEnter={() => setIsHovered(true)}
      onMouseMove={handleMouseMove}
      onMouseLeave={resetTilt}
      style={{
        transform: `perspective(1000px) rotateX(${tilt.x}deg) rotateY(${tilt.y}deg)`,
        transformStyle: 'preserve-3d',
      }}
      className={cn(
        'bg-card w-full max-w-sm rounded-xl border shadow-sm transition-[transform,box-shadow] duration-300 ease-out will-change-transform hover:shadow-xl',
        className,
      )}
    >
      <div
        className="relative h-64 touch-pan-y overflow-hidden rounded-t-xl select-none"
        onPointerDown={handlePointerDown}
        onPointerUp={handlePointerUp}
      >
        {/* Slides — translateX with a slight overshoot easing, distinct from the scale hovers used elsewhere */}
        <div
          className="flex h-full transition-transform duration-500 ease-[cubic-bezier(0.34,1.56,0.64,1)]"
          style={{ transform: `translateX(-${index * 100}%)` }}
        >
          {images.map((src, i) => (
            <img
              key={src}
              src={src}
              alt={`${title} — ${i + 1}`}
              className="h-64 w-full flex-shrink-0 object-cover"
              loading="lazy"
              draggable={false}
            />
          ))}
        </div>

        {/* Arrows fade in on hover only */}
        <button
          type="button"
          onClick={() => goTo(index - 1)}
          aria-label="Previous image"
          className={cn(
            'absolute top-1/2 left-2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-zinc-700 shadow-sm transition-all duration-200',
            isHovered ? 'opacity-100' : 'pointer-events-none opacity-0',
          )}
        >
          <ChevronLeft className="h-4 w-4" />
        </button>
        <button
          type="button"
          onClick={() => goTo(index + 1)}
          aria-label="Next image"
          className={cn(
            'absolute top-1/2 right-2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-zinc-700 shadow-sm transition-all duration-200',
            isHovered ? 'opacity-100' : 'pointer-events-none opacity-0',
          )}
        >
          <ChevronRight className="h-4 w-4" />
        </button>

        {/* Dots — active one stretches into a pill instead of just changing color */}
        <div className="absolute bottom-3 left-1/2 flex -translate-x-1/2 gap-1.5">
          {images.map((_, i) => (
            <button
              key={i}
              type="button"
              onClick={() => goTo(i)}
              aria-label={`Go to image ${i + 1}`}
              className={cn(
                'h-1.5 rounded-full bg-white/60 shadow-sm transition-all duration-300',
                i === index ? 'w-5 bg-white' : 'w-1.5 hover:bg-white/90',
              )}
            />
          ))}
        </div>
      </div>

      <div className="space-y-3 p-4">
        <span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
          {category}
        </span>
        <h3 className="text-foreground text-base font-semibold">{title}</h3>
        <div className="flex items-center gap-3">
          <div className="flex items-center gap-0.5">
            {[1, 2, 3, 4, 5].map((star) => {
              const filled = star <= Math.floor(rating);
              const half =
                !filled && star === Math.ceil(rating) && rating % 1 !== 0;
              return (
                <Star
                  key={star}
                  className={cn(
                    'h-4 w-4',
                    filled
                      ? 'fill-yellow-400 text-yellow-400'
                      : half
                        ? 'fill-yellow-400/50 text-yellow-400'
                        : 'fill-none text-zinc-300 dark:text-zinc-600',
                  )}
                  aria-hidden="true"
                />
              );
            })}
          </div>
          <span className="text-muted-foreground text-xs">({reviewCount})</span>
        </div>
        <div className="flex items-center justify-between pt-1">
          <span className="text-foreground text-xl font-bold">
            ${price.toFixed(2)}
          </span>
          <button
            type="button"
            onClick={handleAddToCart}
            className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none active:scale-95"
          >
            <ShoppingCart
              className={cn(
                'h-4 w-4 transition-transform duration-300',
                isAdding && 'scale-125 -rotate-6',
              )}
              aria-hidden="true"
            />
            {isAdding ? 'Added' : 'Add to Cart'}
          </button>
        </div>
      </div>
    </article>
  );
}

Usage

import ProductCardCarrossel from "@/components/mvpblocks/cards/product/product-card-carrossel";

export default function MyComponent() {
  return (
    <div className="flex items-center justify-center min-h-screen p-8">
      <ProductCardCarrossel
        title="Mechanical Keyboard"
        category="Accessories"
        price={149.99}
        rating={4.5}
        reviewCount={324}
        images={[
          "https://images.unsplash.com/photo-1587829741301-dc798b83add3?w=600&q=80",
          "https://images.unsplash.com/photo-1541140532154-b024d705b90a?w=600&q=80",
        ]}
      />
    </div>
  );
}

Carrossel Card API

PropTypeDefault
className?
string
undefined
title?
string
"Mechanical Keyboard"
category?
string
"Accessories"
price?
number
149.99
rating?
number
4.5
reviewCount?
number
324
images?
string[]
['https://images.unsplash.com/photo-1587829741301-dc798b83add3?w=600&q=80', ...]

Features

  • Image carousel — Swipeable and arrow-navigable image gallery
  • Auto-play — Automatically advances images at 1.6s intervals on hover
  • 3D tilt effect — Perspective transform follows mouse movement
  • Star rating — Supports full, half, and empty star states
  • Dot indicators — Active dot stretches into a pill shape
  • Swipe support — Pointer events with 40px threshold for touch/click drag
  • Add to Cart — Animated button with visual feedback

Styling and Animation Details

  • 3D tiltperspective(1000px) rotateX() rotateY() computed from mouse position
  • Slide transitioncubic-bezier(0.34, 1.56, 0.64, 1) for an overshoot easing
  • Auto-play — Starts on mouseenter, stops on mouseleave via setInterval
  • Navigation arrows — Fade in on hover with opacity transition
  • Dot pill — Active dot expands from w-1.5 to w-5 with rounded-full
  • Cart feedback — Icon scale-125 -rotate-6 on add

Discount Card

Installation

Install the required dependencies:

npm install lucide-react react
pnpm install lucide-react react
yarn add lucide-react react
bun add lucide-react react
/**
 * @author: @fridsonfirmino
 * @description: Product Card Discount - MVP Development Theme
 * @version: 1.1.0
 * @date: 2026-07-27
 * @license: MIT
 * @github: https://github.com/fridsonfirmino
 */

'use client';

import { cn } from '@/lib/utils';
import { ShoppingCart } from 'lucide-react';
import { useState } from 'react';

interface ProductCardDiscountProps {
  className?: string;
  title?: string;
  category?: string;
  originalPrice?: number;
  discountedPrice?: number;
  discountPercent?: number;
  image?: string;
}

export default function ProductCardDiscount({
  className,
  title = 'Running Shoes',
  category = 'Footwear',
  originalPrice = 189.99,
  discountedPrice = 132.99,
  discountPercent = 31,
  image = 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?w=600&q=80',
}: ProductCardDiscountProps) {
  const [isAdding, setIsAdding] = useState(false);

  const handleAddToCart = () => {
    setIsAdding(true);
    window.setTimeout(() => setIsAdding(false), 400);
  };

  return (
    <article
      className={cn(
        'group relative h-[520px] w-full max-w-sm overflow-hidden rounded-xl border p-2 shadow-sm transition-all duration-300 hover:shadow-xl',
        className,
      )}
    >
      {/* Full-bleed image */}
      <img
        src={image}
        alt={title}
        className="absolute inset-0 h-full w-full object-cover transition-transform duration-700 ease-out group-hover:scale-110"
        loading="lazy"
      />

      {/* Gradient deepens slightly on hover so the copy stays readable as the image moves */}
      <div className="absolute inset-0 bg-linear-to-t from-black/90 via-black/40 to-black/10 transition-opacity duration-500 group-hover:from-black/95 group-hover:via-black/50" />

      {/* Badges — discount pulses gently to draw the eye, sale badge stays still for contrast */}
      <div className="absolute top-4 left-4 flex flex-col gap-1.5">
        <span className="w-fit animate-[pulse_2.2s_ease-in-out_infinite] rounded-md bg-rose-600 px-2.5 py-1 text-xs font-bold text-white shadow-sm">
          -{discountPercent}%
        </span>
        <span className="w-fit rounded-md bg-amber-500 px-2.5 py-1 text-xs font-bold text-white shadow-sm">
          Sale
        </span>
      </div>

      {/* Content pinned to the bottom, over the gradient */}
      <div className="absolute inset-x-0 bottom-0 space-y-3 p-5">
        <span className="text-xs font-medium tracking-wider text-white/70 uppercase">
          {category}
        </span>
        <h3 className="text-lg leading-snug font-semibold text-white">
          {title}
        </h3>

        <div className="flex items-baseline gap-2.5">
          <span className="text-2xl font-bold text-white">
            ${discountedPrice.toFixed(2)}
          </span>
          <span className="text-sm text-white/60 line-through">
            ${originalPrice.toFixed(2)}
          </span>
          <span className="rounded bg-green-500/20 px-1.5 py-0.5 text-xs font-semibold text-green-400">
            Save ${(originalPrice - discountedPrice).toFixed(0)}
          </span>
        </div>

        {/* Button sits low by default, rises into place on hover — reveals itself rather than always occupying space */}
        <button
          type="button"
          onClick={handleAddToCart}
          className="flex w-full translate-y-1 items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-medium text-zinc-900 opacity-90 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100 hover:bg-white/90 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-black focus-visible:outline-none active:scale-95"
        >
          <ShoppingCart
            className={cn(
              'h-4 w-4 transition-transform duration-300',
              isAdding && 'scale-125 -rotate-6',
            )}
            aria-hidden="true"
          />
          {isAdding ? 'Added' : 'Shop Now'}
        </button>
      </div>
    </article>
  );
}

Usage

import ProductCardDiscount from "@/components/mvpblocks/cards/product/product-card-discount";

export default function MyComponent() {
  return (
    <div className="flex items-center justify-center min-h-screen p-8">
      <ProductCardDiscount
        title="Running Shoes"
        category="Footwear"
        originalPrice={189.99}
        discountedPrice={132.99}
        discountPercent={31}
        image="https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?w=600&q=80"
      />
    </div>
  );
}

Discount Card API

PropTypeDefault
className?
string
undefined
title?
string
"Running Shoes"
category?
string
"Footwear"
originalPrice?
number
189.99
discountedPrice?
number
132.99
discountPercent?
number
31
image?
string
"https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?w=600&q=80"

Features

  • Full-bleed background image — Image covers the entire card as a background
  • Discount badge — Pulsing -31% badge to draw attention
  • "Sale" badge — Static amber badge for contrast
  • Price comparison — Discounted price, original strikethrough, and "Save $" badge
  • Animated CTA — "Shop Now" button slides up from below on hover

Styling and Animation Details

  • Image zoomgroup-hover:scale-110 over 700ms for a dramatic reveal
  • Gradient overlaybg-linear-to-t from-black/90 via-black/40 to-black/10 deepens on hover
  • Pulsing badgeanimate-[pulse_2.2s_ease-in-out_infinite] on the discount percentage
  • Button reveal — Slides translate-y-1 to translate-y-0 on card hover
  • Cart feedback — Icon scale-125 -rotate-6 on add
  • Fixed aspect — Fixed height h-[520px] for consistent card sizing

Horizontal Card

Installation

Install the required dependencies:

npm install lucide-react react
pnpm install lucide-react react
yarn add lucide-react react
bun add lucide-react react
/**
 * @author: @fridsonfirmino
 * @description: Product Card Horizontal - MVP Development Theme
 * @version: 1.1.0
 * @date: 2026-07-27
 * @license: MIT
 * @github: https://github.com/fridsonfirmino
 */

'use client';

import { cn } from '@/lib/utils';
import { Minus, Plus, ShoppingCart, Star } from 'lucide-react';
import { useState } from 'react';

interface ProductCardHorizontalProps {
  className?: string;
  showQuantity?: boolean;
  title?: string;
  category?: string;
  description?: string;
  price?: number;
  rating?: number;
  reviewCount?: number;
  inStock?: boolean;
  stockCount?: number;
  image?: string;
}

export default function ProductCardHorizontal({
  className,
  showQuantity = false,
  title = 'Smart Watch',
  category = 'Wearables',
  description = 'Track workouts, heart rate, and sleep with a bright always-on display and up to 5 days of battery life.',
  price = 249.99,
  rating = 4.3,
  reviewCount = 87,
  inStock = true,
  stockCount = 6,
  image = 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=600&q=80',
}: ProductCardHorizontalProps) {
  const [quantity, setQuantity] = useState(1);
  const [isAdding, setIsAdding] = useState(false);

  const handleAddToCart = () => {
    setIsAdding(true);
    window.setTimeout(() => setIsAdding(false), 400);
  };

  const subtotal = (price * quantity).toFixed(2);
  const lowStock = inStock && stockCount <= 8;

  return (
    <article
      className={cn(
        'group bg-card grid max-h-[600px] w-full max-w-dvh translate-x-0 scale-100 grid-cols-1 overflow-hidden rounded-xl border opacity-100 shadow-sm transition-all duration-300 ease-out sm:grid-cols-[auto_1fr] sm:flex-row',
        className,
      )}
      style={{
        transitionProperty: 'max-height, opacity, transform, box-shadow',
      }}
    >
      <div className="relative aspect-4/3 w-full shrink-0 overflow-hidden sm:aspect-square sm:w-44 md:w-52">
        <img
          src={image}
          alt={title}
          className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
          loading="lazy"
        />
        {lowStock && (
          <span className="absolute bottom-2 left-2 rounded-md bg-orange-500/90 px-2 py-0.5 text-[10px] font-semibold text-white shadow-sm">
            Only {stockCount} left
          </span>
        )}
      </div>

      <div className="flex flex-1 flex-col justify-between gap-3 p-4">
        <div className="space-y-2">
          <div className="flex items-start justify-between gap-2">
            <div className="space-y-1">
              <div className="flex items-center gap-2">
                <span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
                  {category}
                </span>
                {inStock && (
                  <span className="flex items-center gap-1 text-[11px] font-medium text-emerald-600 dark:text-emerald-400">
                    <span className="relative flex h-1.5 w-1.5">
                      <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-500 opacity-75" />
                      <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
                    </span>
                    In stock
                  </span>
                )}
              </div>
              <h3 className="text-foreground text-base font-semibold">
                {title}
              </h3>
            </div>
          </div>

          <p className="text-muted-foreground line-clamp-2 text-sm leading-relaxed">
            {description}
          </p>

          <div className="flex items-center gap-2">
            <div className="flex items-center gap-0.5">
              {[1, 2, 3, 4, 5].map((star) => (
                <Star
                  key={star}
                  className={cn(
                    'h-3.5 w-3.5',
                    star <= Math.floor(rating)
                      ? 'fill-yellow-400 text-yellow-400'
                      : 'fill-none text-zinc-300 dark:text-zinc-600',
                  )}
                  aria-hidden="true"
                />
              ))}
            </div>
            <span className="text-muted-foreground text-xs">
              ({reviewCount})
            </span>
          </div>
        </div>

        <div className="flex flex-wrap items-center justify-between gap-3">
          <div className="flex items-center gap-3">
            {showQuantity && (
              <div className="border-input flex items-center rounded-lg border">
                <button
                  type="button"
                  onClick={() => setQuantity((q) => Math.max(1, q - 1))}
                  disabled={quantity <= 1}
                  className="text-muted-foreground hover:bg-secondary flex h-8 w-8 items-center justify-center rounded-l-lg transition-colors disabled:cursor-not-allowed disabled:opacity-40"
                  aria-label="Decrease quantity"
                >
                  <Minus className="h-3.5 w-3.5" />
                </button>
                <span className="w-7 text-center text-sm font-medium tabular-nums">
                  {quantity}
                </span>
                <button
                  type="button"
                  onClick={() =>
                    setQuantity((q) => Math.min(stockCount, q + 1))
                  }
                  disabled={quantity >= stockCount}
                  className="text-muted-foreground hover:bg-secondary flex h-8 w-8 items-center justify-center rounded-r-lg transition-colors disabled:cursor-not-allowed disabled:opacity-40"
                  aria-label="Increase quantity"
                >
                  <Plus className="h-3.5 w-3.5" />
                </button>
              </div>
            )}

            <span className="text-foreground text-xl font-bold tabular-nums">
              ${showQuantity ? subtotal : price.toFixed(2)}
            </span>
          </div>

          <button
            type="button"
            onClick={handleAddToCart}
            className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none active:scale-95"
          >
            <ShoppingCart
              className={cn(
                'h-4 w-4 transition-transform duration-300',
                isAdding && 'scale-125 -rotate-6',
              )}
              aria-hidden="true"
            />
            {isAdding ? 'Added' : 'Add to Cart'}
          </button>
        </div>
      </div>
    </article>
  );
}

Usage

import ProductCardHorizontal from "@/components/mvpblocks/cards/product/product-card-horinzontal";

export default function MyComponent() {
  return (
    <div className="flex items-center justify-center min-h-screen p-4">
      <ProductCardHorizontal
        title="Smart Watch"
        category="Wearables"
        price={249.99}
        rating={4.3}
        reviewCount={87}
        inStock
        stockCount={6}
        image="https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=600&q=80"
        showQuantity
      />
    </div>
  );
}

Horizontal Card API

PropTypeDefault
className?
string
undefined
showQuantity?
boolean
false
title?
string
"Smart Watch"
category?
string
"Wearables"
description?
string
"Track workouts, heart rate, and sleep..."
price?
number
249.99
rating?
number
4.3
reviewCount?
number
87
inStock?
boolean
true
stockCount?
number
6
image?
string
"https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=600&q=80"

Features

  • Horizontal layout — Image on the left, product details on the right
  • Quantity selector — Optional (showQuantity) increment/decrement controls
  • Star rating display — Five-star visual with review count
  • Stock indicator — "In stock" label with a live ping animation
  • Low stock badge"Only N left" overlay when stock ≤ 8
  • Subtotal calculation — Shows price × quantity when quantity selector is active
  • Add to Cart — Animated button

Styling and Animation Details

  • Responsive gridgrid-cols-1 on mobile, sm:grid-cols-[auto_1fr] on desktop
  • Image zoomgroup-hover:scale-105 on hover
  • Stock pinganimate-ping on the green dot for a live indicator effect
  • Cart feedback — Icon scale-125 -rotate-6 on add
  • Button activeactive:scale-95 for press feedback
  • Tabular numberstabular-nums for consistent price digit widths

Last updated on