Card (Tailwind)

PreviousNext

A container component for displaying content.

Docs
roiuiitem

Preview

Loading preview…
registry/brook/tailwind/ui/card.tsx
import { cva, type VariantProps } from "class-variance-authority";
import Image from "next/image";
import { cn } from "@/lib/utils";

const cardVariants = cva(
  [
    "flex flex-col gap-6 rounded-[var(--radius)] p-6 text-foreground",
    "relative justify-between bg-[oklch(from_var(--card)_l_c_h_/_0.3)]",
    "shadow-[0_0_0_1px_oklch(from_var(--border)_l_c_h_/_0.2),var(--shadow-border-stack)]",
  ],
  {
    variants: {
      variant: {
        default: "",
        lift: [
          "gap-0 overflow-hidden pb-0",
          "[&_.card-content]:px-6 [&_.card-content]:pt-6 [&_.card-content]:pb-8",
          "[&_.card-content]:transition-all [&_.card-content]:duration-[250ms] [&_.card-content]:ease-[var(--ease-in-out-quad)]",
          "[&_.card-content]:-mx-6 [&_.card-content]:w-[calc(100%+3rem)] [&_.card-content]:bg-[var(--mix-card-33-bg)]",
          "max-md:[&_.card-image]:-mx-6 max-md:[&_.card-image]:w-[calc(100%+3rem)]",
          "[&_.card-footer]:opacity-0 [&_.card-footer]:transition-all",
          "[&_.card-footer]:!duration-[250ms] [&_.card-footer]:!ease-[var(--ease-in-out-quad)]",
          "[&_.card-footer]:absolute [&_.card-footer]:right-6 [&_.card-footer]:bottom-0 [&_.card-footer]:left-6",
          "[&_.card-image]:transition-all [&_.card-image]:duration-[250ms] [&_.card-image]:ease-[var(--ease-in-out-quad)]",
          "hover:[&_.card-content]:-translate-y-8",
          "hover:[&_.card-footer]:-translate-y-4 hover:[&_.card-footer]:opacity-100",
          "hover:[&_.card-image]:scale-[1.04]",
        ],
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
);

/**
 * Card component for displaying content in a contained layout.
 *
 * @param variant - The visual style of the card
 *   - `"default"` - Standard card appearance
 *   - `"lift"` - Animated card with hover effects. On hover, the image scales up, content and footer animate with elevation effects
 * @param className - Optional CSS class names
 *
 * @example
 * ```tsx
 * // Standard card
 * <Card>
 *   <CardHeader>
 *     <CardTitle>Title</CardTitle>
 *   </CardHeader>
 *   <CardContent>Content goes here</CardContent>
 * </Card>
 *
 * // Lift variant with hover animations
 * <Card variant="lift">
 *   <CardImage src="/image.jpg" alt="Image" />
 *   <CardContent>
 *     <CardTitle>Title</CardTitle>
 *   </CardContent>
 *   <CardFooter>Footer content</CardFooter>
 * </Card>
 * ```
 */
function Card({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof cardVariants>) {
  return <div className={cn(cardVariants({ variant }), className)} data-slot="card" {...props} />;
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      className={cn("grid max-w-full grid-cols-[1fr_auto] items-center gap-2", className)}
      data-slot="card-header"
      {...props}
    />
  );
}

function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      className={cn("m-0 font-medium text-xl leading-none tracking-tight", "max-sm:text-lg", className)}
      data-slot="card-title"
      {...props}
    />
  );
}

function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      className={cn(
        "col-span-full m-0 whitespace-normal text-muted-foreground text-sm leading-5",
        "max-sm:text-[0.9375rem] max-sm:leading-[1.5]",
        className
      )}
      data-slot="card-description"
      {...props}
    />
  );
}

function CardContent({ className, ...props }: React.ComponentProps<"div">) {
  return <div className={cn("card-content flex flex-col gap-3", className)} data-slot="card-content" {...props} />;
}

function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
  return <div className={cn("card-footer flex items-center", className)} data-slot="card-footer" {...props} />;
}

/**
 * CardImage component for displaying images within a Card. Uses Next Image.
 *
 * @param src - The image source URL
 * @param alt - Alternative text for the image (required for accessibility)
 * @param className - Optional CSS class names
 *
 * @example
 * ```tsx
 * <Card>
 *   <CardImage src="/scene.jpg" alt="Mountain landscape" />
 *   <CardContent>...</CardContent>
 * </Card>
 * ```
 */
function CardImage({
  className,
  src,
  alt,
  ...props
}: {
  className?: string;
  src: string;
  alt: string;
} & Omit<React.ComponentProps<typeof Image>, "src" | "alt" | "width" | "height" | "children">) {
  return (
    <Image
      alt={alt}
      className={cn(
        "card-image box-border aspect-video w-full max-w-none object-cover",
        "-mt-6 mx-0 mb-0 h-[250px]",
        "md:-mt-6 md:-mx-6 md:mb-0 md:h-[300px] md:w-[calc(100%+3rem)] md:max-w-[100vw]",
        "rounded-t-[var(--radius)] transition-transform duration-[250ms] ease-[var(--ease-in-out-quad)]",
        className
      )}
      data-slot="card-image"
      height={300}
      src={src}
      width={300}
      {...props}
    />
  );
}

/**
 * CardImageContent component for overlaying content on top of a CardImage.
 * Creates an absolutely positioned overlay with a gradient background for text readability.
 *
 * @param className - Optional CSS class names
 * @param children - Content to display in the overlay (typically text or CTAs)
 *
 * @example
 * ```tsx
 * <Card>
 *   <CardImage src="/scene.jpg" alt="Mountain landscape" />
 *   <CardImageContent>
 *     <h2>Mountain Adventure</h2>
 *     <p>Explore the peaks</p>
 *   </CardImageContent>
 *   <CardContent>...</CardContent>
 * </Card>
 * ```
 */
function CardImageContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      className={cn(
        "absolute inset-0 flex flex-col justify-end",
        "bg-gradient-to-b from-transparent to-black/60 text-white",
        className
      )}
      data-slot="card-image-content"
      {...props}
    />
  );
}

function CardIcon({ className, children, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      className={cn(
        "rounded-[var(--radius)] bg-muted",
        "shadow-[0_0_0_1px_oklch(from_var(--border)_l_c_h_/_0.5),0_1px_2px_rgba(0,0,0,0.04),inset_0_0_0_1px_rgba(255,255,255,0.02)]",
        "relative z-10 flex items-center justify-center",
        "mb-[1.125rem] h-8 w-8 p-1.5",
        "max-sm:mb-4 max-sm:h-7 max-sm:w-7",
        className
      )}
      data-slot="card-icon"
      {...props}
    >
      {children}
    </div>
  );
}

function CardAction({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      className={cn("col-start-2 row-start-1 self-start justify-self-end", className)}
      data-slot="card-action"
      {...props}
    />
  );
}

export {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardIcon,
  CardImage,
  CardImageContent,
  CardTitle,
};

Installation

npx shadcn@latest add @roiui/card-tailwind

Usage

import { CardTailwind } from "@/components/card-tailwind"
<CardTailwind />