Cards

Feature Card

An icon, title, and body card whose icon gives a small animated response on hover.

Install

npx shadcn@latest add @paragon/feature-card

feature-card.tsx

import * as React from "react";
import { BarChart3 } from "lucide-react";
import { cn } from "@/lib/utils";

export interface FeatureCardProps extends React.ComponentProps<"div"> {
  /** Icon for the tile — any lucide icon or small SVG. Sized to 20px. */
  icon?: React.ReactNode;
  /** Feature name, rendered as the card heading. */
  title?: string;
  /** Supporting body copy under the title. */
  description?: React.ReactNode;
  /** Disables the hover response where the motion would distract. */
  static?: boolean;
}

/**
 * An icon, title, and body card. The motion budget is spent on exactly one
 * element: on fine-pointer hover the icon tile lifts 2px on an enumerated
 * transform transition (150ms, ease-out) while the card itself stays still —
 * only its depth border deepens via shadow-border → shadow-border-hover.
 * The nudge is gated behind (hover: hover) and (pointer: fine) and removed
 * entirely under prefers-reduced-motion.
 */
export function FeatureCard({
  icon = <BarChart3 />,
  title = "Real-time analytics",
  description = "Query live product data and share dashboards with your team without waiting on nightly syncs.",
  static: isStatic = false,
  className,
  children,
  ...props
}: FeatureCardProps) {
  return (
    <div
      data-slot="feature-card"
      className={cn(
        "group/feature-card flex flex-col items-start gap-4 rounded-xl bg-card p-5 text-card-foreground shadow-border transition-[box-shadow] duration-(--duration-quick) ease-(--ease-out)",
        !isStatic && "hover:shadow-border-hover",
        className,
      )}
      {...props}
    >
      <div
        aria-hidden
        className={cn(
          "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg]:size-5 [&_svg]:shrink-0",
          !isStatic && [
            "transition-transform duration-(--duration-quick) ease-(--ease-out)",
            "motion-safe:[@media(hover:hover)_and_(pointer:fine)]:group-hover/feature-card:-translate-y-0.5",
          ],
        )}
      >
        {icon}
      </div>
      <div className="flex flex-col gap-1">
        <h3 className="text-sm font-medium">{title}</h3>
        {description && (
          <p className="text-sm text-muted-foreground">{description}</p>
        )}
      </div>
      {children}
    </div>
  );
}