Marketing
Product Card
Full-bleed product tile with serif name and a Meet cue.
pnpm dlx shadcn@latest add @paragon/product-cardBrings in@paragon/eyebrow@paragon/panel@base-ui/reactlucide-reactcn
registry/paragon/components/product-card.tsx
import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { ArrowRight } from "lucide-react"
import { cn } from "cn"
import { Eyebrow } from "@/registry/paragon/components/eyebrow"
import { panelVariants } from "@/registry/paragon/components/panel"
/**
* Full-bleed product tile: eyebrow up top, serif product name, one-line
* promise and a "Meet …" cue at the bottom. Pass `image` for photography,
* otherwise the `tone` gradient is used.
*/
function ProductCard({
eyebrow,
name,
description,
cta,
image,
tone = "night",
className,
render,
...props
}: Omit<useRender.ComponentProps<"a">, "children"> & {
eyebrow?: React.ReactNode
name: React.ReactNode
description?: React.ReactNode
cta?: React.ReactNode
/** Background image URL. A night scrim keeps the text legible. */
image?: string
tone?: "night" | "dusk" | "haze"
}) {
return useRender({
defaultTagName: "a",
props: mergeProps<"a">(
{
className: cn(
panelVariants({ tone, padding: "none" }),
"group/product @container dark flex min-h-[26rem] flex-col justify-between p-6 text-white outline-none focus-visible:ring-3 focus-visible:ring-ring/50 md:p-8",
className
),
children: (
<>
{image ? (
<>
<span
aria-hidden="true"
className="absolute inset-0 -z-20 bg-cover bg-center transition-transform duration-700 ease-paragon group-hover/product:scale-[1.03]"
style={{ backgroundImage: `url(${image})` }}
/>
<span
aria-hidden="true"
className="absolute inset-0 -z-10 bg-[linear-gradient(180deg,color-mix(in_oklab,var(--night),transparent_70%)_0%,transparent_35%,color-mix(in_oklab,var(--night),transparent_35%)_100%)]"
/>
</>
) : null}
{eyebrow ? <Eyebrow tone="inverse">{eyebrow}</Eyebrow> : <span />}
<div className="flex flex-col gap-3">
<h3 className="font-serif text-title font-normal [font-size:min(3.25rem,13cqi)] break-words">{name}</h3>
{description ? (
<p className="max-w-sm text-lead text-white/80">{description}</p>
) : null}
{cta ? (
<span className="mt-2 inline-flex w-fit items-center gap-2 border-b border-white/60 pb-0.5 text-lead font-medium transition-colors group-hover/product:border-white">
{cta}
<ArrowRight
aria-hidden="true"
strokeWidth={1.75}
className="size-[1.05em] transition-transform duration-300 ease-paragon group-hover/product:translate-x-0.5 rtl:-scale-x-100"
/>
</span>
) : null}
</div>
</>
),
},
props
),
render,
state: { slot: "product-card", tone },
})
}
export { ProductCard }