# Product Card 1 - Product card with sale badge

> A linked product card with a product image, urgency badge, name, short description, and regular plus sale price.

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

- Page, with a live preview: https://shadcn.exchange/r/shadcnblocks/product-card1
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcnblocks/product-card1-light-7ff32a2658ce01ff.png
- Registry item: https://shadcnblocks.com/r/product-card1.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcnblocks.com/r/product-card1.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: `cn`
- Registry items: `aspect-ratio`, `badge`, `card`

## Files

### `block/product-card1/product-card1.tsx`

```tsx
import { cn } from "cn";

import { Price, PriceValue } from "@/components/shadcnblocks/price";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Badge } from "@/components/ui/badge";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
interface ProductPrice {
  regular: number;
  sale?: number;
  currency: string;
}

interface Product {
  name: string;
  image: {
    src: string;
    alt: string;
  };
  link: string;
  description: string;
  price: ProductPrice;
  badge: {
    text: string;
    backgroundColor?: string;
  };
}

const PRODUCT_CARD: Product = {
  name: "Vexon CoreStep '08 LX",
  image: {
    src: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/ecommerce/clothes/joshua-diaz-ETNoDLl8yFE-unsplash-1.jpg",
    alt: "",
  },
  link: "#",
  description:
    "Everyday comfort meets bold tri-color style in this performance-driven design.",
  price: {
    regular: 499.0,
    sale: 399.0,
    currency: "USD",
  },
  badge: {
    text: "Selling fast!",
    backgroundColor: "oklch(50.5% 0.213 27.518)",
  },
};

interface ProductCard1Props {
  className?: string;
}

const ProductCard1 = ({ className }: ProductCard1Props) => {
  const { regular, sale, currency } = PRODUCT_CARD.price;

  return (
    <a
      href={PRODUCT_CARD.link}
      className={cn(
        "block max-w-md transition-opacity hover:opacity-80",
        className,
      )}
    >
      <Card className="h-full overflow-hidden p-0">
        <CardHeader className="relative block p-0">
          <AspectRatio ratio={1.268115942} className="overflow-hidden">
            <img
              src={PRODUCT_CARD.image.src}
              alt={PRODUCT_CARD.image.alt}
              className="block size-full object-cover object-center"
            />
          </AspectRatio>
          {PRODUCT_CARD.badge && (
            <Badge
              style={{
                background: PRODUCT_CARD.badge.backgroundColor,
              }}
              className="absolute start-4 top-4"
            >
              {PRODUCT_CARD.badge.text}
            </Badge>
          )}
        </CardHeader>
        <CardContent className="flex h-full flex-col gap-4 pb-6">
          <CardTitle className="text-xl font-semibold">
            {PRODUCT_CARD.name}
          </CardTitle>
          <CardDescription className="font-medium text-muted-foreground">
            {PRODUCT_CARD.description}
          </CardDescription>
          <div className="mt-auto">
            <Price onSale={sale != null} className="text-lg font-semibold">
              <PriceValue
                price={regular}
                currency={currency}
                variant="regular"
              />
              <PriceValue price={sale} currency={currency} variant="sale" />
            </Price>
          </div>
        </CardContent>
      </Card>
    </a>
  );
};

export { ProductCard1 };
```

### `components/shadcnblocks/price.tsx`

```tsx
"use client";

import type { ReactNode } from "react";
import { createContext, useContext } from "react";
import { cn } from "cn";

const PriceContext = createContext<{ onSale?: boolean }>({ onSale: false });
export const usePriceContext = () => useContext(PriceContext);

interface PriceProps {
  className?: string;
  children: ReactNode;
  onSale?: boolean;
}

interface PriceValueProps {
  price?: number;
  currency?: string;
  variant?: "regular" | "sale";
  className?: string;
}

export type PriceType = {
  currency?: string;
  regular: number;
  sale?: number;
};

const formatterCache = new Map<string, Intl.NumberFormat>();

function formatCurrency(value: number, currency = "USD", locale = "en-US") {
  const key = `${locale}-${currency}`;
  if (!formatterCache.has(key)) {
    formatterCache.set(
      key,
      new Intl.NumberFormat(locale, { style: "currency", currency }),
    );
  }
  return formatterCache.get(key)!.format(value);
}

const Price = ({ className, children, onSale }: PriceProps) => {
  return (
    <PriceContext.Provider value={{ onSale }}>
      <div className={cn("flex flex-wrap items-center gap-x-2", className)}>
        {children}
      </div>
    </PriceContext.Provider>
  );
};

const PriceValue = ({
  price,
  currency = "USD",
  variant = "regular",
  className,
}: PriceValueProps) => {
  const { onSale } = usePriceContext();

  if (price == null) return null;

  return (
    <span
      className={cn(
        "leading-tight",
        variant === "regular"
          ? onSale
            ? "text-muted-foreground line-through"
            : "text-foreground"
          : "text-foreground",
        className,
      )}
    >
      {formatCurrency(price, currency)}
    </span>
  );
};

export { Price, PriceValue };
```
